Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions graphify/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
"Callable", "Type", "ClassVar", "Final", "Literal", "Protocol",
"Counter", "defaultdict", "OrderedDict", "datetime", "Enum",
"os", "sys", "re", "json", "io", "abc", "typing",
# Swift / Foundation / SwiftUI framework symbols and module imports that
# otherwise dominate god-node rankings on Swift codebases (#2147)
"Foundation", "SwiftUI", "UIKit", "AppKit", "Combine",
"String", "Int", "Double", "Float", "Bool", "Data", "URL", "Date", "UUID",
"Sendable", "Codable", "Decodable", "Encodable", "Equatable", "Hashable",
"Identifiable", "Comparable", "AnyObject", "Error", "LocalizedError",
"NSObject", "NSString", "NSError", "NSLock",
"View", "Color", "Font", "DispatchQueue",
})

# Language families — extensions sharing a runtime can legitimately call each other
Expand Down
6 changes: 6 additions & 0 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -2191,6 +2191,12 @@ def _key(label: str) -> str:
type_qualified = False
if not type_name:
continue
# A builtin receiver type (Data, NSLock, DispatchQueue, ...) must not
# resolve to a same-named user symbol — the cross-file CALL resolver and
# the TS/Python member-call resolvers already skip these globals (#1726);
# do the same for Swift (#2147).
if type_name in _LANGUAGE_BUILTIN_GLOBALS:
continue
type_defs = type_def_nids.get(_key(type_name), [])
if len(type_defs) != 1: # ambiguous or absent -> bail (god-node guard)
continue
Expand Down
19 changes: 19 additions & 0 deletions graphify/extractors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@
"print", "open", "isinstance", "type", "super", "sorted", "reversed",
"any", "all", "abs", "round", "next", "iter", "hash", "id", "repr",
"callable", "getattr", "setattr", "hasattr", "delattr", "vars", "dir",
# Swift standard library / Foundation / SwiftUI (#2147). Value-type
# initializers (Data(x), Int(x), UUID()) and protocol conformance targets
# appear from virtually every file of a Swift codebase, exactly like the
# ECMAScript constructors above. String/Date/URL/Error are already listed.
"Int", "Int8", "Int16", "Int32", "Int64",
"UInt", "UInt8", "UInt16", "UInt32", "UInt64",
"Double", "Float", "Bool", "Character",
"Sendable", "Codable", "Decodable", "Encodable", "Equatable", "Hashable",
"Identifiable", "Comparable", "CaseIterable", "RawRepresentable",
"CustomStringConvertible", "CustomDebugStringConvertible", "AnyObject",
"LocalizedError",
"Data", "UUID", "Decimal", "Calendar", "Locale", "TimeZone", "Bundle",
"IndexPath", "IndexSet", "NotificationCenter", "UserDefaults",
"FileManager", "URLSession", "URLRequest", "URLComponents",
"JSONDecoder", "JSONEncoder", "DateFormatter", "NumberFormatter",
"ISO8601DateFormatter",
"NSObject", "NSString", "NSError", "NSLock", "NSAttributedString",
"DispatchQueue", "DispatchGroup", "OperationQueue", "RunLoop",
"View", "Color", "Font",
})


Expand Down
143 changes: 143 additions & 0 deletions tests/test_swift_builtin_noise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
"""Swift/Foundation/SwiftUI builtins must not become god nodes or bind to user symbols.

#2147: `_LANGUAGE_BUILTIN_GLOBALS` and `_BUILTIN_NOISE_LABELS` covered only
JS/TS and Python, so on Swift codebases framework types (`Foundation`,
`NSLock`, `View`, `Data`, `Sendable`, ...) ranked as god nodes and the Swift
member-call resolver could bind a builtin-typed receiver (`let d: Data`) to a
same-named user symbol in another file — the same phantom-edge shape #1726
fixed for TypeScript.
"""
import networkx as nx
import pytest

from graphify.analyze import god_nodes
from graphify.extract import extract


def _labels_by_id(r):
return {n["id"]: n.get("label") for n in r["nodes"]}


