fix(js): resolve calls inside exported functions, incl. aliased imports (#3346) - #3517
fix(js): resolve calls inside exported functions, incl. aliased imports (#3346)#3517abhay-codes07 wants to merge 1 commit into
Conversation
…ts (Graphify-Labs#3346) _js_top_level_function_bodies scanned only direct program children, so a top-level `export function f(){}` / `export const g = () => {}` — wrapped in an export_statement — was skipped and the calls inside it never became `uses` facts. Only the use-fact path consults the per-file import-alias table, so an aliased-import call (`import { bar as baz }; baz()`) produced no calls edge: the plain-name global resolver has no `baz` to match, and the alias mapping that would have pointed it at `bar` was never exercised. Unwrapping a non-re-export export_statement to its inner function/arrow declaration treats exported and non-exported functions identically, so the alias table resolves the call to the real definition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Fixes JS/TS call resolution missing calls inside exported top-level functions: _js_top_level_function_bodies now unwraps an export_statement to its inner function_declaration/lexical_declaration (skipping export ... from re-exports) so exported functions and arrows are scanned identically to bare ones. The practical effect is that a call using an aliased import (import { bar as baz }; baz()) inside an exported function emits a uses fact and resolves to the real definition (bar) via the import table, rather than producing no calls edge. Adds tests covering exported functions, exported arrows, the plain-name and non-exported paths as controls, and asserting the edge never binds to a phantom alias node.
Worth a look
- export default arrow/function bodies no longer scanned —
graphify/extractors/resolution.py:1580· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Default-exported arrow functions are still skipped —
graphify/extractors/resolution.py:1581· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 1878 functions depend on the 156 functions this change touches.
Health — this change adds coupling hotspots:
- new:
extract()— 587 callers, 44 callees - new:
_rebuild_code()— 115 callers, 51 callees - new:
_extract_generic()— 18 callers, 26 callees - new:
extract_js()— 85 callers, 4 callees - new:
extract_xaml()— 19 callers, 17 callees - new:
_resolve_js_module_path()— 34 callers, 9 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
extract_objc()— 27 callers, 9 callees - …and 35 more — each is listed as a finding
Verification — 1878 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: 826 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
34 of 274 test file(s) selected (12%) 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_forwarding_review_findings.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_fn_call_resolution.py— impact, changed-testtests/test_js_exported_scalar_bindings.py— impacttests/test_languages.py— impacttests/test_multilang.py— impacttests/test_package_json_subpath_imports.py— impacttests/test_pascal.py— impacttests/test_pascal_resolution.py— impacttests/test_phantom_external_import.py— impacttests/test_python_import_resolution.py— impacttests/test_python_parse_memoization.py— impacttests/test_python_underscore_resolution.py— impacttests/test_rationale.py— impacttests/test_resolve_memoization.py— impacttests/test_ruby_resolution.py— impacttests/test_scala_self_type.py— impacttests/test_source_key_memoization.py— impacttests/test_src_layout_import_resolution.py— impacttests/test_swift_computed_properties.py— impacttests/test_ts_new_expression_calls.py— impacttests/test_typescript_module_extensions.py— impacttests/test_unmapped_at_alias_resolution.py— impacttests/test_vue_extraction.py— impacttests/test_walk_python_tree_iterative.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\_top\_level\_function\_bodies.
The verifier did not have enough to check \_js\_top\_level\_function\_bodies, 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: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set
· 43 more finding(s) on lines outside this diff (see the check run).
|
Checked both against the actual behavior: "export default arrow/function bodies no longer scanned" — false positive. A NAMED default function IS scanned and resolves: "Default-exported arrow functions are still skipped" — true, but a pre-existing anonymous-function trait, out of scope here. No code change from these — the named cases work and the anonymous-default case is consistent with existing anonymous handling. |
|
Shipped in v0.9.60 — landed on |
Fixes the aliased-named-import half of #3346, by fixing the root cause it shares with a broader gap: calls inside a top-level exported function were never resolved through the import table.
The bug
_js_top_level_function_bodiesscanned only the direct children of the program node. A top-levelexport function f(){}/export const g = () => {}is anexport_statementwrapping the declaration, so every exported function was skipped — and the calls inside it never becameusesfacts.That matters specifically for aliased imports. A call resolves to its target one of two ways:
import { foo }; foo()work), orusespath maps the call's local name through the per-file import-alias table (local_aliases_by_file).For
import { bar as baz }; baz()only the second path can work — the global table has nobaz, onlybar. But becauseuseAliaswas an exported function, nousesfact was ever produced forbaz(), so the alias mapping never ran and no edge was emitted. It fails silently: caller and callee nodes both exist, only the edge is missing.The fix
Unwrap a non-re-export
export_statementto its innerfunction_declaration/lexical_declarationin_js_top_level_function_bodies, so exported and non-exported functions are collected identically. The aliased call then resolves through the existing alias table to the real definition (bar), function and arrow forms alike. Re-exports (export … from '…') are left alone — they declare no local body.Scope
This fixes the aliased named import case (issue's second repro) and, more generally, call-through-import resolution inside any exported function. The namespace-import case (
import * as api; api.foo()) is a distinct mechanism — it's a member call whose receiver is a module alias, which needs namespace-binding capture and member-call resolution rather than the local-name alias table — and is left for a follow-up so this change stays focused and low-risk.Tests
tests/test_js_exported_fn_call_resolution.py— the aliased import resolves from an exported function and an exported arrow; the edge lands on the imported definition (bar), never a phantombaz; and the plain-named-import and non-exported-function paths are unchanged. With the fix reverted, the two exported-function aliased cases fail. The JS/TS + resolution suites are otherwise unchanged (1806 passed; the four failing names fail identically on cleanv8on this Windows machine — pre-existing/flaky, unrelated).🤖 Generated with Claude Code
https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q