fix(extractors): resolve inline object-literal dispatch tables on WASM#2035
Conversation
WASM's extractSubscriptCallInfo had no equivalent of native's RES-2
extract_dispatch_table_call, so ({a:fnA,b:fnB})[key]() classified as a
generic flagged computed-key sink while native resolved it to fnA/fnB
via the points-to wildcard solver — a genuine engine-parity gap.
Add extractDispatchTableCall, mirroring the native extractor: when a
subscript call's object is an object literal (optionally unwrapped
from a parenthesized expression) and the index is a bare identifier,
seed arrayElemBindings under a synthetic <dt_line_col> name and emit a
<dt_line_col>[*] call with dynamicKind 'dispatch-table' so the existing
pts wildcard resolution path (already used for array-literal dispatch)
picks it up identically on both engines. Threaded through both
extraction paths (dispatchQueryMatch for the WASM worker, handleCallExpr
for direct/test callers) per the two-path extractor architecture.
Adds 'dispatch-table' to the DynamicKind union and restores the
pts-javascript dispatch-table fixture, points-to unit tests, and a
query-vs-walk + cross-engine parity case that were dropped from a prior
merge without being ported to WASM.
Impact: 9 functions changed, 20 affected
Greptile SummaryCloses a WASM/native engine-parity gap (#1897): WASM's subscript-call extractor previously had no equivalent of the native
Confidence Score: 4/5The core extraction logic and pts integration are correct; the only gap is the benchmark report under-counting points-to technique coverage for the new dispatch-table edges. The extractDispatchTableCall implementation is faithful to the Rust mirror: parenthesized-expression unwrap, identifier-value filtering, BUILTIN_GLOBALS guard, synthetic dt_line_col naming, and wildcard call emission all look correct. Both WASM extraction paths now consistently thread arrayElemBindings, and dispatch-table is correctly excluded from FLAG_ONLY_DYNAMIC_KINDS. The one omission is pts-dispatch-table missing from TECHNIQUE_MAP in the resolution benchmark — those two new edges will appear under other instead of points-to in the per-technique report, which may confuse future contributors reading benchmark output. tests/benchmarks/resolution/resolution-benchmark.test.ts — TECHNIQUE_MAP needs a pts-dispatch-table: points-to entry Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant AST as Tree-sitter AST
participant ESQ as extractSymbolsQuery / handleCallExpr
participant EDT as extractDispatchTableCall (new)
participant AEB as arrayElemBindings[]
participant PTS as buildPointsToMap
participant Graph as Edge Builder
AST->>ESQ: subscript_expression node
ESQ->>EDT: "obj=object_literal, index=key, callNode, arrayElemBindings"
EDT->>AEB: "push {arrayName:dt_5_2, index:0, elemName:fnA}"
EDT->>AEB: "push {arrayName:dt_5_2, index:1, elemName:fnB}"
EDT-->>ESQ: "Call { name:dt_5_2[*], dynamicKind:dispatch-table }"
ESQ->>PTS: calls + arrayElemBindings
PTS->>PTS: buildArrayElemConstraints
PTS-->>Graph: "resolve(dt_5_2[*]) = {fnA, fnB}"
Graph->>Graph: emit calls edges runDispatch to fnA and fnB
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant AST as Tree-sitter AST
participant ESQ as extractSymbolsQuery / handleCallExpr
participant EDT as extractDispatchTableCall (new)
participant AEB as arrayElemBindings[]
participant PTS as buildPointsToMap
participant Graph as Edge Builder
AST->>ESQ: subscript_expression node
ESQ->>EDT: "obj=object_literal, index=key, callNode, arrayElemBindings"
EDT->>AEB: "push {arrayName:dt_5_2, index:0, elemName:fnA}"
EDT->>AEB: "push {arrayName:dt_5_2, index:1, elemName:fnB}"
EDT-->>ESQ: "Call { name:dt_5_2[*], dynamicKind:dispatch-table }"
ESQ->>PTS: calls + arrayElemBindings
PTS->>PTS: buildArrayElemConstraints
PTS-->>Graph: "resolve(dt_5_2[*]) = {fnA, fnB}"
Graph->>Graph: emit calls edges runDispatch to fnA and fnB
Reviews (1): Last reviewed commit: "fix(extractors): resolve inline object-l..." | Re-trigger Greptile |
Codegraph Impact Analysis9 functions changed → 18 callers affected across 2 files
|
Summary
extractSubscriptCallInfohad no equivalent of native's RES-2extract_dispatch_table_call, so({a:fnA,b:fnB})[key]()classified as a generic flaggedcomputed-keysink on WASM while native resolved it tofnA/fnBvia the points-to wildcard solver — a genuine engine-parity gap.extractDispatchTableCallinsrc/extractors/javascript.ts, mirroringextract_dispatch_table_callincrates/codegraph-core/src/extractors/javascript.rs: when a subscript call's object is an object literal (optionally unwrapped from a parenthesized expression) and the index is a bare identifier, seedsarrayElemBindingsunder a synthetic<dt_line_col>name and emits a<dt_line_col>[*]call withdynamicKind: 'dispatch-table'— the existing points-to wildcard resolution path (already used for array-literal dispatch, e.g.const arr=[f1,f2]; arr[i]()) then resolves it to each concrete target identically on both engines.dispatchQueryMatch(used by the WASM worker) andhandleCallExpr(direct/test callers) both now passarrayElemBindingsintoextractCallInfo→extractSubscriptCallInfo.'dispatch-table'to theDynamicKindunion insrc/types.ts, intentionally excluded fromFLAG_ONLY_DYNAMIC_KINDS(matching native's sink-edge exclusion) so a resolved table never produces a spurious sink edge.Note on history
While investigating, I found this exact fix (types.ts union member,
extractSubscriptCallInfodispatch-table branch, thepts-javascript/dispatch-table.jsfixture, andpoints-to.test.tsunit tests) was already implemented and merged once before (commit9a6b882f, closing #1665) but silently disappeared from the tree afterward — presumably lost in a merge/rebase — while the native-side mirror (added later in PR #1690, closing #1684) survived. This PR re-implements the WASM side from scratch against the current architecture (the extraction pipeline has changed substantially since then — single-pass collector walk,runCollectorWalk, etc.) and restores the dropped fixture/tests, plus adds new query-vs-walk and cross-engine parity regression coverage that didn't exist before.Test plan
npx vitest run tests/parsers/javascript.test.ts tests/unit/points-to.test.ts tests/engines/query-walk-parity.test.ts— new unit tests pass (297/297)npx vitest run tests/engines/parity.test.ts— new cross-engine parity case passes against a locally rebuilt native binary (37 passed, 8 skipped)npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts -t "pts-javascript"— 5/5 pass (restoreddispatch-table.js+ expected-edges entries)npm test— 3978 passed, 30 skipped, 2 todo, 1 pre-existing known-flaky failure unrelated to this change (checkNoNewCyclesnode-ordering, tracked in Flaky cycle-node ordering in checkNoNewCycles full-suite run (tests/integration/check.test.ts) #1990)cargo test --release -p codegraph-core— 591/591 pass (native side untouched; rebuilt binary confirms parity)npx tsc --noEmit/npm run lint— clean (no new issues)Closes #1897