fix(extract): disambiguate imported type stubs across files without blocking rewire (#1462)

Two files that both import and use the same type as a bare name (e.g.
`from pathlib import Path` used as a type hint in a.py and b.py) collapsed into one
node, even with no project definition to anchor them. The referencing file is now
recorded in an internal `origin_file` field and used as the disambiguation key when
_disambiguate_colliding_node_ids splits same-id nodes — while source_file stays
empty, so the corpus-level rewire still collapses these stubs onto a real project
definition when one exists (the #1402 path is untouched). origin_file is read only
inside disambiguation; the rewire and the Java/C# type resolvers key off
source_file as before.

Ported from PR #1479 by @jiangyq9 (squash-merged onto current v8; the branch's
stale base made the raw diff appear to revert later features — the 3-way merge
applies only the surgical origin_file change). Resolved a test-adjacency conflict
with #1500 by keeping both cross-file tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jiangyq9
2026-06-27 23:38:26 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 76b6eabdb0
commit 6509d0ca6f
3 changed files with 30 additions and 2 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- 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).
+10 -2
View File
@@ -2468,6 +2468,7 @@ def _extract_generic(
"file_type": "code",
"source_file": "",
"source_location": "",
"origin_file": str_path,
})
return nid
@@ -7560,6 +7561,13 @@ def _source_key(source_file: str, root: Path) -> str:
return str(source_path)
def _node_disambiguation_source_key(node: dict, root: Path) -> str:
source_file = str(node.get("source_file", ""))
if source_file:
return _source_key(source_file, root)
return _source_key(str(node.get("origin_file", "")), root)
def _disambiguate_colliding_node_ids(
nodes: list[dict],
edges: list[dict],
@@ -7585,12 +7593,12 @@ def _disambiguate_colliding_node_ids(
remap: dict[tuple[str, str], str] = {}
ambiguous_ids: set[str] = set()
for old_id, group in by_id.items():
source_keys = {_source_key(str(node.get("source_file", "")), root) for node in group}
source_keys = {_node_disambiguation_source_key(node, root) for node in group}
if len(group) < 2 or len(source_keys) < 2:
continue
ambiguous_ids.add(old_id)
for node in group:
source_key = _source_key(str(node.get("source_file", "")), root)
source_key = _node_disambiguation_source_key(node, root)
if not source_key:
continue
new_id = _make_id(source_key, old_id)
+19
View File
@@ -152,6 +152,25 @@ def test_go_cross_file_type_refs_resolve_to_single_node(tmp_path):
assert "_go" not in thing_nodes[0]["id"], thing_nodes[0]["id"]
def test_imported_type_stubs_do_not_collide_across_source_files(tmp_path):
"""#1462: imported stdlib/type stubs with the same label are distinct uses
when there is no single project definition to rewire onto. They need the
referencing file as a disambiguator while still keeping ``source_file`` empty
so real project definitions can be rewired by #1402."""
first = tmp_path / "pkg/a.py"
second = tmp_path / "pkg/b.py"
first.parent.mkdir(parents=True)
first.write_text("from pathlib import Path\ndef use_a(p: Path):\n return p\n", encoding="utf-8")
second.write_text("from pathlib import Path\ndef use_b(p: Path):\n return p\n", encoding="utf-8")
result = extract([first, second], cache_root=tmp_path)
path_nodes = [node for node in result["nodes"] if node["label"] == "Path"]
assert len(path_nodes) == 2
assert len({node["id"] for node in path_nodes}) == 2
assert all(not node.get("source_file") for node in path_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"