Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(js): attribute module level anonymous closure calls to the file node (#3124) - #3533

Open
ayushcodes10 wants to merge 2 commits into
Graphify-Labs:v8from
ayushcodes10:fix-3124-anon-callback-module-level
Open

fix(js): attribute module level anonymous closure calls to the file node (#3124)#3533
ayushcodes10 wants to merge 2 commits into
Graphify-Labs:v8from
ayushcodes10:fix-3124-anon-callback-module-level

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

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_bodies entry keyed by the file node, mirroring the existing _js_scan_member_assignments fix (#3408) that already does this for this.X = fn assignments 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(...) containing register(...) 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.py dedupes strictly by (source, target) regardless of relation, so the file node's own pre-existing imports/imports_from edge to that target pre-empts the calls edge 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 valid calls source 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 (where target is 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

  • New tests/test_js_module_level_anonymous_calls.py with 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.
  • Verified the two positive tests fail without the fix and pass with it (reverted engine.py, confirmed AssertionError).
  • Ran the full existing JS/TS test suite (test_js_callback_calls.py and 15 other JS/TS test files, 186 tests) — all pass, no regressions.
  • Full suite: python3 -m pytest -q — 5490 passed, only the pre-existing unrelated failures (test_ollama_retry_cap.py missing openai in this env, one flaky timing assertion in test_ts_import_type_arguments.py).
  • python3 -m tools.skillgen --check — OK.

🤖 Generated with Claude Code

https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh

ayushcodes10 and others added 2 commits September 13, 2026 18:25
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

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_extract.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_js_module_level_anonymous_calls.py — impact, changed-test
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_trailing_newline_not_a_syntax_error.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant