Skip to content

fix(js): resolve calls inside exported functions, incl. aliased imports (#3346) - #3517

Closed
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/js-exported-fn-call-resolution
Closed

fix(js): resolve calls inside exported functions, incl. aliased imports (#3346)#3517
abhay-codes07 wants to merge 1 commit into
Graphify-Labs:v8from
abhay-codes07:fix/js-exported-fn-call-resolution

Conversation

@abhay-codes07

Copy link
Copy Markdown
Contributor

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_bodies scanned only the direct children of the program node. A top-level export function f(){} / export const g = () => {} is an export_statement wrapping the declaration, so every exported function was skipped — and the calls inside it never became uses facts.

That matters specifically for aliased imports. A call resolves to its target one of two ways:

  • the shared resolver matches the written callee name against the global symbol table (this is what makes plain import { foo }; foo() work), or
  • the symbol-resolution uses path 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 no baz, only bar. But because useAlias was an exported function, no uses fact was ever produced for baz(), 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_statement to its inner function_declaration / lexical_declaration in _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 phantom baz; 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 clean v8 on this Windows machine — pre-existing/flaky, unrelated).

🤖 Generated with Claude Code

https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q

…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
Copilot AI lite review requested due to automatic review settings September 12, 2026 18:41

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@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.

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 scannedgraphify/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 skippedgraphify/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 — 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_forwarding_review_findings.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_fn_call_resolution.py — impact, changed-test
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_package_json_subpath_imports.py — impact
  • tests/test_pascal.py — impact
  • tests/test_pascal_resolution.py — impact
  • tests/test_phantom_external_import.py — impact
  • tests/test_python_import_resolution.py — impact
  • tests/test_python_parse_memoization.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_resolve_memoization.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_source_key_memoization.py — impact
  • tests/test_src_layout_import_resolution.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_unmapped_at_alias_resolution.py — impact
  • tests/test_vue_extraction.py — impact
  • tests/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).

@abhay-codes07

Copy link
Copy Markdown
Contributor Author

Checked both against the actual behavior:

"export default arrow/function bodies no longer scanned" — false positive. A NAMED default function IS scanned and resolves: export default function run() { return baz(); } with import { bar as baz } produces run() -> bar() (verified). It's caught because the unwrap collects the function_declaration child of the export_statement regardless of the default keyword. (And nothing was "no longer" scanned — before this PR no export_statement-wrapped body was scanned at all; this PR strictly adds coverage.)

"Default-exported arrow functions are still skipped" — true, but a pre-existing anonymous-function trait, out of scope here. export default () => baz() has no name, so there is no caller node to attribute the call to — the same reason any anonymous arrow/function gets no node. My change neither introduced nor worsened this; resolving it would mean synthesizing a caller node for the anonymous default export, a separate enhancement. This PR stays focused on the aliased-named-import root cause (calls inside named exported functions), which it fixes completely.

No code change from these — the named cases work and the anonymous-default case is consistent with existing anonymous handling.

@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.60 — landed on v8 via cherry-pick with your authorship preserved. Thanks @abhay-codes07 — calls inside exported functions and to aliased imports now resolve. Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.60

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.

3 participants