fix(extractors): emit per-element definitions for array-pattern destructuring#2038
Conversation
…ucturing Both engines previously emitted a single "constant" Definition for a top-level const array pattern (const [a, b] = fn()) whose name was the raw pattern source text (e.g. "[a, b]") -- never a real identifier and never a valid call target. The native Rust extractor additionally emitted no Definition at all for array_pattern name nodes, a genuine engine-parity gap. Both engines now emit one "constant" Definition per bound identifier, mirroring how object-pattern destructuring already works per-property (#1773): extractArrayPatternBindings (WASM/TS) and extract_array_pattern_bindings (native), covering plain identifiers, default-value bindings, and rest bindings. Fixes #1901 Impact: 3 functions changed, 8 affected
Greptile SummaryThis PR fixes engine parity for top-level
Confidence Score: 5/5The change is tightly scoped to the array-pattern extraction path, gated by the same function-scope guard that protects object-pattern handling, and exercised end-to-end by both unit and integration tests across both engines. Both TS and Rust implementations mirror the already-working object-pattern counterparts. The only observation is a minor operator-ordering issue in the TS rest-binding lookup with no practical impact, as rest_pattern always places the identifier at child index 1. No files require special attention; all four changed files are straightforward and well-covered by the new tests. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["handle_var_decl / handleVariableDeclarator"] --> B{name node kind?}
B -- identifier --> C[push constant Definition]
B -- object_pattern --> F[extract_destructured_bindings]
B -- array_pattern --> G["extract_array_pattern_bindings (NEW)"]
G --> H{child kind?}
H -- identifier --> I[push constant Definition]
H -- assignment_pattern --> J[left identifier → push constant Definition]
H -- rest_pattern/rest_element --> K[scan for identifier → push constant Definition]
H -- other/hole --> L[skip]
%%{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"}}}%%
flowchart TD
A["handle_var_decl / handleVariableDeclarator"] --> B{name node kind?}
B -- identifier --> C[push constant Definition]
B -- object_pattern --> F[extract_destructured_bindings]
B -- array_pattern --> G["extract_array_pattern_bindings (NEW)"]
G --> H{child kind?}
H -- identifier --> I[push constant Definition]
H -- assignment_pattern --> J[left identifier → push constant Definition]
H -- rest_pattern/rest_element --> K[scan for identifier → push constant Definition]
H -- other/hole --> L[skip]
Reviews (1): Last reviewed commit: "fix(extractors): emit per-element defini..." | Re-trigger Greptile |
| // [...rest] — the identifier is at child index 1 (index 0 is the `...` | ||
| // token itself), matching extractParameters' own rest_pattern handling. | ||
| const inner = child.child(1) || child.childForFieldName('name'); | ||
| if (inner && inner.type === 'identifier') { |
There was a problem hiding this comment.
The
|| fallback to childForFieldName('name') only fires when child.child(1) returns a falsy value (null/undefined). If child(1) returns a truthy non-identifier node (e.g., array_pattern in a nested rest like ...[a, b]), the childForFieldName('name') branch is never reached and the definition is silently skipped. The Rust implementation avoids this by scanning all children for the first identifier. Flipping the order to prefer childForFieldName with a positional fallback would be more explicit about the intent.
| // [...rest] — the identifier is at child index 1 (index 0 is the `...` | |
| // token itself), matching extractParameters' own rest_pattern handling. | |
| const inner = child.child(1) || child.childForFieldName('name'); | |
| if (inner && inner.type === 'identifier') { | |
| // [...rest] — the identifier is the named child (field 'name'), with a | |
| // positional fallback for grammar versions that don't expose the field. | |
| const inner = child.childForFieldName('name') ?? child.child(1); | |
| if (inner && inner.type === 'identifier') { |
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!
Codegraph Impact Analysis3 functions changed → 8 callers affected across 1 files
|
Summary
For a top-level
const [a, b] = someCall();declaration, both engines previously emitted a singleconstantDefinitionwhosenamewas the raw pattern source text (e.g."[a, b]") — never a real identifier and never itself a valid call target:src/extractors/javascript.ts, both the walk pathhandleVariableDeclaratorand the query pathextractDestructuredDeclarators): emitted the single-node-named-by-raw-textDefinition.crates/codegraph-core/src/extractors/javascript.rs,handle_var_decl): as of Exported consts with non-recognized initializer shapes are not extracted as definitions at all #1819 also emitted the same single-node-named-by-raw-textDefinition— but per the issue's original report (filed before Exported consts with non-recognized initializer shapes are not extracted as definitions at all #1819 landed), native previously emitted no definition at all forarray_patternname nodes, a genuine engine-parity gap.Both engines now emit one
constantDefinitionper bound identifier in the array pattern — mirroring how object-pattern destructuring already works per-property (extractDestructuredBindings/extract_destructured_bindings, fixed in #1773/#1902) — via newextractArrayPatternBindings(WASM/TS) andextract_array_pattern_bindings(native) helpers, shared by both extraction paths on the WASM side. Covers plain identifiers ([a, b]), default-value bindings ([a = 1]), and rest bindings ([...rest]).Why this shape
Per the issue's own suggested fix: a single node named
[a, b]is not a real identifier and can never resolve as a call target even thougha/bthemselves might be. Per-element extraction makesa()/b()call sites resolvable, exactly like the already-fixed object-pattern case.Out-of-scope finding
While verifying the rest-binding (
...rest) child-node structure via direct AST inspection, found that the unrelated dynamic-import/CJS-require name-collection helpers (extract_rest_identifierin Rust, and thearray_patternbranch ofextractDynamicImportNamesin TS) read the wrong child index forrest_patternand never actually extract the rest-bound name. Filed as #2037; not fixed here (different code path, different concern).Test plan
npx vitest run tests/parsers/javascript.test.ts— updated the two existing array-pattern tests to the new per-element shape, added new default/rest-binding coveragetests/integration/issue-1901-array-pattern-definition.test.ts— builds a real fixture with bothengine: 'wasm'andengine: 'native', asserts identical per-elementconstantdefinitions in both, and that the old raw-pattern-text node is gonecargo test --package codegraph-core— 592 passed (added 2 new tests for the array-pattern helper: default/rest bindings, and updated the existing call-expression-initializer test)npm test— full suite green (249 files, 3982 passed)npx vitest run tests/benchmarks/resolution/resolution-benchmark.test.ts(javascript/typescript/dynamic-javascript/pts-javascript) — no precision/recall regressionnpx vitest run tests/benchmarks/resolution/jelly-micro.test.ts— all 64 fixtures pass, includingdestructuring(the fixture containingconst [x, y] = new Set([...]))npm run lint— clean (files touched);npm run build— clean; native addon rebuilt vianapi build --platform --release, codesigned, verified directly against both enginesStacked on #1897/#1900/#1902 (base branch
fix/issue-1897-engine-parity-wasm-doesnt-resolve-inline) — only the array-pattern extractor/test diff is this issue's change.