fix(js): retain lazy CommonJS require dependencies (#2700)

A require(...) written inside a function body (the canonical lazy-require idiom
for breaking circular deps) produced no edge at all, while the identical require
at module scope resolved as EXTRACTED imports_from/imports edges. walk_calls now
feeds nested require declarations to the same _require_imports_js routine,
attributing the edge to the enclosing callable; dynamic require(var) is still
skipped and no bare local node is minted (#1077 scope guard).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
rajanpanth
2026-08-15 16:44:34 +01:00
committed by safishamsi
co-authored by Claude Opus 4.8
parent 94c2050f93
commit 259bb6acfb
2 changed files with 36 additions and 3 deletions
+11 -3
View File
@@ -1861,7 +1861,7 @@ def _find_require_call(value_node):
return _find_require_call(obj)
return None
def _require_imports_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> bool:
def _require_imports_js(node, source: bytes, importer_nid: str, stem: str, edges: list, str_path: str) -> bool:
"""Detect CommonJS require imports inside lexical_declaration / variable_declaration.
Handles three patterns:
@@ -1900,7 +1900,7 @@ def _require_imports_js(node, source: bytes, file_nid: str, stem: str, edges: li
tgt_nid, resolved_path = resolved
line = node.start_point[0] + 1
edge = {
"source": file_nid,
"source": importer_nid,
"target": tgt_nid,
"relation": "imports_from",
"context": "import",
@@ -1937,7 +1937,7 @@ def _require_imports_js(node, source: bytes, file_nid: str, stem: str, edges: li
if target_stem is not None:
for sym in sym_names:
edges.append({
"source": file_nid,
"source": importer_nid,
"target": _make_id(target_stem, sym),
"relation": "imports",
"context": "import",
@@ -4746,6 +4746,14 @@ def _extract_generic(
walk_calls(child, caller_nid, receiver_types, closure_locals)
return
# CommonJS imports are valid at any lexical depth. The module-level
# pass records top-level require() declarations; this pass owns function
# bodies, so detect lazy/cycle-breaking requires here and attribute the
# dependency to the enclosing callable rather than silently dropping it.
if (config.ts_module in ("tree_sitter_javascript", "tree_sitter_typescript")
and node.type in ("lexical_declaration", "variable_declaration")):
_require_imports_js(node, source, caller_nid, stem, edges, str_path)
if node.type in config.call_types:
# JS/TS dynamic imports: await import('./foo.js')
if config.ts_module in ("tree_sitter_javascript", "tree_sitter_typescript"):
+25
View File
@@ -776,6 +776,31 @@ def test_extract_js_member_require_emits_property_symbol():
assert _make_id(helpers_stem, "helperFn") in sym_targets
def test_extract_js_function_scoped_require_emits_import_edge(tmp_path):
"""Lazy CommonJS requires belong to their enclosing function, not nowhere."""
target = tmp_path / "target.js"
target.write_text("exports.helper = () => 42;\n", encoding="utf-8")
caller = tmp_path / "lazy.js"
caller.write_text(
"function useItLazily() {\n"
" const { helper } = require('./target');\n"
" return helper();\n"
"}\n",
encoding="utf-8",
)
result = extract([caller, target], cache_root=tmp_path, root=tmp_path, parallel=False)
labels = {node["id"]: node["label"] for node in result["nodes"]}
lazy_edges = [
edge for edge in result["edges"]
if edge["relation"] == "imports_from" and "target" in edge["target"]
]
assert len(lazy_edges) == 1
assert labels[lazy_edges[0]["source"]] == "useItLazily()"
assert lazy_edges[0]["confidence"] == "EXTRACTED"
def test_extract_js_arrow_function_still_extracted():
"""Regression: arrow functions in lexical_declaration must still produce nodes."""
from graphify.extract import extract_js