fix(extract): emit symbol-level edges for TS/JS named imports

_import_js only created a file→file imports_from edge, leaving named
imported symbols (classes, error types, constants) as degree-1 orphans
with no connection to their consumers.

Track resolved_path when the import target resolves to a local file
(relative path or tsconfig alias). After emitting the file-level edge,
walk import_clause → named_imports → import_specifier to emit a
file → symbol EXTRACTED edge for each named specifier, using the same
_make_id(target_stem, name) key that _extract_generic uses when defining
the symbol.

On a 1696-file TypeScript monorepo this raised edge count from 8530 to
11267 (+2737 edges) and reduced isolated communities from 878 to 604.
This commit is contained in:
RubenAAA
2026-05-02 15:45:02 +02:00
committed by Ruben Avanesov
parent d40e1c0cef
commit 5a342e385c
+29
View File
@@ -182,6 +182,7 @@ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, s
def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
resolved_path: "Path | None" = None
for child in node.children:
if child.type == "string":
raw = _read_text(child, source).strip("'\"` ")
@@ -197,6 +198,7 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p
elif resolved.suffix == ".jsx":
resolved = resolved.with_suffix(".tsx")
tgt_nid = _make_id(str(resolved))
resolved_path = resolved
else:
# Check tsconfig.json path aliases (e.g. "@/" → "src/") before treating as external (#575)
aliases = _load_tsconfig_aliases(Path(str_path).parent)
@@ -208,6 +210,7 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p
break
if resolved_alias is not None:
tgt_nid = _make_id(str(resolved_alias))
resolved_path = resolved_alias
else:
# Bare/scoped import (node_modules) - use last segment; dropped as external
module_name = raw.split("/")[-1]
@@ -225,6 +228,32 @@ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_p
})
break
# Emit symbol-level edges for named imports from local/aliased files.
# e.g. `import { Foo, type Bar } from './bar'` → file → Foo, file → Bar (EXTRACTED)
# Uses the same _make_id(target_stem, name) key that _extract_generic emits when
# defining the symbol, so these edges wire importers directly to existing symbol nodes.
if resolved_path is not None:
target_stem = _file_stem(resolved_path)
line = node.start_point[0] + 1
for child in node.children:
if child.type == "import_clause":
for sub in child.children:
if sub.type == "named_imports":
for spec in sub.children:
if spec.type == "import_specifier":
name_node = spec.child_by_field_name("name")
if name_node:
sym = _read_text(name_node, source)
edges.append({
"source": file_nid,
"target": _make_id(target_stem, sym),
"relation": "imports",
"confidence": "EXTRACTED",
"source_file": str_path,
"source_location": f"L{line}",
"weight": 1.0,
})
def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
def _walk_scoped(n) -> str: