mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-25 08:11:54 +00:00
fix(go): drop generic type parameters from signature type references
A `[T any]` type parameter is a local binding, not a corpus symbol, but `_go_collect_type_refs` appended the bare `T`/`R` identifiers used in a function signature, struct field, or interface method as type references and ran them through the sourceless-stub path. Every generic function/type 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. TS, C#, and Java already exclude their type parameters. A new `_go_type_param_names` helper collects the names a declaration binds (its own `[T, R]` list and, for a method, the receiver's `Box[T]` arguments — Go methods reuse the receiver's parameters), and `_go_collect_type_refs` skips them. Genuine user types and the generic container head (the `Box` in a `Box[T]` return) still reference.
This commit is contained in:
1 parent
7536dc0665
commit
434eb689c8
2 files changed
+142
-14
No files matched your search
+68
-14
@@ -48,14 +48,59 @@ _GO_PREDECLARED_FUNCS = frozenset({
|
||||
"recover",
|
||||
})
|
||||
|
||||
def _go_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]]) -> None:
|
||||
"""Walk a Go type expression; append (name, role) tuples."""
|
||||
def _go_type_param_names(decl_node, source: bytes) -> frozenset[str]:
|
||||
"""Names bound by a declaration's generic type parameters.
|
||||
|
||||
Covers a function/type's own `[T any, R comparable]` list and, for a method,
|
||||
the receiver's `(b Box[T])` type arguments (Go methods cannot declare their
|
||||
own type parameters; they reuse the receiver's). These names are local
|
||||
bindings, not corpus symbols, so a signature's use of them must not mint a
|
||||
sourceless stub.
|
||||
"""
|
||||
names: set[str] = set()
|
||||
for child in decl_node.children:
|
||||
if child.type != "type_parameter_list":
|
||||
continue
|
||||
for decl in child.children:
|
||||
if decl.type != "type_parameter_declaration":
|
||||
continue
|
||||
for c in decl.children:
|
||||
if c.type == "identifier":
|
||||
text = _read_text(c, source)
|
||||
if text:
|
||||
names.add(text)
|
||||
receiver = decl_node.child_by_field_name("receiver")
|
||||
if receiver is not None:
|
||||
stack = [receiver]
|
||||
while stack:
|
||||
n = stack.pop()
|
||||
if n.type == "type_arguments":
|
||||
leaves = [n]
|
||||
while leaves:
|
||||
leaf = leaves.pop()
|
||||
if leaf.type == "type_identifier":
|
||||
text = _read_text(leaf, source)
|
||||
if text:
|
||||
names.add(text)
|
||||
leaves.extend(leaf.children)
|
||||
continue
|
||||
stack.extend(n.children)
|
||||
return frozenset(names)
|
||||
|
||||
|
||||
def _go_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[str, str]],
|
||||
skip: frozenset[str] = frozenset()) -> None:
|
||||
"""Walk a Go type expression; append (name, role) tuples.
|
||||
|
||||
Names in ``skip`` (the enclosing declaration's generic type parameters) are
|
||||
local bindings, not references, and are dropped.
|
||||
"""
|
||||
if node is None:
|
||||
return
|
||||
t = node.type
|
||||
if t == "type_identifier":
|
||||
text = _read_text(node, source)
|
||||
if text and text not in _GO_PREDECLARED_TYPES:
|
||||
if text and text not in _GO_PREDECLARED_TYPES and text not in skip:
|
||||
out.append((text, "generic_arg" if generic else "type"))
|
||||
return
|
||||
if t == "qualified_type":
|
||||
@@ -69,24 +114,24 @@ def _go_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[st
|
||||
type_field = node.child_by_field_name("type")
|
||||
if type_field is not None:
|
||||
sub: list[tuple[str, str]] = []
|
||||
_go_collect_type_refs(type_field, source, generic, sub)
|
||||
_go_collect_type_refs(type_field, source, generic, sub, skip)
|
||||
out.extend(sub)
|
||||
for c in node.children:
|
||||
if c.type == "type_arguments":
|
||||
for arg in c.children:
|
||||
if arg.is_named:
|
||||
_go_collect_type_refs(arg, source, True, out)
|
||||
_go_collect_type_refs(arg, source, True, out, skip)
|
||||
return
|
||||
if t in ("pointer_type", "slice_type", "array_type", "map_type",
|
||||
"channel_type", "parenthesized_type"):
|
||||
for c in node.children:
|
||||
if c.is_named:
|
||||
_go_collect_type_refs(c, source, generic, out)
|
||||
_go_collect_type_refs(c, source, generic, out, skip)
|
||||
return
|
||||
if node.is_named:
|
||||
for c in node.children:
|
||||
if c.is_named:
|
||||
_go_collect_type_refs(c, source, generic, out)
|
||||
_go_collect_type_refs(c, source, generic, out, skip)
|
||||
|
||||
def extract_go(path: Path) -> dict:
|
||||
"""Extract functions, methods, type declarations, and imports from a .go file."""
|
||||
@@ -171,7 +216,13 @@ def extract_go(path: Path) -> dict:
|
||||
})
|
||||
return nid
|
||||
|
||||
def emit_go_method_refs(func_node, func_nid: str, line: int) -> None:
|
||||
def emit_go_method_refs(func_node, func_nid: str, line: int,
|
||||
type_params: frozenset[str] = frozenset()) -> None:
|
||||
# The signature's own generic type parameters (and, for a method, those
|
||||
# bound by the receiver or an enclosing generic interface) are local
|
||||
# bindings, not references — dropping them keeps a `[T any]` signature
|
||||
# from minting a sourceless `T` god node every generic function shares.
|
||||
skip = type_params | _go_type_param_names(func_node, source)
|
||||
params = func_node.child_by_field_name("parameters")
|
||||
if params is not None:
|
||||
for p in params.children:
|
||||
@@ -179,7 +230,7 @@ def extract_go(path: Path) -> dict:
|
||||
continue
|
||||
type_node = p.child_by_field_name("type")
|
||||
refs: list[tuple[str, str]] = []
|
||||
_go_collect_type_refs(type_node, source, False, refs)
|
||||
_go_collect_type_refs(type_node, source, False, refs, skip)
|
||||
for ref_name, role in refs:
|
||||
ctx = "generic_arg" if role == "generic_arg" else "parameter_type"
|
||||
tgt = ensure_named_node(ref_name, line)
|
||||
@@ -198,7 +249,7 @@ def extract_go(path: Path) -> dict:
|
||||
type_node = c
|
||||
break
|
||||
refs = []
|
||||
_go_collect_type_refs(type_node, source, False, refs)
|
||||
_go_collect_type_refs(type_node, source, False, refs, skip)
|
||||
for ref_name, role in refs:
|
||||
ctx = "generic_arg" if role == "generic_arg" else "return_type"
|
||||
tgt = ensure_named_node(ref_name, line)
|
||||
@@ -206,7 +257,7 @@ def extract_go(path: Path) -> dict:
|
||||
add_edge(func_nid, tgt, "references", line, context=ctx)
|
||||
else:
|
||||
refs = []
|
||||
_go_collect_type_refs(result, source, False, refs)
|
||||
_go_collect_type_refs(result, source, False, refs, skip)
|
||||
for ref_name, role in refs:
|
||||
ctx = "generic_arg" if role == "generic_arg" else "return_type"
|
||||
tgt = ensure_named_node(ref_name, line)
|
||||
@@ -352,6 +403,9 @@ def extract_go(path: Path) -> dict:
|
||||
type_nid = _make_id(pkg_scope, type_name)
|
||||
add_node(type_nid, type_name, line)
|
||||
add_edge(file_nid, type_nid, "contains", line)
|
||||
# A generic type's own parameters (`type Box[T any]`) are local
|
||||
# bindings; a field or method using `T` must not mint a stub.
|
||||
type_params = _go_type_param_names(child, source)
|
||||
# Type body: struct fields (with embeds) or interface embedding.
|
||||
type_body = None
|
||||
for tc in child.children:
|
||||
@@ -377,7 +431,7 @@ def extract_go(path: Path) -> dict:
|
||||
type_node = fc
|
||||
break
|
||||
refs: list[tuple[str, str]] = []
|
||||
_go_collect_type_refs(type_node, source, False, refs)
|
||||
_go_collect_type_refs(type_node, source, False, refs, type_params)
|
||||
for ref_name, role in refs:
|
||||
tgt = ensure_named_node(ref_name, field.start_point[0] + 1)
|
||||
if tgt == type_nid:
|
||||
@@ -411,7 +465,7 @@ def extract_go(path: Path) -> dict:
|
||||
m_nid = symbol_nid(_make_id(type_nid, m_name), m_name)
|
||||
add_node(m_nid, f".{m_name}()", m_line)
|
||||
add_edge(type_nid, m_nid, "method", m_line)
|
||||
emit_go_method_refs(elem, m_nid, m_line)
|
||||
emit_go_method_refs(elem, m_nid, m_line, type_params)
|
||||
continue
|
||||
if elem.type != "type_elem":
|
||||
continue
|
||||
@@ -428,7 +482,7 @@ def extract_go(path: Path) -> dict:
|
||||
refs = []
|
||||
for sub in elem.children:
|
||||
if sub.is_named:
|
||||
_go_collect_type_refs(sub, source, False, refs)
|
||||
_go_collect_type_refs(sub, source, False, refs, type_params)
|
||||
for ref_name, role in refs:
|
||||
tgt = ensure_named_node(ref_name, elem.start_point[0] + 1)
|
||||
if tgt == type_nid:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Go generic type parameters must not mint sourceless god nodes.
|
||||
|
||||
A `[T any]` type parameter is a local binding, not a corpus symbol. The Go
|
||||
type-ref pass appended the bare `T`/`R` identifiers used in a signature, field,
|
||||
or interface method as type references and ran them through the sourceless-stub
|
||||
path, so every generic function/type in the corpus collapsed its `T` onto one
|
||||
bare `t` node — a god node that pollutes god-node rankings (single-letter type
|
||||
params are not in the analyzer's builtin-noise filter). TS, C#, and Java
|
||||
already exclude their type parameters; Go 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_function_type_params_mint_no_stub(tmp_path):
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"func Map[T any, R comparable](x T, f func(T) R) R {\n"
|
||||
" var r R\n"
|
||||
" return r\n"
|
||||
"}\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
assert _sourceless(r) == []
|
||||
assert not any(e.get("target") in ("t", "r") for e in r["edges"])
|
||||
|
||||
|
||||
def test_generic_struct_and_method_type_params_mint_no_stub(tmp_path):
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type Box[T any] struct { v T }\n"
|
||||
"func (b Box[T]) Get() T { return b.v }\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
assert _sourceless(r) == []
|
||||
assert not any(e.get("target") == "t" for e in r["edges"])
|
||||
|
||||
|
||||
def test_generic_interface_type_params_mint_no_stub(tmp_path):
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type Container[T any] interface {\n"
|
||||
" Add(item T)\n"
|
||||
" Items() []T\n"
|
||||
"}\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
assert _sourceless(r) == []
|
||||
assert not any(e.get("target") == "t" for e in r["edges"])
|
||||
|
||||
|
||||
def test_user_and_container_types_still_referenced(tmp_path):
|
||||
# The guard drops only the declared type parameter; a real user type and the
|
||||
# generic container head it instantiates still reference.
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type User struct { name string }\n"
|
||||
"type Box[T any] struct { v T }\n"
|
||||
"func Wrap[T any](u User) Box[T] { return Box[T]{} }\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
targets = {
|
||||
r_n["label"]
|
||||
for e in r["edges"] if e.get("relation") == "references"
|
||||
for r_n in r["nodes"] if r_n["id"] == e["target"]
|
||||
}
|
||||
assert "User" in targets
|
||||
assert "Box" in targets
|
||||
assert "T" not in targets
|
||||
Reference in new issue
Block a user