From 5a342e385cbf3e0f6ef7083a493e2abbcdb171d7 Mon Sep 17 00:00:00 2001 From: RubenAAA Date: Sat, 2 May 2026 15:42:54 +0200 Subject: [PATCH] fix(extract): emit symbol-level edges for TS/JS named imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _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. --- graphify/extract.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/graphify/extract.py b/graphify/extract.py index d45a97a..509853b 100644 --- a/graphify/extract.py +++ b/graphify/extract.py @@ -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: