fix(extract): Go cross-file type refs emit sourceless stubs (#1500)

Go's ensure_named_node emitted a SOURCED stub for a cross-file type reference,
unlike the generic/ObjC/Java extractors which emit sourceless ones. A sourced stub
bakes the referencing file's path into the node id at disambiguation time, which
blocks the corpus-level rewire from collapsing it onto the real definition — so a
type defined in one Go file and referenced from two others produced three nodes
(the real def plus two phantom per-file duplicates). Go now emits a sourceless
stub like the other languages, so the rewire collapses them to a single node.

Ported from PR #1501 by @TPAteeq. Closes the #1402 gap for Go.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mohammed Ateeq
2026-06-27 23:32:55 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent b17e88cc49
commit 36b76ce8e0
3 changed files with 48 additions and 1 deletions
+4
View File
@@ -2,6 +2,10 @@
Full release notes with details on each version: [GitHub Releases](https://github.com/safishamsi/graphify/releases)
## Unreleased
- Fix: the Go AST extractor no longer creates phantom duplicate nodes for cross-file type references — the Go copy of `ensure_named_node` still used the older sourced-stub fallback; it now emits a sourceless stub like the other extractors, extending the #1402 fix to Go (#1500).
## 0.8.50 (2026-06-27)
- Feat: `graphify label --missing-only` relabels only communities that are unnamed or still hold a `Community N` placeholder, preserving existing non-placeholder labels from `.graphify_labels.json` (#1481, thanks @jiangyq9; supersedes #1421 by @matiasduartee, who proposed the same flag). Lets a large graph be relabeled incrementally without re-naming (and paying for) communities that already have good names.
+15 -1
View File
@@ -6472,7 +6472,21 @@ def extract_go(path: Path) -> dict:
return nid
nid = _make_id(name)
if nid not in seen_ids:
add_node(nid, name, line)
# The name isn't declared in this file, so this is a cross-file reference
# (e.g. a type defined in another file of the package). Emit a SOURCELESS
# stub — like the inheritance-base path in the other extractors — so the
# corpus-level rewire can collapse it onto the real definition. A sourced
# stub here makes _disambiguate_colliding_node_ids bake the referencing
# file's path (with extension) into the id and blocks the rewire, which is
# the phantom-duplicate-node bug (#1402).
seen_ids.add(nid)
nodes.append({
"id": nid,
"label": name,
"file_type": "code",
"source_file": "",
"source_location": "",
})
return nid
def emit_go_method_refs(func_node, func_nid: str, line: int) -> None:
+29
View File
@@ -123,6 +123,35 @@ def test_cross_file_type_annotation_refs_resolve_to_single_node(tmp_path):
assert "_py" not in thing_nodes[0]["id"], thing_nodes[0]["id"]
def test_go_cross_file_type_refs_resolve_to_single_node(tmp_path):
"""#1402 (Go): the sourceless-stub fix landed in six extractors but the Go copy
of ``ensure_named_node`` was missed, so a Go type defined once but referenced via
parameter/return types in N sibling files produced 1+N phantom duplicate nodes
with the referencing file's path (extension and all) baked into the id
(e.g. ``pkg_a_go_thing``). Same-package references must resolve to the single
canonical type node instead."""
pkg = tmp_path / "pkg"
pkg.mkdir()
(pkg / "thing.go").write_text(
"package pkg\n\ntype Thing struct{}\n\nfunc (t Thing) Run() int { return 1 }\n",
encoding="utf-8",
)
(pkg / "a.go").write_text(
"package pkg\n\nfunc UseA(obj Thing) Thing { return obj }\n", encoding="utf-8"
)
(pkg / "b.go").write_text(
"package pkg\n\nfunc UseB(obj Thing) Thing { return obj }\n", encoding="utf-8"
)
result = extract([pkg / "thing.go", pkg / "a.go", pkg / "b.go"], cache_root=tmp_path)
thing_nodes = [n for n in result["nodes"] if n["label"] == "Thing"]
assert len(thing_nodes) == 1, [n["id"] for n in thing_nodes]
# The phantom signature is the referencing file's path (with .go extension)
# baked into the id — must not appear.
assert "_go" not in thing_nodes[0]["id"], thing_nodes[0]["id"]
def test_extract_updates_raw_call_callers_after_duplicate_id_disambiguation(tmp_path):
first = tmp_path / "apps/api/Program.cs"
second = tmp_path / "tools/api/Program.cs"