fix(extract,analyze): filter Swift/Foundation/SwiftUI builtins from resolution and god-node ranking (#2147)

_LANGUAGE_BUILTIN_GLOBALS and _BUILTIN_NOISE_LABELS covered only JS/TS and
Python, so on Swift codebases framework symbols (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.

- extractors/base.py: add Swift stdlib value types, conformance protocols,
  Foundation types, and SwiftUI View/Color/Font to _LANGUAGE_BUILTIN_GLOBALS
- analyze.py: add the same set plus framework module names (Foundation,
  SwiftUI, UIKit, AppKit, Combine) to _BUILTIN_NOISE_LABELS
- extract.py: _resolve_swift_member_calls now skips builtin receiver types,
  matching the guard the TS/Python member-call resolvers already have (#1726)
- tests: god_nodes exclusion (parametrized) + Swift builtin-receiver
  no-bind regression + user-type-still-resolves guard

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
MasterFede5
2026-07-25 15:44:50 +01:00
committed by safishamsi
co-authored by Claude Fable 5
parent 6107f14545
commit 33aa89c722
4 changed files with 176 additions and 0 deletions
+8
View File
@@ -18,6 +18,14 @@ _BUILTIN_NOISE_LABELS = frozenset({
"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
+6
View File
@@ -2197,6 +2197,12 @@ def _resolve_swift_member_calls(
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
+19
View File
@@ -29,6 +29,25 @@ _LANGUAGE_BUILTIN_GLOBALS: frozenset[str] = frozenset({
"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",
})
+143
View File
@@ -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"