mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 00:19:33 +00:00
fix(go): normalize a generic receiver type before building its id
(b *Box[T]) reads its receiver's type field as "Box[T]" verbatim, but
the type declaration's own id is keyed on the bare name alone: type
Box[T any] struct {...} captures [T any] as a separate
type_parameter_list, never part of the name. Without stripping the
instantiation before that text turns into an id, every method on a
generic type attached to a node disconnected from the type itself --
Box's own node carried no method edges at all, even though
go_type_table-based call resolution could still reach a method through
the receiver's own-function self_type binding.
Adds _go_strip_generic_args and applies it everywhere a receiver's raw
type text is turned into an id: the live walk() pass that attaches a
method to its owner, and the separate case-collision pre-scan
(_receiver_type_of) that has to agree with it or case-only-collision
salting silently breaks for generic types.
Found while verifying the already-merged `go_type_table` member-call
resolution (deca9fa3 / cherry-picked as 1695efae) against the edge
cases from #3368 -- this is the one gap left in that fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(cherry picked from commit 1a8a04ac50)
This commit is contained in:
1 parent
7f04857f01
commit
1331be175c
2 files changed
+112
-2
No files matched your search
@@ -133,6 +133,22 @@ def _go_collect_type_refs(node, source: bytes, generic: bool, out: list[tuple[st
|
||||
if c.is_named:
|
||||
_go_collect_type_refs(c, source, generic, out, skip)
|
||||
|
||||
def _go_strip_generic_args(type_text: str) -> str:
|
||||
"""Drop a generic instantiation's type arguments from a receiver type's text.
|
||||
|
||||
``(b *Box[T])`` reads its ``type`` field as ``*Box[T]`` verbatim (after the
|
||||
leading ``*`` is stripped, ``Box[T]``), but the type declaration's own id
|
||||
is keyed on the bare name alone: ``type Box[T any] struct {...}`` captures
|
||||
``[T any]`` as a separate ``type_parameter_list``, never part of the name.
|
||||
Without stripping this before building an id from it, a generic receiver's
|
||||
id never matched its own type's id, so a generic type's methods attached
|
||||
to a node disconnected from the type itself (#3368) — this must run
|
||||
everywhere a receiver's raw type text turns into an id, not only one of
|
||||
them, or the two computations silently disagree.
|
||||
"""
|
||||
return type_text.split("[", 1)[0].strip()
|
||||
|
||||
|
||||
def _go_single_type_name(node, source: bytes) -> str | None:
|
||||
"""The one declared type a Go type expression names, or None.
|
||||
|
||||
@@ -351,7 +367,9 @@ def extract_go(path: Path) -> dict:
|
||||
if param.type == "parameter_declaration":
|
||||
type_node = param.child_by_field_name("type")
|
||||
if type_node:
|
||||
return _read_text(type_node, source).lstrip("*").strip()
|
||||
return _go_strip_generic_args(
|
||||
_read_text(type_node, source).lstrip("*").strip()
|
||||
)
|
||||
break
|
||||
return None
|
||||
|
||||
@@ -430,7 +448,9 @@ def extract_go(path: Path) -> dict:
|
||||
if param.type == "parameter_declaration":
|
||||
type_node = param.child_by_field_name("type")
|
||||
if type_node:
|
||||
receiver_type = _read_text(type_node, source).lstrip("*").strip()
|
||||
receiver_type = _go_strip_generic_args(
|
||||
_read_text(type_node, source).lstrip("*").strip()
|
||||
)
|
||||
name_node = param.child_by_field_name("name")
|
||||
if name_node:
|
||||
receiver_name = _read_text(name_node, source)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""A generic type's methods must attach to the type's own node (#3368).
|
||||
|
||||
`(b *Box[T]) Get()` reads its receiver's `type` field as `Box[T]` verbatim,
|
||||
but the type declaration's own id is the bare `Box`: `type Box[T any] struct
|
||||
{...}` captures `[T any]` as a separate `type_parameter_list`, never part of
|
||||
the name. Without stripping the instantiation before the receiver's text
|
||||
turns into an id, `Box`'s own node and the method's owner node were two
|
||||
different, disconnected ids — `graphify explain Box` (or anything else
|
||||
walking `method` edges from the type) found no methods at all, even though
|
||||
`go_type_table`-based call resolution could still reach them through the
|
||||
receiver's own-function `self_type` binding.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _method_owner(r, method_label: str) -> str | None:
|
||||
target = next(n["id"] for n in r["nodes"] if n["label"] == method_label)
|
||||
return next(
|
||||
(e["source"] for e in r["edges"]
|
||||
if e["relation"] == "method" and e["target"] == target),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def test_generic_receiver_method_attaches_to_its_own_type(tmp_path):
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type Box[T any] struct { val T }\n"
|
||||
"func (b *Box[T]) Get() T { return b.val }\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
box_nid = next(n["id"] for n in r["nodes"] if n["label"] == "Box")
|
||||
assert _method_owner(r, ".Get()") == box_nid
|
||||
|
||||
|
||||
def test_value_receiver_without_a_pointer_also_attaches_to_its_own_type(tmp_path):
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type Box[T any] struct { val T }\n"
|
||||
"func (b Box[T]) Get() T { return b.val }\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
box_nid = next(n["id"] for n in r["nodes"] if n["label"] == "Box")
|
||||
assert _method_owner(r, ".Get()") == box_nid
|
||||
|
||||
|
||||
def test_a_generic_type_mints_exactly_one_callable_class_node(tmp_path):
|
||||
# Before the fix, the type declaration and the method's owner were two
|
||||
# separate `_callable_class` nodes ("Box" and "Box[T]"): the same type
|
||||
# appeared twice, once with no methods.
|
||||
(tmp_path / "g.go").write_text(
|
||||
"package p\n"
|
||||
"type Box[T any] struct { val T }\n"
|
||||
"func (b *Box[T]) Get() T { return b.val }\n"
|
||||
"func (b *Box[T]) Set(v T) { b.val = v }\n"
|
||||
)
|
||||
r = extract([tmp_path / "g.go"], cache_root=tmp_path, parallel=False)
|
||||
box_nodes = [n for n in r["nodes"] if n["label"].startswith("Box")]
|
||||
assert [n["label"] for n in box_nodes] == ["Box"]
|
||||
|
||||
|
||||
def test_generic_receivers_concrete_field_chain_resolves_through_its_own_type(tmp_path):
|
||||
# A generic type can still have a field of a concrete, non-type-parameter
|
||||
# type; a call through it must resolve to the right target even though
|
||||
# "Print" is ambiguous corpus-wide (two types both declare it).
|
||||
(tmp_path / "box.go").write_text(
|
||||
"package p\n"
|
||||
"type Box[T any] struct {\n\tval T\n\tmeta *Metadata\n}\n"
|
||||
"func (b *Box[T]) Describe() { b.meta.Print() }\n"
|
||||
)
|
||||
(tmp_path / "metadata.go").write_text(
|
||||
"package p\ntype Metadata struct{}\nfunc (m *Metadata) Print() {}\n"
|
||||
)
|
||||
(tmp_path / "other.go").write_text(
|
||||
"package p\ntype Other struct{}\nfunc (o *Other) Print() {}\n"
|
||||
)
|
||||
r = extract(
|
||||
[tmp_path / "box.go", tmp_path / "metadata.go", tmp_path / "other.go"],
|
||||
cache_root=tmp_path, parallel=False,
|
||||
)
|
||||
label_of = {n["id"]: n["label"] for n in r["nodes"]}
|
||||
source_file_of = {n["id"]: str(n.get("source_file") or "") for n in r["nodes"]}
|
||||
calls = [
|
||||
e for e in r["edges"]
|
||||
if e["relation"] == "calls" and label_of.get(e["target"]) == ".Print()"
|
||||
]
|
||||
assert len(calls) == 1
|
||||
assert source_file_of[calls[0]["target"]].endswith("metadata.go")
|
||||
Reference in new issue
Block a user