diff --git a/CHANGELOG.md b/CHANGELOG.md index 99aa3f1..cf371d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/graphify/extract.py b/graphify/extract.py index 5575e70..fa103cb 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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) diff --git a/tests/test_extract.py b/tests/test_extract.py index 6f9a0ba..2ec8ab9 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -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"