fix(extract): remove dangling alias import edges

This commit is contained in:
oleksii-tumanov
2026-07-17 10:54:07 +01:00
committed by safishamsi
parent b193607967
commit dea6ec0c24
2 changed files with 186 additions and 6 deletions
+49 -3
View File
@@ -7,6 +7,7 @@ import json
import os
import re
import sys
from collections import Counter
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable
@@ -4536,6 +4537,7 @@ def extract(
e["target"] = id_remap[e["target"]]
if prefix_remap:
sym_remap: dict[str, str] = {}
edge_alias_candidates: dict[str, set[str]] = {}
for n in all_nodes:
sf = n.get("source_file")
if not sf:
@@ -4556,12 +4558,28 @@ def extract(
# Try both the input-form and absolute-form prefixes for this file
# (#1529). source_file gating above already prevents cross-file
# contamination, so the first matching prefix wins.
canonical_nid: str | None = None
for old_pref, new_pref in entry:
if nid.startswith(old_pref + "_"):
new_nid = new_pref + nid[len(old_pref):]
if new_nid != nid:
sym_remap[nid] = new_nid
canonical_nid = new_pref + nid[len(old_pref):]
if canonical_nid != nid:
sym_remap[nid] = canonical_nid
break
if nid.startswith(new_pref + "_"):
canonical_nid = nid
break
if canonical_nid is None:
continue
# Named alias imports can retain an absolute-prefixed target even
# when the symbol node is already canonical. Record every old form
# so a redundant edge can be recognized without globally
# reinterpreting an id that another real node may own.
for old_pref, new_pref in entry:
if not canonical_nid.startswith(new_pref + "_"):
continue
old_nid = old_pref + canonical_nid[len(new_pref):]
if old_nid != canonical_nid:
edge_alias_candidates.setdefault(old_nid, set()).add(canonical_nid)
if sym_remap:
for n in all_nodes:
if n.get("id") in sym_remap:
@@ -4578,6 +4596,34 @@ def extract(
cn = rc.get("caller_nid")
if cn in sym_remap:
rc["caller_nid"] = sym_remap[cn]
if edge_alias_candidates:
edge_key_counts = Counter(
json.dumps(edge, sort_keys=True, separators=(",", ":"), default=str)
for edge in all_edges
)
owned_node_ids = {node.get("id") for node in all_nodes}
deduped_edges: list[dict] = []
for edge in all_edges:
candidates = (
edge_alias_candidates.get(edge.get("target", ""), set())
if edge.get("relation") == "imports"
else set()
)
if len(candidates) == 1:
candidate = next(iter(candidates))
twin = {**edge, "target": candidate}
twin_key = json.dumps(
twin, sort_keys=True, separators=(",", ":"), default=str
)
# Drop only when the shared resolver emitted the exact
# canonical twin. Otherwise the target may be a legitimate
# owned node id.
if edge_key_counts[twin_key]:
if edge.get("target") in owned_node_ids:
edge_key_counts[twin_key] -= 1
continue
deduped_edges.append(edge)
all_edges[:] = deduped_edges
_merge_swift_extensions(per_file, all_nodes, all_edges)
_disambiguate_colliding_node_ids(all_nodes, all_edges, all_raw_calls, root)
+137 -3
View File
@@ -1212,15 +1212,149 @@ def test_alias_import_edge_resolves_with_relative_input_paths(tmp_path, monkeypa
# that exists in the graph), not an orphan keyed by an absolute prefix.
assert _has_edge(result, "src/components/Button.tsx", "src/lib/utils.ts")
assert target_id in node_ids
import_targets = {
import_targets = [
e["target"]
for e in result["edges"]
if e["relation"] == "imports_from" and e["source"] == _file_node_id(Path("src/components/Button.tsx"))
}
assert target_id in import_targets
]
assert import_targets == [target_id]
# No surviving edge target may carry an absolute-path prefix from tmp_path.
abs_prefix = _file_node_id(Path("src/lib/utils.ts").resolve())
assert all(not t.startswith(abs_prefix + "_") and t != abs_prefix for t in import_targets)
# The named-symbol edge to formatDate must resolve to the real symbol node too.
assert _has_symbol_edge(result, "src/components/Button.tsx", "src/lib/utils.ts", "formatDate")
symbol_target = _make_id(_file_stem(Path("src/lib/utils.ts")), "formatDate")
named_imports = [
edge
for edge in result["edges"]
if edge["source"] == _file_node_id(Path("src/components/Button.tsx"))
and edge["relation"] == "imports"
and edge["source_location"] == "L1"
]
assert [edge["target"] for edge in named_imports] == [symbol_target]
assert all(
edge["source"] in node_ids and edge["target"] in node_ids
for edge in result["edges"]
if edge["relation"] in ("imports", "imports_from")
)
def test_alias_import_symbol_resolves_from_parent_working_directory(tmp_path, monkeypatch):
project = tmp_path / "project"
_write(
project / "tsconfig.json",
json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"@/*": ["src/*"]}}}),
)
_write(project / "src/lib/utils.ts", "export function formatDate(d) { return d }\n")
_write(
project / "src/components/Button.tsx",
"import { formatDate } from '@/lib/utils'\n",
)
monkeypatch.chdir(tmp_path)
result = extract(
[Path("project/src/lib/utils.ts"), Path("project/src/components/Button.tsx")],
cache_root=Path("project"),
)
node_ids = {node["id"] for node in result["nodes"]}
source_id = _file_node_id(Path("src/components/Button.tsx"))
symbol_target = _make_id(_file_stem(Path("src/lib/utils.ts")), "formatDate")
named_imports = [
edge
for edge in result["edges"]
if edge["source"] == source_id and edge["relation"] == "imports"
]
assert [edge["target"] for edge in named_imports] == [symbol_target]
assert all(edge["source"] in node_ids and edge["target"] in node_ids for edge in named_imports)
def test_alias_import_does_not_remap_an_owned_symbol_id(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
target = Path("src/lib/utils.ts")
absolute_prefix = _file_node_id(target.resolve())
mirror = Path(f"{absolute_prefix}.ts")
button = Path("src/components/Button.tsx")
mirror_user = Path("src/components/Mirror.tsx")
_write(
Path("tsconfig.json"),
json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"@/*": ["src/*"]}}}),
)
_write(target, "export function formatDate(d) { return d }\n")
_write(mirror, "export function formatDate(d) { return 999 }\n")
_write(
button,
"import { formatDate } from '@/lib/utils'\nexport const a = formatDate(1)\n",
)
_write(
mirror_user,
f"import {{ formatDate }} from '../../{mirror.stem}'\nexport const b = formatDate(2)\n",
)
result = extract(
[target, mirror, button, mirror_user],
cache_root=Path("."),
)
node_ids = {node["id"] for node in result["nodes"]}
symbols = {
node["source_file"]: node["id"]
for node in result["nodes"]
if node.get("label") == "formatDate()"
}
target_symbol = _make_id(_file_stem(target), "formatDate")
mirror_symbol = _make_id(_file_stem(mirror), "formatDate")
assert symbols[str(target)] == target_symbol
assert symbols[str(mirror)] == mirror_symbol
imports = [
edge
for edge in result["edges"]
if edge["relation"] == "imports" and edge["source_location"] == "L1"
]
by_source: dict[str, list[str]] = {}
for edge in imports:
by_source.setdefault(edge["source_file"], []).append(edge["target"])
assert by_source[str(button)] == [target_symbol]
assert by_source[str(mirror_user)] == [mirror_symbol]
assert all(edge["source"] in node_ids and edge["target"] in node_ids for edge in imports)
def test_alias_import_preserves_owned_same_line_symbol_edge(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
target = Path("src/lib/utils.ts")
absolute_prefix = _file_node_id(target.resolve())
mirror = Path(f"{absolute_prefix}.ts")
importer = Path("src/components/Both.tsx")
_write(
Path("tsconfig.json"),
json.dumps({"compilerOptions": {"baseUrl": ".", "paths": {"@/*": ["src/*"]}}}),
)
_write(target, "export function formatDate(d) { return d }\n")
_write(mirror, "export function formatDate(d) { return 999 }\n")
_write(
importer,
f"import {{ formatDate as a }} from '@/lib/utils'; "
f"import {{ formatDate as b }} from '../../{mirror.stem}';\n"
"export const value = a(1) + b(2)\n",
)
result = extract([target, mirror, importer], cache_root=Path("."))
node_ids = {node["id"] for node in result["nodes"]}
target_symbol = _make_id(_file_stem(target), "formatDate")
mirror_symbol = _make_id(_file_stem(mirror), "formatDate")
imports = [
edge
for edge in result["edges"]
if edge["source"] == _file_node_id(importer)
and edge["relation"] == "imports"
and edge["source_location"] == "L1"
]
assert sorted(edge["target"] for edge in imports) == sorted([target_symbol, mirror_symbol])
assert all(edge["source"] in node_ids and edge["target"] in node_ids for edge in imports)