From 786e8868a0fe6679d960e5ed2a8f57eeac2cf8e3 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Tue, 13 Oct 2026 13:46:57 +0530 Subject: [PATCH] fix(extract): drop Swift generic type parameters from references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `struct Stack` / `func map(...)` binds `Element`/`R` as local names, not corpus symbols, but `_swift_collect_type_refs` appended the bare identifiers used in a signature or stored-property type as type references and ran them through the sourceless-stub path. Every generic type/function in the corpus then collapsed its `T` onto one bare `t` node — a god node that pollutes god-node rankings, since single-letter type parameters are not in the analyzer's builtin-noise filter. C#, Java, TypeScript, Kotlin, Scala, C++, and Go already exclude theirs. Mirrors that pattern (and complements the `Self` keyword fix already in this collector): a new `_swift_type_parameters_in_scope` helper walks up to every enclosing declaration's `type_parameters` list and takes each `type_parameter`'s first `type_identifier` as the bound name (a constraint like `T: Equatable` is a separate `user_type`, left to reference), and `_swift_collect_type_refs` auto-computes and threads that skip set. Genuine user types and the `Int`/ `String` builtin hubs are unaffected. (cherry picked from commit 2e8a5c0f97820a67447192929a3b0c268bf7f783) --- graphify/extractors/engine.py | 51 +++++++++++++++++++++---- tests/test_swift_generic_type_params.py | 48 +++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 tests/test_swift_generic_type_params.py diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index c4388d861..414a948e5 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -1193,28 +1193,63 @@ def _swift_user_type_name(user_type_node, source: bytes) -> str | None: return text or None return None -def _swift_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: - """Walk a Swift type expression; append (name, role) tuples (role 'type' or 'generic_arg').""" +def _swift_type_parameters_in_scope(node, source: bytes) -> frozenset[str]: + """Return Swift generic type-parameter names visible from ``node``. + + A `struct Stack` / `func map(...)` binds `Element`/`R` as local + names, not corpus symbols. Walking up to every enclosing declaration's + `type_parameters` list collects them; a parameter's name is the first + `type_identifier` of its `type_parameter` node (a constraint like + `T: Equatable` is a separate `user_type` and is left to reference). + """ + names: set[str] = set() + scope = node + while scope is not None: + for child in scope.children: + if child.type != "type_parameters": + continue + for param in child.children: + if param.type != "type_parameter": + continue + for sub in param.children: + if sub.type == "type_identifier": + name = _read_text(sub, source) + if name: + names.add(name) + break + scope = scope.parent + return frozenset(names) + + +def _swift_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]], + skip: frozenset[str] | None = None) -> None: + """Walk a Swift type expression; append (name, role) tuples (role 'type' or 'generic_arg'). + + Generic type parameters in scope (``skip``) are local bindings, not + references; emitting one mints a sourceless stub (the bare `t` god node a + `` type or function would otherwise share corpus-wide).""" if node is None: return + if skip is None: + skip = _swift_type_parameters_in_scope(node, source) t = node.type if t == "type_annotation": for c in node.children: if c.is_named: - _swift_collect_type_refs(c, source, generic, out) + _swift_collect_type_refs(c, source, generic, out, skip) return if t == "user_type": for c in node.children: if c.type == "type_identifier": text = _read_text(c, source) - if text and text != "Self": + if text and text != "Self" and text not in skip: out.append((text, "generic_arg" if generic else "type")) break for c in node.children: if c.type == "type_arguments": for arg in c.children: if arg.is_named: - _swift_collect_type_refs(arg, source, True, out) + _swift_collect_type_refs(arg, source, True, out, skip) return if t == "type_identifier": text = _read_text(node, source) @@ -1223,19 +1258,19 @@ def _swift_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple # method collapses onto (a god node), the same keyword-type pitfall as # Rust's `Self`. A genuine type (including `Int`/`String` builtin hubs) # is unaffected. - if text and text != "Self": + if text and text != "Self" and text not in skip: out.append((text, "generic_arg" if generic else "type")) return if t in ("optional_type", "implicitly_unwrapped_optional_type", "array_type", "dictionary_type", "tuple_type"): for c in node.children: if c.is_named: - _swift_collect_type_refs(c, source, generic, out) + _swift_collect_type_refs(c, source, generic, out, skip) return if node.is_named: for c in node.children: if c.is_named: - _swift_collect_type_refs(c, source, generic, out) + _swift_collect_type_refs(c, source, generic, out, skip) def _swift_property_type_node(property_node): """Return the type_annotation child of a Swift property_declaration, if any.""" diff --git a/tests/test_swift_generic_type_params.py b/tests/test_swift_generic_type_params.py new file mode 100644 index 000000000..204e24580 --- /dev/null +++ b/tests/test_swift_generic_type_params.py @@ -0,0 +1,48 @@ +"""Swift generic type parameters must not mint sourceless god nodes. + +A `struct Stack` / `func map(...)` binds `Element`/`R` as local +names, not corpus symbols. The Swift type-ref pass appended the bare +identifiers used in a signature or stored-property type as type references and +ran them through the sourceless-stub path, so every generic type/function in +the corpus collapsed its `T` onto one bare `t` node — a god node (single-letter +type params are not in the analyzer's builtin-noise filter). C#, Java, +TypeScript, Kotlin, Scala, C++, and Go already exclude their type parameters; +Swift now matches. +""" +from pathlib import Path + +from graphify.extract import extract + + +def _sourceless(r): + return sorted({(n["id"], n.get("label")) for n in r["nodes"] if not n.get("source_file")}) + + +def test_generic_struct_and_method_type_params_mint_no_stub(tmp_path): + (tmp_path / "s.swift").write_text( + "struct Stack {\n" + " var items: [Element]\n" + " func peek() -> Element? { return items.last }\n" + " func map(_ f: (Element) -> R) -> [R] { return [] }\n" + "}\n" + ) + r = extract([tmp_path / "s.swift"], cache_root=tmp_path, parallel=False) + assert _sourceless(r) == [] + assert not any(e.get("target") in ("element", "r") for e in r["edges"]) + + +def test_constrained_param_dropped_but_real_type_kept(tmp_path): + # `T` is a parameter (dropped); a genuine user type in the signature still + # references. + (tmp_path / "s.swift").write_text( + "struct User { var id: Int }\n" + "func wrap(_ x: T, u: User) -> User { return u }\n" + ) + r = extract([tmp_path / "s.swift"], cache_root=tmp_path, parallel=False) + assert not any(e.get("target") == "t" for e in r["edges"]) + refs = { + rn["label"] + for e in r["edges"] if e.get("relation") == "references" + for rn in r["nodes"] if rn["id"] == e["target"] + } + assert "User" in refs