diff --git a/CHANGELOG.md b/CHANGELOG.md index 280e951f..2ffaf58a 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 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`. ## 0.9.43 (2026-08-14) diff --git a/tests/test_indirect_call_external_import_shadow.py b/tests/test_indirect_call_external_import_shadow.py index 11fdfd76..e519b1db 100644 --- a/tests/test_indirect_call_external_import_shadow.py +++ b/tests/test_indirect_call_external_import_shadow.py @@ -152,3 +152,24 @@ def test_unimported_same_file_callable_still_emits(tmp_path): )}) indirect = _rels(r, "indirect_call") assert (nid["run"], nid["handler"]) in indirect + + +def test_external_import_shadow_does_not_bind_to_a_same_named_local_callable(tmp_path): + """The precise collision the fix must survive: a name that is BOTH imported + externally AND defined as a callable in another corpus file. Using the + external `Filter` value must not fabricate an indirect_call onto the unrelated + corpus `Filter` — while a genuine by-name use of a local callable still binds.""" + r, nid = _extract_js_dir(tmp_path, { + "table.ts": "export function Filter() { return null; }\n", # unrelated corpus callable + "toolbar.tsx": ( + "import { Filter } from 'lucide-react';\n" # external, same name + "function onClick(x) { return x; }\n" + "export function build(sink, pool) {\n" + " sink.push(Filter);\n" # external -> must NOT bind to table.ts Filter + " pool.submit(onClick);\n" # local -> must still bind + "}\n" + ), + }) + indirect = _rels(r, "indirect_call") + assert (nid["build"], nid["Filter"]) not in indirect # no cross-file phantom + assert (nid["build"], nid["onClick"]) in indirect # real local reference preserved