feat(resolver): Phase 8.3f — object destructuring rest parameter resolution (WASM + native)#1355
Conversation
…arameters (JS)
When a function parameter uses object destructuring with a rest element
(`...rest`) and the rest object's property is then called, codegraph now
resolves the callee.
Resolution chain (Phase 8.3f):
1. JS extractor seeds typeMap['obj.X'] = { type: 'X' } for shorthand
properties in object literals (`var obj = { e4 }`).
2. New extractObjectRestParamBindingsWalk records objectRestParamBinding
{ callee, argIndex, restName } from `function f({ a, ...rest })`.
3. build-edges.ts cross-references with paramBindings from f(obj) calls
to seed typeMap[restName] = { type: argName, confidence: 0.65 }.
4. resolveByMethodOrGlobal: typeMap[restName] → obj; typeMap[obj.method]
→ method → resolved edge.
Adds:
- ObjectRestParamBinding type + ExtractorOutput.objectRestParamBindings field
- objectRestParamBindings serialised through WASM worker boundary
- extractObjectRestParamBindingsWalk in javascript.ts
- Object literal shorthand property seeding in handleVarDeclaratorTypeMap
- Phase 8.3f typeMap seeding in buildCallEdgesJS
- 6 unit tests in tests/parsers/javascript.test.ts
- Integration test (WASM engine) in issue-1336-object-rest-param-resolution.test.ts
- Jelly micro-test fixture tests/benchmarks/resolution/fixtures/jelly-micro/rest/
Files issue #1348 (missing WASM worker serialisation for paramBindings/
returnTypeMap/callAssignments) and #1349 (native engine parity) as follow-ups.
Closes #1336
…e 8.3f parity)
Add buildObjectRestParamPostPass to the native call-edge path, mirroring the
Phase 8.3f seeding already present in buildCallEdgesJS (WASM path).
The post-pass seeds typeMap[restName] = { type: argName } by cross-referencing
objectRestParamBindings (which formal param uses a rest binding) with
paramBindings (which call-site arg was passed). It then resolves calls whose
receiver is the rest-binding name via resolveCallTargets with the enriched
typeMap, adding any edges the native Rust engine missed.
For same-file and directly-imported callees, the native Rust engine already
resolves via its own same-file (step 2) and import-aware (step 1) lookups;
seenByPair prevents duplicate edges. The post-pass provides the typeMap-chain
fallback for the cross-file non-imported case where the chain
typeMap[restName] → argName → typeMap[argName.method] → target resolves
a callee the Rust engine cannot find.
Also updates the #1336 integration test to assert both WASM and native engines
produce the f3 → e4 edge.
Closes #1349
|
Claude finished @carlos-alm's task —— View job PR Review: Phase 8.3f — Object Destructuring Rest Parameter Resolution
Root Cause AnalysisProblem Being Solved: When a JS/TS function uses object-destructuring with a rest parameter ( Why This Approach: The fix chains four steps: (1) seed Risk Assessment: Medium. The WASM path has a serialization gap (see Critical Concerns below). Native path looks correct. Duplicate-prevention via Phase 2: Configuration ChangesNo linting rules relaxed, no TypeScript strict flags disabled, no coverage thresholds changed, no build configuration modified. Clean. Phase 3: Test IntegrityTests are additive (+435/-0). New assertions are appropriately strict ( One concern: The WASM integration test may be validating behavior through a path that doesn't exercise the serialization roundtrip — see Critical Concerns. Phase 4: Code Quality
The implementation correctly handles
The Minor: The Critical Concern:
|
Greptile SummaryImplements Phase 8.3f object-destructuring rest-parameter resolution for both the WASM and native engine paths, addressing
Confidence Score: 5/5Safe to merge — all known correctness issues from prior review rounds have been addressed in this PR's commits. The defensive new Map(...) copy in both the WASM and native paths prevents typeMap seeds from persisting into the cached ExtractorOutput across incremental runs. The native post-pass correctly initialises seenByPair from the full allEdgeRows snapshot before adding any new edges, consistent with all sibling post-passes. The dynamic: 0 assertion was added to the native integration test in this PR. The restName scoping gap (#1358) is explicitly deferred and tracked upstream. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Extractor as javascript.ts (Extractor)
participant TypeMap as typeMap
participant PostPass as buildObjectRestParamPostPass / buildCallEdgesJS
participant Resolver as resolveCallTargets
Extractor->>TypeMap: "seed typeMap['obj.e4'] = {type:'e4'} (object literal seeding)"
Extractor->>Extractor: "record objectRestParamBinding {callee:'f3', argIndex:0, restName:'eerest'}"
Extractor->>Extractor: "record paramBinding {callee:'f3', argIndex:0, argName:'obj'} from f3(obj)"
PostPass->>TypeMap: "seed typeMap['eerest'] = {type:'obj', confidence:0.65} (Phase 8.3f)"
PostPass->>Resolver: "resolveCallTargets(call{receiver:'eerest', method:'e4'}, typeMap)"
Resolver->>TypeMap: lookup typeMap['eerest'] → 'obj'
Resolver->>TypeMap: lookup typeMap['obj.e4'] → 'e4'
Resolver-->>PostPass: target node for e4
PostPass->>PostPass: check seenByPair — dedup native engine edges
PostPass->>PostPass: allEdgeRows.push([f3_id, e4_id, 'calls', conf, 0, 'points-to'])
Reviews (12): Last reviewed commit: "Merge branch 'main' into fix/native-rest..." | Re-trigger Greptile |
| const typeMap: Map<string, TypeMapEntry | string> = symbols.typeMap || new Map(); | ||
|
|
||
| // Phase 8.3f: seed typeMap[restName] = { type: argName } for each object-destructuring | ||
| // rest parameter binding cross-referenced with call-site argument bindings. | ||
| // e.g. function f({ a, ...rest }) called as f(obj) → typeMap['rest'] = { type: 'obj' } | ||
| // so that `rest.method()` resolves via typeMap['obj.method']. | ||
| if (symbols.objectRestParamBindings?.length && symbols.paramBindings?.length) { | ||
| for (const orpb of symbols.objectRestParamBindings) { | ||
| for (const pb of symbols.paramBindings) { | ||
| if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) { | ||
| if (!typeMap.has(orpb.restName)) { | ||
| typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 }); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Shared typeMap mutated in-place on WASM path
symbols.typeMap is assigned by reference (symbols.typeMap || new Map()), so the subsequent typeMap.set(orpb.restName, ...) call permanently writes into the stored ExtractorOutput object. The native post-pass (buildObjectRestParamPostPass) correctly copies with new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []). In incremental or watch-mode pipelines where fileSymbols for unchanged files is cached across buildEdges invocations, the !typeMap.has(orpb.restName) guard will block re-seeding on the second run — meaning a changed call-site argument (e.g., f3(obj1) → f3(obj2)) won't update the stale entry for the cached file.
There was a problem hiding this comment.
Fixed in commit f3a42fe ("fix(resolver): defensive-copy typeMap in buildCallEdgesJS Phase 8.3f block"). buildCallEdgesJS now opens Phase 8.3f with const typeMap = new Map(symbols.typeMap instanceof Map ? symbols.typeMap : []); — a fresh copy — so seeds never persist into the cached ExtractorOutput across incremental/watch runs. The !typeMap.has(orpb.restName) guard is now only preventing duplicate seeding within a single buildEdges invocation, not across invocations.
| const restNames = new Set<string>(); | ||
| for (const orpb of symbols.objectRestParamBindings!) { | ||
| for (const pb of symbols.paramBindings!) { | ||
| if (pb.callee === orpb.callee && pb.argIndex === orpb.argIndex) { | ||
| if (!typeMap.has(orpb.restName)) { | ||
| typeMap.set(orpb.restName, { type: pb.argName, confidence: 0.65 }); | ||
| restNames.add(orpb.restName); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Rest-param name collision when two functions share the same rest-binding identifier
The typeMap key is orpb.restName (e.g., "rest", "opts", "props") with no function-scope qualifier. If a file contains two functions that each destructure-rest with the same name — a common pattern — the !typeMap.has(orpb.restName) guard silently discards the second seeding. Any rest.method() call in the second function will then be resolved using the first function's argument type, producing incorrect call edges. The same issue is present in the WASM path's Phase 8.3f seeding block in buildCallEdgesJS.
There was a problem hiding this comment.
This scoping collision was filed as issue #1358 and deferred — fixing it requires keying typeMap entries by function scope rather than just restName, which coordinates changes in seeding + resolution lookup. The current PR addresses the primary pattern documented in the fixture test. The gap is tracked and not lost.
| it('Native: emits a calls edge from f3 to e4 via eerest.e4() rest-receiver resolution', () => { | ||
| const edges = readCallEdges(path.join(tmpNative, '.codegraph', 'graph.db')); | ||
| const edge = edges.find((e) => e.src === 'f3' && e.tgt === 'e4'); | ||
| expect(edge).toBeDefined(); | ||
| }); |
There was a problem hiding this comment.
The native engine test only asserts
edge is defined, omitting the dynamic: 0 check that the WASM test includes. This means a dynamic edge (which would indicate incorrect resolution) would silently pass on the native path.
| it('Native: emits a calls edge from f3 to e4 via eerest.e4() rest-receiver resolution', () => { | |
| const edges = readCallEdges(path.join(tmpNative, '.codegraph', 'graph.db')); | |
| const edge = edges.find((e) => e.src === 'f3' && e.tgt === 'e4'); | |
| expect(edge).toBeDefined(); | |
| }); | |
| it('Native: emits a calls edge from f3 to e4 via eerest.e4() rest-receiver resolution', () => { | |
| const edges = readCallEdges(path.join(tmpNative, '.codegraph', 'graph.db')); | |
| const edge = edges.find((e) => e.src === 'f3' && e.tgt === 'e4'); | |
| expect(edge).toBeDefined(); | |
| expect(edge!.dynamic).toBe(0); | |
| }); |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Fixed — added expect(edge!.dynamic).toBe(0) to the native engine test at line 93 (commit 054c953). The assertion now matches the WASM test, so a dynamic-dispatch fallback in the native engine will be caught as a regression.
Codegraph Impact Analysis5 functions changed → 20 callers affected across 5 files
|
…Map/callAssignments/paramBindings in wasm-worker boundary
…iteral methods (#1357) docs check acknowledged
…block Native buildObjectRestParamPostPass already clones symbols.typeMap before seeding (line 755); the WASM path buildCallEdgesJS was taking a direct reference, causing Phase 8.3f seeds to persist into symbols.typeMap across watch/incremental builds and blocking correct re-seeding when call-site arguments changed. Tracks rest-param scoping gap (two functions with same restName in one file) in issue #1358.
|
Addressed review feedback:
|
|
Thanks for the thorough follow-up. Confirming what I see in the diff: Addressed:
Still open: The native integration test is missing the Everything else looks good. LGTM once that assertion is added. |
Native test for the rest-receiver edge (f3 → e4) was only checking that the edge exists; it now also asserts dynamic===0, matching the WASM test. This ensures a dynamic-dispatch fallback in the native engine is caught as a regression.
|
Added the missing |
|
Addressing Claude's review feedback: Critical Concern: paramBindings serialization — Fixed in commit 6031440 ("fix(wasm-worker): wire paramBindings, returnTypeMap, callAssignments through worker boundary (#1352)"), which was merged into this branch via 3c164f2. The Class method / object-literal method coverage — Fixed in commit 31d7fe7. typeMap defensive copy — Fixed in commit f3a42fe. dynamic:0 assertion in native test — Added in commit 054c953. All four issues from the review are resolved. |
…d in prior merge (#1355) Re-add func-prop-this-dispatch.test.ts, prototype-method-resolution.test.ts, and jelly-micro benchmark fixtures (this/, spread/, more1/) that were accidentally excluded when resolving an earlier merge conflict with main. These files were introduced in main via PR #1331 and provide integration-level coverage for features #1334 and #1317. Their absence meant regressions in this-dispatch and prototype method resolution would not be caught by CI.
|
Addressed Greptile's two remaining concerns:
|
|
Fixed Greptile's remaining suggestion: added a |
Summary
Implements Phase 8.3f — resolving
rest.method()calls whererestis the rest binding from an object-destructuring function parameter — for both WASM and native engines.Supersedes and closes PR #1350 (WASM-only implementation).
Pattern resolved:
Resolution chain
handleVarDeclaratorTypeMapseedstypeMap['obj.e4'] = { type: 'e4' }for shorthand properties in object literals.extractObjectRestParamBindingsWalkrecords{ callee: 'f3', argIndex: 0, restName: 'eerest' }fromfunction f3({ a, ...eerest }).typeMap['eerest'] = { type: 'obj' }by cross-referencing objectRestParamBindings × paramBindings.resolveByMethodOrGlobal:typeMap['eerest'] → 'obj';typeMap['obj.e4'] → 'e4';lookup→ resolved edge.WASM path: seeding happens in
buildCallEdgesJSbefore call-edge resolution.Native path: new
buildObjectRestParamPostPassruns afterbuildFnRefBindingsPtsPostPass; for same-file/imported callees the native Rust engine already resolves via its own steps 1–2, soseenByPairprevents duplicates; the post-pass adds coverage for the typeMap-chain path.Changes
Commit 1 (cherry-picked from #1350): WASM/JS implementation
src/types.ts—ObjectRestParamBindingtype +ExtractorOutput.objectRestParamBindingssrc/extractors/javascript.ts—extractObjectRestParamBindingsWalk; object literal property seedingsrc/domain/graph/builder/stages/build-edges.ts— Phase 8.3f seeding inbuildCallEdgesJSwasm-worker-{protocol,entry,pool}.tsCommit 2 (this PR): Native engine parity
src/domain/graph/builder/stages/build-edges.ts—buildObjectRestParamPostPasswired into native call-edge path#1336integration test to assert both WASM and native engines producef3 → e4Commit 3: wasm-worker boundary fix (#1352)
paramBindings,returnTypeMap,callAssignmentsthrough WASM worker serialization boundaryCommit 4: defensive-copy typeMap in
buildCallEdgesJSbuildCallEdgesJSwas taking a direct reference tosymbols.typeMap; clones it before Phase 8.3f seeding to prevent seeds persisting across incremental rebuildsCommit 5: Extend binding walk to class and object-literal methods (#1357)
extractObjectRestParamBindingsWalknow also handlesmethod_definition(class methods and object-literal shorthand methods) andpairnodes with function valuescurrentClasscontext so class methods emitClassName.methodNameas calleeRelated
paramBindings/returnTypeMap/callAssignments)