fix(js): attribute module level anonymous closure calls to the file node (#3124) - #3533
fix(js): attribute module level anonymous closure calls to the file node (#3124)#3533ayushcodes10 wants to merge 2 commits into
Conversation
Number 1740 fixed calls inside an anonymous callback by attributing
them to the enclosing named function, but that only works when a
named function actually encloses it. it("...", async () => { ... })
has no such enclosing function anywhere, and that shape hits test
files hardest since it is the idiomatic way to write one. The
original report already named the intended fallback, attribute to
the module node when no named function encloses the call, but that
half was never built. A topmost closure argument to a module level
call statement now gets its own function_bodies entry keyed by the
file node, mirroring the existing fix for this.X assignments found
in the exact same statement shape, so the ordinary call walking
machinery, including its own descent into further nested closures,
picks it up from there. Toward Graphify-Labs#3124.
Known gap left open, described in the new test module's own
docstring: a callee that is both directly imported by the same file
and only called from a module level anonymous closure still does not
resolve, because the cross file resolution pass dedupes purely by
source and target regardless of relation, so the file node's own
import edge to that target pre-empts the calls edge. Fixing that
dedup properly reaches far beyond this one case and is left for a
separate change.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Covers the same file case, the doubly nested repro shape from the report itself, confirms the number 1740 named enclosing function case is unaffected, and documents the direct import collision gap called out in the fix commit as a pin rather than a silent assumption. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Captures JS/TS calls made directly inside a module-level anonymous closure with no named enclosing function anywhere (e.g. it("...", async () => { helper() })), attributing them to the file node in _js_extra_walk — completing the fallback #1740 named but never implemented, so calls in this idiomatic test shape are no longer dropped. Named-enclosing-function attribution is unchanged. One known gap remains and is documented by a test: a callee that's both directly imported by the same file and called only from such a closure still loses its calls edge, because the cross-file resolver dedupes by (source, target) regardless of relation and the file node's imports edge pre-empts it.
No blocking issues surfaced. 2 lower-confidence candidates did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 666 functions depend on the 222 functions this change touches.
Health — this change adds coupling hotspots:
- new:
_extract_generic()— 18 callers, 26 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
extract_objc()— 27 callers, 9 callees - new:
extract_julia()— 17 callers, 7 callees - new:
extract_cpp()— 29 callers, 3 callees - new:
extract_vue()— 10 callers, 7 callees - new:
walk()— 1 callers, 59 callees - …and 8 more — each is listed as a finding
Verification — 666 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 606 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
23 of 276 test file(s) selected (8%) via static blast radius.
tests/test_astro_extraction.py— impacttests/test_build.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_dotnet.py— impacttests/test_extract.py— impacttests/test_import_extension_resolution.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_js_exported_scalar_bindings.py— impacttests/test_js_module_level_anonymous_calls.py— impact, changed-testtests/test_languages.py— impacttests/test_multilang.py— impacttests/test_python_underscore_resolution.py— impacttests/test_rationale.py— impacttests/test_ruby_resolution.py— impacttests/test_scala_self_type.py— impacttests/test_swift_computed_properties.py— impacttests/test_trailing_newline_not_a_syntax_error.py— impacttests/test_ts_new_expression_calls.py— impacttests/test_typescript_module_extensions.py— impacttests/test_vue_extraction.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
Formal verification
Could not verify: Could not verify \_js\_extra\_walk.
The verifier did not have enough to check \_js\_extra\_walk, so it is saying so rather than guessing. No false assurance is the whole point.
Guarantee: No guarantee either way, this is an honest abstention, not a pass.
Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)
· 16 more finding(s) on lines outside this diff (see the check run).
Summary
Toward #3124. #1740 fixed calls inside an anonymous callback by attributing them to the enclosing NAMED function — but that only works when a named function actually encloses the call. When the nearest enclosing scope is itself anonymous, nothing is emitted at all.
it("...", async () => { ... })is exactly this shape, and it hits test files hardest, since that's the idiomatic way to write one.#1740's own issue text named the intended fallback ("not attributed to the enclosing function, not to the module node") but the module-node half was never implemented. This closes it: a topmost closure argument to a module-level call statement now gets its own
function_bodiesentry keyed by the file node, mirroring the existing_js_scan_member_assignmentsfix (#3408) that already does this forthis.X = fnassignments found in the exact same kind of statement. Once tracked, the ordinary call-walking machinery — including its own descent into further nested closures — picks it up from there, so the issue's own doubly-nested repro (it(...)containingregister(...)containing the real call) resolves correctly.Known gap, deliberately left open (documented in the new test file's own module docstring, and pinned by a dedicated test): a callee that is both directly imported by the same file and only called from a module-level anonymous closure still does not resolve. Tracing this down: the cross-file resolution pass in
extract.pydedupes strictly by(source, target)regardless of relation, so the file node's own pre-existingimports/imports_fromedge to that target pre-empts thecallsedge from ever being added. This collision is specific to a caller that is the file node — a NAMED function's calls never hit it, since imports are always attributed to the file node, never to a function node — so it only surfaces now that the file node is a validcallssource for the first time. Fixing that dedup to be relation-aware reaches across every language's resolution pass with a large, hard-to-bound blast radius, so I've scoped it out of this PR rather than guess at a narrower patch. This does mean the issue's own literal repro (wheretargetis both imported and called this way) still won't fully resolve — I want to be upfront about that rather than claim a complete fix.Test plan
tests/test_js_module_level_anonymous_calls.pywith 4 tests: same-file call inside a top-level anonymous closure; the exact doubly-nested repro from the issue; confirmation that JS/TS extractor silently drops calls inside anonymous arrow callbacks (addEventListener, forEach, etc.) #1740's named-enclosing-function case is unaffected; and a pinned test documenting the known direct-import-collision gap above.engine.py, confirmedAssertionError).test_js_callback_calls.pyand 15 other JS/TS test files, 186 tests) — all pass, no regressions.python3 -m pytest -q— 5490 passed, only the pre-existing unrelated failures (test_ollama_retry_cap.pymissingopenaiin this env, one flaky timing assertion intest_ts_import_type_arguments.py).python3 -m tools.skillgen --check— OK.🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh