From e3bf52b3184c036394ea81f6e4431e8808eb2ea7 Mon Sep 17 00:00:00 2001 From: rajatnagda45 Date: Tue, 13 Oct 2026 13:43:45 +0530 Subject: [PATCH] fix(extract): drop C++ template type parameters from references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `template` binds `T` as a local name, not a corpus symbol, but `_cpp_collect_type_refs` appended the bare identifiers used in a template signature or member as type references and ran them through the sourceless-stub path. Every template 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, and Go already exclude theirs. Mirrors that pattern: a new `_cpp_type_parameters_in_scope` helper walks up to every enclosing `template_declaration`'s `template_parameter_list` and collects names from its `type_parameter_declaration`/`variadic_type_parameter_declaration` children (a non-type `int N` parameter is a value and is left alone), and `_cpp_collect_type_refs` auto-computes and threads that skip set. Genuine user types and the generic container head still reference. --- graphify/extractors/engine.py | 52 ++++++++++++++++++++---- tests/test_cpp_template_type_params.py | 55 ++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 7 deletions(-) create mode 100644 tests/test_cpp_template_type_params.py diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 443af97ce..f99ba5c82 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -1345,41 +1345,79 @@ def _c_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str if c.is_named: _c_collect_type_refs(c, source, generic, out) -def _cpp_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None: +def _cpp_type_parameters_in_scope(node, source: bytes) -> frozenset[str]: + """Return C++ template type-parameter names visible from ``node``. + + A `template` binds `T`/`U` as local names, not corpus + symbols. Walking up to every enclosing `template_declaration`'s + `template_parameter_list` collects the names from its + `type_parameter_declaration`/`variadic_type_parameter_declaration` children. + A non-type parameter (`int N`) is a value, not a type, and is left alone. + """ + names: set[str] = set() + scope = node + while scope is not None: + for child in scope.children: + if child.type != "template_parameter_list": + continue + for param in child.children: + if param.type not in ( + "type_parameter_declaration", + "variadic_type_parameter_declaration", + ): + 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 _cpp_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]], + skip: frozenset[str] | None = None) -> None: """Walk a C++ type expression; append (name, role) tuples. Resolves qualified_identifier tails (std::string → string) and template_type - base + arguments (std::vector → vector + HttpClient as generic_arg).""" + base + arguments (std::vector → vector + HttpClient as generic_arg). + + Template type parameters in scope (``skip``) are local bindings, not + references; emitting one mints a sourceless stub (the bare `t` god node a + `template` class or function would otherwise share corpus-wide).""" if node is None or node.type in _C_PRIMITIVE_TYPE_NODES: return + if skip is None: + skip = _cpp_type_parameters_in_scope(node, source) t = node.type if t == "type_identifier": text = _read_text(node, source) - if text: + if text and text not in skip: out.append((text, "generic_arg" if generic else "type")) return if t == "qualified_identifier": name_node = node.child_by_field_name("name") if name_node is not None: - _cpp_collect_type_refs(name_node, source, generic, out) + _cpp_collect_type_refs(name_node, source, generic, out, skip) return if t == "template_type": name_node = node.child_by_field_name("name") if name_node is not None: text = _read_text(name_node, source) - if text: + if text and text not in skip: out.append((text, "generic_arg" if generic else "type")) args_node = node.child_by_field_name("arguments") if args_node is not None: for c in args_node.children: if c.is_named: - _cpp_collect_type_refs(c, source, True, out) + _cpp_collect_type_refs(c, source, True, out, skip) return if t in ("type_descriptor", "pointer_declarator", "reference_declarator", "array_declarator", "type_qualifier", "abstract_pointer_declarator", "abstract_reference_declarator", "abstract_array_declarator"): for c in node.children: if c.is_named: - _cpp_collect_type_refs(c, source, generic, out) + _cpp_collect_type_refs(c, source, generic, out, skip) def _scala_given_synthetic_name(node, source: bytes) -> str | None: """A stable label for an anonymous Scala 3 given (`given Show[Int] with`). diff --git a/tests/test_cpp_template_type_params.py b/tests/test_cpp_template_type_params.py new file mode 100644 index 000000000..fc1006422 --- /dev/null +++ b/tests/test_cpp_template_type_params.py @@ -0,0 +1,55 @@ +"""C++ template type parameters must not mint sourceless god nodes. + +A `template` binds `T` as a local name, not a corpus symbol. The +C++ type-ref pass appended the bare `T`/`K`/`V` identifiers used in a template +signature or member as type references and ran them through the sourceless-stub +path, so every template 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, and Go already exclude their type +parameters; C++ 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_class_and_function_template_params_mint_no_stub(tmp_path): + (tmp_path / "b.cpp").write_text( + "template\n" + "class Box {\n" + " T value;\n" + "public:\n" + " T get() { return value; }\n" + "};\n" + "template\n" + "V lookup(K key) { V v; return v; }\n" + ) + r = extract([tmp_path / "b.cpp"], cache_root=tmp_path, parallel=False) + assert _sourceless(r) == [] + assert not any(e.get("target") in ("t", "k", "v") for e in r["edges"]) + + +def test_non_type_param_and_real_types_unaffected(tmp_path): + # `N` is a non-type (value) template parameter, not a type; a genuine user + # type in the signature still references. + (tmp_path / "b.cpp").write_text( + "struct User { int id; };\n" + "template\n" + "class Buffer {\n" + " T data[N];\n" + "public:\n" + " User owner() { return User(); }\n" + "};\n" + ) + r = extract([tmp_path / "b.cpp"], 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