@pytest.mark.parametrize("builtin_label", [
"Foundation", "SwiftUI", "NSLock", "Data", "View", "Sendable", "Codable",
"DispatchQueue", "Color",
])
def test_god_nodes_excludes_swift_builtin_labels(builtin_label: str) -> None:
"""Swift framework symbols must be filtered from god_nodes output.

Constructs a graph where the builtin-labelled node has the highest degree
(the #2147 report shape: `Foundation` at 105 edges, `NSLock` at 102) and a
real project abstraction has lower degree. The builtin must be excluded and
the project symbol kept.

Args:
builtin_label: The Swift/Foundation/SwiftUI label to test (parametrized).
"""
G = nx.Graph()
G.add_node(
"real_node",
label="AudioStreamer",
source_file="Sources/AudioStreamer.swift",
file_type="code",
source_location="L1",
)
G.add_node(
"builtin_node",
label=builtin_label,
source_file="",
file_type="code",
source_location="",
)
for i in range(20):
peer = f"user_type_{i}"
G.add_node(
peer,
label=f"Feature{i}",
source_file=f"Sources/Feature{i}.swift",
file_type="code",
source_location="L1",
)
G.add_edge(
peer,
"builtin_node",
relation="references",
confidence="EXTRACTED",
source_file=f"Sources/Feature{i}.swift",
weight=1.0,
)
G.add_edge(
"real_node",
"user_type_0",
relation="calls",
confidence="EXTRACTED",
source_file="Sources/AudioStreamer.swift",
weight=1.0,
)

result = god_nodes(G, top_n=10)
result_ids = [r["id"] for r in result]

assert "builtin_node" not in result_ids, (
f"god_nodes() should filter Swift builtin '{builtin_label}' "
f"but it appeared in the result: {result}"
)
assert "real_node" in result_ids, (
f"god_nodes() should include project symbol 'AudioStreamer' "
f"but it was absent: {result}"
)


def test_swift_builtin_receiver_does_not_bind_to_user_symbol(tmp_path):
# #2147 (same shape as #1726 for TS): a receiver typed with a builtin
# (`let payload: Data`) must not have its member calls bound to a user type
# that happens to be named `Data` in another file.
(tmp_path / "Model.swift").write_text(
"class Data {\n"
" func append(_ s: String) {}\n"
"}\n")
(tmp_path / "Uploader.swift").write_text(
"class Uploader {\n"
" let payload: Data = Data()\n"
" func send() {\n"
" payload.append(\"x\")\n"
" }\n"
"}\n")
r = extract(sorted(tmp_path.glob("*.swift")), cache_root=tmp_path, parallel=False)
lbl = _labels_by_id(r)
by_id = {n["id"]: n for n in r["nodes"]}
data_ids = [n["id"] for n in r["nodes"]
if n.get("label") == "Data"
and str(n.get("source_file", "")).endswith("Model.swift")]
assert data_ids, "the user class Data must still exist as a node"
for e in r["edges"]:
if e.get("target") in data_ids and e.get("relation") in ("calls", "references") \
and e.get("context") == "call":
src_sf = str(by_id.get(e["source"], {}).get("source_file", ""))
assert not src_sf.endswith("Uploader.swift"), (
f"builtin-typed receiver bound to user Data: "
f"{lbl.get(e['source'])!r} -> Data ({e})"
)


def test_swift_user_receiver_type_still_resolves(tmp_path):
# Guard must be a no-op for genuine user types: a member call on a
# user-typed property still resolves cross-file (#1356 inference table).
(tmp_path / "Engine.swift").write_text(
"class AudioEngine {\n"
" func play() {}\n"
"}\n")
(tmp_path / "Player.swift").write_text(
"class Player {\n"
" let engine: AudioEngine = AudioEngine()\n"
" func start() {\n"
" engine.play()\n"
" }\n"
"}\n")
r = extract(sorted(tmp_path.glob("*.swift")), cache_root=tmp_path, parallel=False)
lbl = _labels_by_id(r)
resolved = {
(lbl.get(e["source"]), lbl.get(e["target"]))
for e in r["edges"]
if e.get("context") == "call" and "play" in str(lbl.get(e.get("target"), "")).lower()
}
assert resolved, "user-typed member call must still resolve cross-file"