From d177f04270df1625cdd4c46db1334ccefcc1951b Mon Sep 17 00:00:00 2001 From: Mohammed Ateeq <88542309+TPAteeq@users.noreply.github.com> Date: Sun, 28 Jun 2026 19:01:55 +0100 Subject: [PATCH] fix(extract): add origin_file to cross-file stubs in the six dedicated extractors (#1515) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #1462 same-label cross-file stub disambiguation (the origin_file key) only existed in the generic extractor, so the six dedicated extractors — Julia, Fortran, Go, Rust, PowerShell, ObjC — still collapsed same-named imported-type stubs from different files into one conflated bare-id node (a false cross-package link). Each now sets origin_file on its sourceless stub, identical to the generic extractor; the generic _node_disambiguation_source_key consumes it, so two files importing the same type stay distinct while source_file stays empty (the #1402 rewire onto a real definition is unchanged). Ported from PR #1515 by @TPAteeq. Must ship with #1516: this widens origin_file to six more languages, and #1516 is what strips it from graph.json. Verified: a 2-file Go corpus now yields 2 distinct Widget stubs AND graph.json carries no origin_file. Resolved a test-insertion conflict with #1516 by keeping both tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + graphify/extract.py | 6 ++++++ tests/test_extract.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 925c4c5..cf6d521 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu - Fix: imported type stubs with the same label no longer falsely merge across files when there is no project definition to rewire onto (#1462, thanks @jiangyq9). Two files that both `from pathlib import Path` and use `Path` as a type previously collapsed into one node; the referencing file is now kept as an internal disambiguator (`origin_file`) used only when splitting colliding ids, while `source_file` stays empty so a real project definition can still be rewired onto (the #1402 path is unaffected). - Feat: resolve C# cross-file type references and extract `enum`/`struct`/`record` declarations (#1466, thanks @TheFedaikin). A new `_resolve_csharp_type_references` (the C# counterpart to the Java resolver) re-points dangling `inherits`/`implements`/`references` edges from no-source "shadow" stubs to their real definitions, disambiguating same-named types in different namespaces via the referencing file's `using` directives and enclosing namespace; ambiguous matches are refused rather than guessed. `enum`/`struct`/`record` types are now extracted as definitions so those references resolve too. Advances #1318 for C#. - 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, thanks @TPAteeq). +- Fix: cross-file references to a same-named type now stay distinct across the six dedicated AST extractors (Go, Rust, Julia, Fortran, PowerShell, ObjC) instead of conflating into one shared node — #1462's `origin_file` stub-disambiguation had only been applied to the generic extractor; it now covers all seven. ## 0.8.50 (2026-06-27) diff --git a/graphify/extract.py b/graphify/extract.py index 39c4bfa..e2ebafc 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -5967,6 +5967,7 @@ def extract_julia(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid @@ -6263,6 +6264,7 @@ def extract_fortran(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid @@ -6512,6 +6514,7 @@ def extract_go(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid @@ -6866,6 +6869,7 @@ def extract_rust(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid @@ -7169,6 +7173,7 @@ def extract_powershell(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid @@ -9481,6 +9486,7 @@ def extract_objc(path: Path) -> dict: "file_type": "code", "source_file": "", "source_location": "", + "origin_file": str_path, }) return nid diff --git a/tests/test_extract.py b/tests/test_extract.py index 54a186b..eac8970 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -201,6 +201,28 @@ def test_origin_file_is_not_serialized_into_extract_output(tmp_path): assert len({node["id"] for node in path_nodes}) == 2 +def test_go_imported_type_stubs_do_not_collide_across_source_files(tmp_path): + """#1462 (dedicated extractors): the imported-type-stub disambiguation (the + ``origin_file`` key) landed only in the generic extractor, so the six dedicated + extractors (Go, Rust, Julia, Fortran, PowerShell, ObjC) still collapsed same-label + cross-file stubs into one conflated bare-id node — a false cross-package link. + They must stay distinct per file while keeping ``source_file`` empty so the #1402 + rewire still collapses them onto a real definition when one exists.""" + first = tmp_path / "a/use_a.go" + second = tmp_path / "b/use_b.go" + first.parent.mkdir(parents=True) + second.parent.mkdir(parents=True) + first.write_text('package a\n\nimport "ext"\n\nfunc UseA(w ext.Widget) {}\n', encoding="utf-8") + second.write_text('package b\n\nimport "ext"\n\nfunc UseB(w ext.Widget) {}\n', encoding="utf-8") + + result = extract([first, second], cache_root=tmp_path) + widget_nodes = [node for node in result["nodes"] if node["label"] == "Widget"] + + assert len(widget_nodes) == 2 + assert len({node["id"] for node in widget_nodes}) == 2 + assert all(not node.get("source_file") for node in widget_nodes) + + 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"