Skip to content

Commit 18d80c9

Browse files
authored
fix(wasm): port missing node extractions to JS extractor (jelly-micro #1471) (#1509)
* chore: gitignore napi-generated artifacts in crates/codegraph-core * chore(tests): remove unused biome suppression in visitor.test.ts * fix(titan-run): sync --start-from enum and phase-timestamp list with actual phases * fix(hooks): track Bash file modifications via before/after git status diff Adds snapshot-pre-bash.sh (PreToolUse Bash) + track-bash-writes.sh (PostToolUse Bash): the pre-hook captures git status --porcelain to a per-worktree temp file before each Bash call; the post-hook diffs the before/after state and appends newly modified or created files to .claude/session-edits.log. This closes the gap where files written by sed -i, printf redirects, tee, heredocs, or build tools (Cargo.lock, lockfiles) were never recorded, causing guard-git.sh to emit false-positive BLOCKED errors. Closes #1457 * chore(native): remove dead code (unused var, method, variant, fields) - clojure.rs: annotate lifetime-anchor assignment to silence false-positive - cfg.rs: remove never-called start_line_of method - complexity.rs: remove never-constructed NotHandled variant; convert irrefutable if-let patterns to plain let destructures - dataflow.rs: remove never-read callee fields from CallReturn/Destructured - incremental.rs: remove never-read lang field from CacheEntry cargo check and cargo clippy both clean after these changes. * refactor(native): extract emit_pts_alias_edges params into PtsAliasCtx struct * fix(wasm): sort call targets by confidence before emit to match native engine * fix(bench): add 2 warmup runs and raise INCREMENTAL_RUNS to 5 for incremental tiers * ci(bench): add per-PR perf canary for extractor/graph/native changes Adds .github/workflows/perf-canary.yml — a path-filtered workflow that fires on PRs touching src/extractors/, src/domain/graph/, or crates/** and runs only the incremental-benchmark suite (full build + no-op + 1-file rebuild, both engines). Catches the class of regressions that accumulated invisibly across the Phase 8.x PRs and were only detected at v3.12.0 publish time. The regression guard gains BENCH_CANARY=1 mode: raises thresholds to 50%/100%/150% (standard/noisy/WASM) and skips the build, query, and resolution suites — only incremental checks run. This absorbs shared- runner timing variance while still blocking catastrophic regressions (+98% full build, +1827% 1-file rebuild from v3.12.0). Closes #1433 * fix(perf): plumb symbolsOnly through parseFilesWasmInline to skip analysis visitors * fix(perf): scope runPostNativeCha to changed files on incremental builds On incremental builds, runPostNativeCha previously scanned all call→qualified-method edges in the DB (~12ms flat, O(graph size)), even for 1-file changes where no hierarchy or RTA evidence changed. Add two cheap indexed gate queries. Gate A checks whether any changed file introduced a class/interface/trait/struct/record node (hierarchy may have new implementors reachable from unchanged call sites). Gate B checks whether any changed file added a call edge to a class-kind target (RTA set may have grown, enabling previously filtered expansions in unchanged callers). If neither gate fires, restrict the candidate query to src.file IN changedFiles — safe because the hierarchy and instantiated set are unchanged for all other files. Full builds (isFullBuild=true) and cases where either gate fires retain the existing full-scan behaviour. Mirrors the changed-files scoping pattern of runPostNativeThisDispatch. Closes #1441 * fix(native): add post-pass phase timings to result.phases Times each JS post-pass in tryNativeOrchestrator and exposes the measurements in BuildResult.phases: - gapDetectMs — dropped-language gap detection + backfill - chaMs — CHA expansion (interface dispatch) - thisDispatchMs — this/super dispatch WASM re-parse (was already tracked but now properly named alongside the rest) - reclassifyMs — scoped role re-classification after edge insertion - techniqueBackfillMs — technique-column UPDATE on native-written edges Previously only thisDispatchMs was reported, causing wall-clock vs phaseSum to diverge by 1.1s+ on 1-file rebuilds and making benchmark regressions undiagnosable from committed history. Updates update-incremental-report.ts to render the new phases in a collapsible details block under each engine's 1-file rebuild section. Closes #1434 * fix(perf): correct INLINE_BACKFILL_THRESHOLD docstring; raise threshold for required-tier grammars The docstring claimed pool cost was "amortised over enough parse work" — measurements show IPC overhead scales linearly (~55–64ms/file pool vs ~8–10ms/file inline). The real motivation is crash safety for exotic WASM grammars (#965); JS/TS/TSX (required-tier, used in all this-dispatch backfill calls) have never triggered the V8 fatal crash class and are safe to run inline. Raise threshold 16 → 32 to keep typical this-dispatch batches (≤ 18 files on the codegraph corpus) on the inline fast path. Exotic-language drops are almost always well under 32 files and also benefit from the inline path without meaningful crash risk increase. Closes #1435 * fix(perf): guard post-native passes against unnecessary work on 1-file incremental rebuilds On 1-file native incremental builds, two JS post-passes ran unconditionally even when they had no work to do: - `backfillNativeDroppedFiles`: called whenever changedCount > 0, even when detectDroppedLanguageGap returned an empty gap. Gate now checks gap.missingAbs.length > 0 || gap.staleRel.length > 0 directly, matching backfillNativeDroppedFiles's own internal early-exit guard. - Node/edge COUNT(*) re-count: ran unconditionally after all post-passes even when none of them wrote any edges. COUNT(*) over 50K+ edge tables is non-trivial, especially via the NativeDbProxy napi-rs round-trip. Now gated on postPassWroteData (backfill | CHA edges | this-dispatch edges). Closes #1454 * chore(types): remove dead protoMethodsMs field and stale comment The post-pass it timed (runPostNativePrototypeMethods) was deleted in b5c03a2 when func-prop extraction moved to Rust (#1432). The optional field was never set by any code path that survived the deletion. Also remove the stale reference to "prototype-methods post-pass" from the parseFilesWasmForBackfill docstring — only the this-dispatch post-pass uses symbolsOnly now. Closes #1432 * fix: class-scope field annotation typeMap keys to prevent cross-class collision Field type annotations (`private repo: OrderRepository`) were seeded as bare file-wide typeMap keys, causing `this.repo` inside `UserService` to resolve to `OrderRepository` when both classes had a `repo` field (issue #1458). Both extractors (TS `handleFieldDefTypeMap` and Rust `field_definition` branch) now seed `ClassName.field` keys at confidence 0.9, matching the `CallerClass.X` resolver fallback added in PR #1382. Bare keys are kept at confidence 0.6 as fallbacks for single-class files or class expressions where no enclosing class name is available. Both engines change identically — parity preserved. * fix(bench): update elixir/julia/objc expected-edges to module-qualified names The resolution benchmark uses WASM-built graphs where the Elixir, Julia, and Objective-C extractors emit module-qualified symbol names (Main.run, App.main, UserService.create_user, etc.). The expected-edges manifests were written with bare unqualified names (run, main, create_user), so every correctly-resolved edge appeared as a false positive and every expected edge appeared as a false negative — causing all three languages to show 0% precision even though resolution was working correctly. Root cause: starting in v3.12.0, cross-module call resolution began working for these languages (via the improved receiver-dispatch and same-class fallback in resolveByMethodOrGlobal / build-edges.ts). With 0 edges previously resolved, the name mismatch was invisible; once edges started resolving, the manifests showed 17 FP (elixir), 11 FP (julia), 6 FP (objc) — all correctly resolved edges misidentified as false positives. Fix: - Update all three expected-edges.json manifests to use the module-qualified names matching actual extractor output: elixir: Main.run, UserService.create_user, Validators.validate_user, etc. julia: App.main, Service.create_user, Repository.new_repo, etc. objc: full ObjC selectors (createUserWithId:name:email:, isValidEmail:, etc.) plus add main -> run (plain C call correctly resolved) - Ratchet THRESHOLDS for all three: elixir: precision 0.0 -> 1.0, recall 0.0 -> 0.8 (17/21 resolved) julia: precision 0.0 -> 1.0, recall 0.0 -> 0.7 (11/15 resolved) objc: precision 0.0 -> 1.0, recall 0.0 -> 0.4 (6/13 resolved) Remaining FNs are genuine unresolved edges (same-file bare calls in elixir/julia, receiver-typed message sends in objc) — not regressions. Closes #1447 * fix(wasm): emit receiver edges for declaration-typed locals in C++/CUDA The JS C++ and CUDA extractors had no handler for 'declaration' AST nodes, so typeMap was never seeded for statically-typed locals (e.g. 'UserService svc;'). Without a typeMap entry for 'svc', resolveReceiverEdge had nothing to look up and silently skipped the receiver edge. Add handleCppDeclaration / handleCudaDeclaration to both extractors. They mirror match_c_family_type_map ('declaration' branch) from the native Rust path: extract the type node text and seed typeMap[varName] = { type, confidence: 0.9 } for each identifier or init_declarator child. Primitive types (int, char, bool, …) are skipped to avoid spurious edges. parity-compare.mjs --langs cpp,cuda --hybrid: PARITY OK (wasm = native = hybrid) All 3044 tests pass. * fix(native): resolve Go factory and Python constructor receiver types in Rust solver Go extractor was only seeding typeMap for var_spec and parameter_declaration, missing short_var_declaration. Added infer_short_var_types to handle: - x := Struct{} → conf 1.0 (composite literal) - x := &Struct{} → conf 1.0 (address-of composite) - x := NewFoo() / x := pkg.NewFoo() → conf 0.7 (New* factory prefix) Python extractor was only seeding typeMap for typed_parameter and typed_default_parameter, missing plain assignment. Added infer_py_assignment_type to handle: - order = Order(...) → conf 1.0 (uppercase constructor) - obj = Module.Class(...) → conf 0.7 (uppercase module prefix, non-builtin) Both mirror the existing JS extractors exactly. Parity check for go and python: wasm vs native/hybrid OK. * fix: align enclosing-caller attribution for variable bindings (haskell, zig) Both engines used different rules for attributing calls inside variable bindings: WASM: attributed to the narrowest enclosing span regardless of kind, so local variable declarations inside fn main() shadowed the enclosing function (Zig: calls attributed to repo/svc variables instead of main), and nested let-bindings inside a Haskell do-block shadowed the top-level main binding. Native: loaded allNodes from a query that excluded 'variable' kind, so top-level Haskell bind nodes (main = do …, kind='variable') never matched in defs_with_ids, causing all calls to fall back to the file node. Unified rule implemented in findCaller (TS) and find_enclosing_caller (Rust): - Function/method definitions are preferred over any variable/constant binding as the enclosing caller scope — local var declarations inside a function body never shadow the enclosing function (fixes Zig repo/svc attribution). - When no function/method encloses the call, fall back to the WIDEST (outermost) variable/constant binding — this handles Haskell where main is a top-level bind node with kind 'variable'. Widest span is used so that nested let-bindings do not shadow the outer main binding. - File node remains the absolute last resort. Also adds 'variable' to NODE_KIND_FILTER_SQL (JS) and EDGE_NODE_KIND_FILTER (Rust pipeline.rs) so top-level variable bindings are included in the allNodes set available for caller matching. parity-compare.mjs --langs haskell,zig --hybrid: PARITY OK — 2/2 fixtures. * chore(lint): fix unused import and formatting in cpp/cuda extractors and test Remove unused TypeMapEntry import from cpp.ts and cuda.ts, reformat primitive-type Set literals and test expect() calls to satisfy biome line-length rules. * fix: align Java interface dispatch across wasm/native/hybrid Java was the only fixture where all three build paths (wasm, native, hybrid) disagreed pairwise. Bug 1 — WASM typeMap pollution: `handleJavaLocalVarDecl` used last-wins Map.set(), so the local `InMemoryUserRepository repo` in the static `createDefault()` method silently overrode the constructor parameter `UserRepository repo`. This caused WASM to bypass the interface and resolve directly to the concrete class, producing no interface edge and the wrong receiver. Fix: switch to first-wins `setTypeMapEntry` to match Rust extractor semantics. First-wins preserves the interface annotation that drives correct CHA dispatch. Bug 2 — native vs wasm/hybrid confidence mismatch: `runPostNativeCha` (native orchestrator path) used `computeConfidence − CHA_DISPATCH_PENALTY = 0.7 − 0.1 = 0.6`, while `runChaPostPass` (DB post-pass used by wasm and hybrid) hardcodes 0.8. Fix: align `runPostNativeCha` to also use 0.8. Result: all three build paths now emit identical edges and confidences. `parity-compare.mjs --langs java --hybrid` passes. Updated expected-edges.json to include both the interface declaration edge (TypeRepository.X at 0.7) and the CHA-expanded impl edge (InMemoryUserRepository.X at 0.8), which are the correct semantics for an interface-typed receiver. Closes #1469 * fix(wasm): align typed-receiver CHA dispatch confidence to 0.8 The inline CHA expansion in buildCallEdges and buildChaPostPass used computeConfidence(relPath, t.file) - CHA_DISPATCH_PENALTY for all CHA targets, producing 0.6 for cross-directory interface dispatch (same-dir = 0.7, minus 0.1 penalty). runChaPostPass (helpers.ts) and runPostNativeCha (native-orchestrator.ts) both hardcode 0.8 for interface/CHA-dispatch edges. The deduplication in runChaPostPass uses the existing DB edge as-is and skips reinsertion, so the 0.6 edges from the inline pass were never upgraded to 0.8. Fix: typed-receiver (interface) dispatch branches now use hardcoded 0.8 matching the post-pass constants. The this/super branch keeps computeConfidence-based proximity scoring to remain aligned with runPostNativeThisDispatch. parity-compare.mjs --langs typescript --hybrid goes green (was 12 edge diffs). Closes #1470 docs check acknowledged * fix(caller): use nullish coalescing for endLine to avoid treating 0 as unbounded Replace `|| Infinity` with `?? Infinity` in findCaller so that a definition with endLine=0 (a valid single-line node at the start of a file) is not incorrectly treated as having unbounded span. * fix(wasm): port missing node extractions to JS extractor Closes #1471 Four extraction gaps in the WASM JS extractor caused 25 node diffs against the native engine in the jelly-micro parity fixture: 1. Computed property method names (e.g. `['property7']`): the query patterns in both parser.ts and wasm-worker-entry.ts only matched `property_identifier` and `private_property_identifier` as the `name` field of `method_definition`; add a third pattern for `computed_property_name`. 2. Class expressions inside functions (`return class PostMixin …`): wasm-worker-entry.ts JS_CLASS_PATTERN was a single string matching only `class_declaration`; rename to JS_CLASS_PATTERNS array and add the `(class name: …)` pattern for anonymous class-expression nodes. Also add the matching `(class name: (type_identifier) …)` pattern to TS_EXTRA_PATTERNS so TS class expressions are covered too. 3. Array destructuring constants (`const [x, y] = …`): both `extractDestructuredBindingsWalk` (query path) and `handleVariableDecl` (walk path) only handled `object_pattern`; add an `array_pattern` branch that emits a single `constant` node whose name is the full array pattern text — matching native behaviour. 4. Parameters of prototype arrow/function methods (`Arit.prototype.sum = (x, y) => …`): `emitPrototypeMethod` emitted the definition node without calling `extractParameters`, so children were always absent; add the call and wire the result through. After this fix `parity-compare.mjs --langs jelly-micro` reports 0 node diffs (was 25). 22 pre-existing edge diffs remain (tracked in #1506) — those are out of scope. * fix(extractor): use setTypeMapEntry (first-wins) in C++/CUDA declaration handlers
1 parent 6e09787 commit 18d80c9

4 files changed

Lines changed: 145 additions & 2 deletions

File tree

src/domain/parser.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
158158
'(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
159159
'(method_definition name: (property_identifier) @meth_name) @meth_node',
160160
'(method_definition name: (private_property_identifier) @meth_name) @meth_node',
161+
'(method_definition name: (computed_property_name) @meth_name) @meth_node',
161162
'(import_statement source: (string) @imp_source) @imp_node',
162163
'(export_statement) @exp_node',
163164
'(call_expression function: (identifier) @callfn_name) @callfn_node',

src/domain/wasm-worker-entry.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ const COMMON_QUERY_PATTERNS: string[] = [
115115
'(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)',
116116
'(method_definition name: (property_identifier) @meth_name) @meth_node',
117117
'(method_definition name: (private_property_identifier) @meth_name) @meth_node',
118+
'(method_definition name: (computed_property_name) @meth_name) @meth_node',
118119
'(import_statement source: (string) @imp_source) @imp_node',
119120
'(export_statement) @exp_node',
120121
'(call_expression function: (identifier) @callfn_name) @callfn_node',
@@ -125,11 +126,17 @@ const COMMON_QUERY_PATTERNS: string[] = [
125126
'(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node',
126127
];
127128

128-
const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node';
129+
const JS_CLASS_PATTERNS: string[] = [
130+
'(class_declaration name: (identifier) @cls_name) @cls_node',
131+
// class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }`
132+
'(class name: (identifier) @cls_name) @cls_node',
133+
];
129134

130135
const TS_EXTRA_PATTERNS: string[] = [
131136
'(class_declaration name: (type_identifier) @cls_name) @cls_node',
132137
'(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node',
138+
// class expressions: `return class Foo extends Bar { ... }`
139+
'(class name: (type_identifier) @cls_name) @cls_node',
133140
'(interface_declaration name: (type_identifier) @iface_name) @iface_node',
134141
'(type_alias_declaration name: (type_identifier) @type_name) @type_node',
135142
];
@@ -433,7 +440,7 @@ async function loadLanguageLazy(entry: LanguageRegistryEntry): Promise<Parser |
433440
const isTS = entry.id === 'typescript' || entry.id === 'tsx';
434441
const patterns = isTS
435442
? [...COMMON_QUERY_PATTERNS, ...TS_EXTRA_PATTERNS]
436-
: [...COMMON_QUERY_PATTERNS, JS_CLASS_PATTERN];
443+
: [...COMMON_QUERY_PATTERNS, ...JS_CLASS_PATTERNS];
437444
_queries.set(entry.id, new Query(lang, patterns.join('\n')));
438445
}
439446
return parser;

src/extractors/javascript.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,15 @@ function extractDestructuredBindingsWalk(node: TreeSitterNode, definitions: Defi
507507
nodeEndLine(declNode),
508508
definitions,
509509
);
510+
} else if (nameN && nameN.type === 'array_pattern') {
511+
// `const [x, y] = ...` — emit a single constant node whose name is the
512+
// full array pattern text (e.g. `[x, y]`), matching native engine behaviour.
513+
definitions.push({
514+
name: nameN.text,
515+
kind: 'constant',
516+
line: nodeStartLine(declNode),
517+
endLine: nodeEndLine(declNode),
518+
});
510519
}
511520
}
512521
}
@@ -1017,6 +1026,16 @@ function handleVariableDecl(node: TreeSitterNode, ctx: ExtractorOutput): void {
10171026
nodeEndLine(node),
10181027
ctx.definitions,
10191028
);
1029+
} else if (isConst && nameN.type === 'array_pattern' && !hasFunctionScopeAncestor(node)) {
1030+
// Array destructuring: `const [x, y] = ...` — emit a single constant node
1031+
// whose name is the full array pattern text (e.g. `[x, y]`), matching
1032+
// native engine behaviour. Scope guard mirrors the object_pattern branch above.
1033+
ctx.definitions.push({
1034+
name: nameN.text,
1035+
kind: 'constant',
1036+
line: nodeStartLine(node),
1037+
endLine: nodeEndLine(node),
1038+
});
10201039
}
10211040
}
10221041
}
@@ -3359,11 +3378,13 @@ function emitPrototypeMethod(
33593378
): void {
33603379
const fullName = `${className}.${methodName}`;
33613380
if (rhs.type === 'function_expression' || rhs.type === 'arrow_function') {
3381+
const params = extractParameters(rhs);
33623382
definitions.push({
33633383
name: fullName,
33643384
kind: 'method',
33653385
line: nodeStartLine(rhs),
33663386
endLine: nodeEndLine(rhs),
3387+
children: params.length > 0 ? params : undefined,
33673388
});
33683389
} else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) {
33693390
// Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' }

tests/parsers/javascript.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,4 +1265,118 @@ describe('JavaScript parser', () => {
12651265
);
12661266
});
12671267
});
1268+
1269+
describe('computed method name extraction (#1471)', () => {
1270+
it('extracts computed getter method from object literal', () => {
1271+
const symbols = parseJS(`const obj = { get ['property7']() {} };`);
1272+
expect(symbols.definitions).toContainEqual(
1273+
expect.objectContaining({ name: "['property7']", kind: 'method' }),
1274+
);
1275+
});
1276+
1277+
it('extracts computed setter method with parameter from object literal', () => {
1278+
const symbols = parseJS(`const obj = { set ['property8'](value) {} };`);
1279+
const def = symbols.definitions.find((d) => d.name === "['property8']");
1280+
expect(def).toBeDefined();
1281+
expect(def).toMatchObject({ kind: 'method' });
1282+
expect(def!.children).toContainEqual(
1283+
expect.objectContaining({ name: 'value', kind: 'parameter' }),
1284+
);
1285+
});
1286+
1287+
it('extracts computed regular method with parameter from object literal', () => {
1288+
const symbols = parseJS(`const obj = { ['property9'](parameters) {} };`);
1289+
const def = symbols.definitions.find((d) => d.name === "['property9']");
1290+
expect(def).toBeDefined();
1291+
expect(def!.children).toContainEqual(
1292+
expect.objectContaining({ name: 'parameters', kind: 'parameter' }),
1293+
);
1294+
});
1295+
1296+
it('extracts computed generator method from object literal', () => {
1297+
const symbols = parseJS(`const obj = { *['generator10'](parameters) {} };`);
1298+
expect(symbols.definitions).toContainEqual(
1299+
expect.objectContaining({ name: "['generator10']", kind: 'method' }),
1300+
);
1301+
});
1302+
1303+
it('extracts computed async method from object literal', () => {
1304+
const symbols = parseJS(`const obj = { async ['property11'](parameters) {} };`);
1305+
expect(symbols.definitions).toContainEqual(
1306+
expect.objectContaining({ name: "['property11']", kind: 'method' }),
1307+
);
1308+
});
1309+
});
1310+
1311+
describe('class expression inside function extraction (#1471)', () => {
1312+
it('extracts named class expression returned from a function', () => {
1313+
const symbols = parseJS(
1314+
`function mixin() { return class PostMixin extends A { constructor() { super(); } }; }`,
1315+
);
1316+
expect(symbols.definitions).toContainEqual(
1317+
expect.objectContaining({ name: 'PostMixin', kind: 'class' }),
1318+
);
1319+
});
1320+
1321+
it('records extends relationship for class expression inside function', () => {
1322+
const symbols = parseJS(`function mixin() { return class PostMixin extends A { m() {} }; }`);
1323+
expect(symbols.classes).toContainEqual(
1324+
expect.objectContaining({ name: 'PostMixin', extends: 'A' }),
1325+
);
1326+
});
1327+
1328+
it('extracts class field properties as children of class expression', () => {
1329+
const symbols = parseJS(
1330+
`function mixin() { return class PostMixin extends A { w = 1; eee = this; }; }`,
1331+
);
1332+
const pm = symbols.definitions.find((d) => d.name === 'PostMixin');
1333+
expect(pm).toBeDefined();
1334+
expect(pm!.children).toContainEqual(expect.objectContaining({ name: 'w', kind: 'property' }));
1335+
expect(pm!.children).toContainEqual(
1336+
expect.objectContaining({ name: 'eee', kind: 'property' }),
1337+
);
1338+
});
1339+
});
1340+
1341+
describe('array destructuring constant extraction (#1471)', () => {
1342+
it('extracts const array pattern as a single constant node', () => {
1343+
const symbols = parseJS(`const [x, y] = new Set([() => {}, () => {}]);`);
1344+
expect(symbols.definitions).toContainEqual(
1345+
expect.objectContaining({ name: '[x, y]', kind: 'constant' }),
1346+
);
1347+
});
1348+
1349+
it('does not extract let or var array destructuring', () => {
1350+
const symbols = parseJS(`let [a, b] = [1, 2];`);
1351+
expect(symbols.definitions.every((d) => d.name !== '[a, b]')).toBe(true);
1352+
});
1353+
});
1354+
1355+
describe('prototype method parameter extraction (#1471)', () => {
1356+
it('extracts parameters from Foo.prototype.bar = (x, y) => arrow', () => {
1357+
const symbols = parseJS(`function Arit() {}\nArit.prototype.sum = (x, y) => x + y;`);
1358+
const def = symbols.definitions.find((d) => d.name === 'Arit.sum');
1359+
expect(def).toBeDefined();
1360+
expect(def!.children).toContainEqual(
1361+
expect.objectContaining({ name: 'x', kind: 'parameter' }),
1362+
);
1363+
expect(def!.children).toContainEqual(
1364+
expect.objectContaining({ name: 'y', kind: 'parameter' }),
1365+
);
1366+
});
1367+
1368+
it('extracts parameters from Foo.prototype.bar = function(key, value)', () => {
1369+
const symbols = parseJS(
1370+
`function Foo() {}\nFoo.prototype.add = function(key, value) { this[key] = value; };`,
1371+
);
1372+
const def = symbols.definitions.find((d) => d.name === 'Foo.add');
1373+
expect(def).toBeDefined();
1374+
expect(def!.children).toContainEqual(
1375+
expect.objectContaining({ name: 'key', kind: 'parameter' }),
1376+
);
1377+
expect(def!.children).toContainEqual(
1378+
expect.objectContaining({ name: 'value', kind: 'parameter' }),
1379+
);
1380+
});
1381+
});
12681382
});

0 commit comments

Comments
 (0)