From e5d662eb63c47f95e4aebb7a060b21ea8a0abf1b Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sat, 15 Aug 2026 16:46:04 +0100 Subject: [PATCH] test(js): cover dynamic-require skip and no-double-count for lazy requires (#2700) Adds the two invariants the deep-dive flagged: require(variable) must not fabricate an edge, and a module-scope require still yields exactly one imports_from edge (guards the module-vs-body pass partition against future double-counting). Also adds the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + tests/test_extract.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ffaf58a..c83540fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.44 (unreleased) +- Fix: a lazy CommonJS `require(...)` inside a function body (the idiom for breaking circular dependencies) now emits the same `imports_from`/`imports` dependency edges as a top-level require, attributed to the enclosing function, instead of being silently dropped; a dynamic `require(variable)` is still skipped (#2700, thanks @rajanpanth). - Fix: a JS/TS identifier bound by an import whose target resolves outside the scanned corpus (e.g. a `lucide-react` icon) is now shadowed, so using it as a value no longer fabricates an INFERRED `indirect_call` onto an unrelated same-named callable elsewhere in the corpus; a relative/in-corpus import still resolves to its real target (#2757, thanks @phudayyy). - Fix: an OCaml qualified call `M.f` to an external module (one not defined in the same file, e.g. Hardcaml's `Reg_spec.create`) no longer binds to a same-named local `let f` — which produced a false `calls` edge and, when the caller was that local `f`, a `f -> f` self-loop. External qualified calls are kept as a distinct target labelled by the full path; unqualified calls and calls into a locally-defined module still resolve locally, and cross-file `Geo.area` still collapses onto another file's `area`. diff --git a/tests/test_extract.py b/tests/test_extract.py index 84a17a5b..416d3582 100644 --- a/tests/test_extract.py +++ b/tests/test_extract.py @@ -801,6 +801,38 @@ def test_extract_js_function_scoped_require_emits_import_edge(tmp_path): assert lazy_edges[0]["confidence"] == "EXTRACTED" +def test_extract_js_dynamic_require_variable_is_not_fabricated(tmp_path): + """A lazy `require(someVar)` has no static string target, so the body pass + must skip it rather than fabricate an edge to a guessed path (#2700).""" + caller = tmp_path / "dyn.js" + caller.write_text( + "function load(name) {\n" + " const mod = require(name);\n" + " return mod;\n" + "}\n", + encoding="utf-8", + ) + result = extract([caller], cache_root=tmp_path, root=tmp_path, parallel=False) + assert not [e for e in result["edges"] if e["relation"] in ("imports_from", "imports")] + + +def test_extract_js_module_scope_require_still_single_edge(tmp_path): + """No-double-count regression: the module-level and body require passes must + never both emit for the same require — a top-level require stays exactly one + imports_from edge (#2700).""" + target = tmp_path / "target.js" + target.write_text("exports.helper = () => 42;\n", encoding="utf-8") + caller = tmp_path / "top.js" + caller.write_text("const { helper } = require('./target');\n", encoding="utf-8") + + result = extract([caller, target], cache_root=tmp_path, root=tmp_path, parallel=False) + lazy_edges = [ + e for e in result["edges"] + if e["relation"] == "imports_from" and "target" in e["target"] + ] + assert len(lazy_edges) == 1 + + def test_extract_js_arrow_function_still_extracted(): """Regression: arrow functions in lexical_declaration must still produce nodes.""" from graphify.extract import extract_js