From 37bb91a3e648e129583c56e2707497c329d932be Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 20:52:40 -0600 Subject: [PATCH 1/4] fix(wasm): port computed methods, class expressions, array destructuring, prototype params to JS extractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/domain/parser.ts | 1 + src/domain/wasm-worker-entry.ts | 11 ++- src/extractors/javascript.ts | 21 ++++++ tests/parsers/javascript.test.ts | 114 +++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+), 2 deletions(-) diff --git a/src/domain/parser.ts b/src/domain/parser.ts index 267895c8..8cbe9766 100644 --- a/src/domain/parser.ts +++ b/src/domain/parser.ts @@ -158,6 +158,7 @@ const COMMON_QUERY_PATTERNS: string[] = [ '(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)', '(method_definition name: (property_identifier) @meth_name) @meth_node', '(method_definition name: (private_property_identifier) @meth_name) @meth_node', + '(method_definition name: (computed_property_name) @meth_name) @meth_node', '(import_statement source: (string) @imp_source) @imp_node', '(export_statement) @exp_node', '(call_expression function: (identifier) @callfn_name) @callfn_node', diff --git a/src/domain/wasm-worker-entry.ts b/src/domain/wasm-worker-entry.ts index 25c05829..4541b073 100644 --- a/src/domain/wasm-worker-entry.ts +++ b/src/domain/wasm-worker-entry.ts @@ -115,6 +115,7 @@ const COMMON_QUERY_PATTERNS: string[] = [ '(variable_declarator name: (identifier) @varfn_name value: (generator_function) @varfn_value)', '(method_definition name: (property_identifier) @meth_name) @meth_node', '(method_definition name: (private_property_identifier) @meth_name) @meth_node', + '(method_definition name: (computed_property_name) @meth_name) @meth_node', '(import_statement source: (string) @imp_source) @imp_node', '(export_statement) @exp_node', '(call_expression function: (identifier) @callfn_name) @callfn_node', @@ -125,11 +126,17 @@ const COMMON_QUERY_PATTERNS: string[] = [ '(expression_statement (assignment_expression left: (member_expression) @assign_left right: (_) @assign_right)) @assign_node', ]; -const JS_CLASS_PATTERN: string = '(class_declaration name: (identifier) @cls_name) @cls_node'; +const JS_CLASS_PATTERNS: string[] = [ + '(class_declaration name: (identifier) @cls_name) @cls_node', + // class expressions: `return class Foo extends Bar { ... }` or `const X = class Foo { ... }` + '(class name: (identifier) @cls_name) @cls_node', +]; const TS_EXTRA_PATTERNS: string[] = [ '(class_declaration name: (type_identifier) @cls_name) @cls_node', '(abstract_class_declaration name: (type_identifier) @cls_name) @cls_node', + // class expressions: `return class Foo extends Bar { ... }` + '(class name: (type_identifier) @cls_name) @cls_node', '(interface_declaration name: (type_identifier) @iface_name) @iface_node', '(type_alias_declaration name: (type_identifier) @type_name) @type_node', ]; @@ -433,7 +440,7 @@ async function loadLanguageLazy(entry: LanguageRegistryEntry): Promise 0 ? params : undefined, }); } else if (rhs.type === 'identifier' && !BUILTIN_GLOBALS.has(rhs.text)) { // Prototype alias: `A.prototype.t = f` → typeMap['A.t'] = { type: 'f' } diff --git a/tests/parsers/javascript.test.ts b/tests/parsers/javascript.test.ts index 9d0ec99d..c7315760 100644 --- a/tests/parsers/javascript.test.ts +++ b/tests/parsers/javascript.test.ts @@ -1265,4 +1265,118 @@ describe('JavaScript parser', () => { ); }); }); + + describe('computed method name extraction (#1471)', () => { + it('extracts computed getter method from object literal', () => { + const symbols = parseJS(`const obj = { get ['property7']() {} };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: "['property7']", kind: 'method' }), + ); + }); + + it('extracts computed setter method with parameter from object literal', () => { + const symbols = parseJS(`const obj = { set ['property8'](value) {} };`); + const def = symbols.definitions.find((d) => d.name === "['property8']"); + expect(def).toBeDefined(); + expect(def).toMatchObject({ kind: 'method' }); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'value', kind: 'parameter' }), + ); + }); + + it('extracts computed regular method with parameter from object literal', () => { + const symbols = parseJS(`const obj = { ['property9'](parameters) {} };`); + const def = symbols.definitions.find((d) => d.name === "['property9']"); + expect(def).toBeDefined(); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'parameters', kind: 'parameter' }), + ); + }); + + it('extracts computed generator method from object literal', () => { + const symbols = parseJS(`const obj = { *['generator10'](parameters) {} };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: "['generator10']", kind: 'method' }), + ); + }); + + it('extracts computed async method from object literal', () => { + const symbols = parseJS(`const obj = { async ['property11'](parameters) {} };`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: "['property11']", kind: 'method' }), + ); + }); + }); + + describe('class expression inside function extraction (#1471)', () => { + it('extracts named class expression returned from a function', () => { + const symbols = parseJS( + `function mixin() { return class PostMixin extends A { constructor() { super(); } }; }`, + ); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: 'PostMixin', kind: 'class' }), + ); + }); + + it('records extends relationship for class expression inside function', () => { + const symbols = parseJS(`function mixin() { return class PostMixin extends A { m() {} }; }`); + expect(symbols.classes).toContainEqual( + expect.objectContaining({ name: 'PostMixin', extends: 'A' }), + ); + }); + + it('extracts class field properties as children of class expression', () => { + const symbols = parseJS( + `function mixin() { return class PostMixin extends A { w = 1; eee = this; }; }`, + ); + const pm = symbols.definitions.find((d) => d.name === 'PostMixin'); + expect(pm).toBeDefined(); + expect(pm!.children).toContainEqual(expect.objectContaining({ name: 'w', kind: 'property' })); + expect(pm!.children).toContainEqual( + expect.objectContaining({ name: 'eee', kind: 'property' }), + ); + }); + }); + + describe('array destructuring constant extraction (#1471)', () => { + it('extracts const array pattern as a single constant node', () => { + const symbols = parseJS(`const [x, y] = new Set([() => {}, () => {}]);`); + expect(symbols.definitions).toContainEqual( + expect.objectContaining({ name: '[x, y]', kind: 'constant' }), + ); + }); + + it('does not extract let or var array destructuring', () => { + const symbols = parseJS(`let [a, b] = [1, 2];`); + expect(symbols.definitions.every((d) => d.name !== '[a, b]')).toBe(true); + }); + }); + + describe('prototype method parameter extraction (#1471)', () => { + it('extracts parameters from Foo.prototype.bar = (x, y) => arrow', () => { + const symbols = parseJS(`function Arit() {}\nArit.prototype.sum = (x, y) => x + y;`); + const def = symbols.definitions.find((d) => d.name === 'Arit.sum'); + expect(def).toBeDefined(); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'x', kind: 'parameter' }), + ); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'y', kind: 'parameter' }), + ); + }); + + it('extracts parameters from Foo.prototype.bar = function(key, value)', () => { + const symbols = parseJS( + `function Foo() {}\nFoo.prototype.add = function(key, value) { this[key] = value; };`, + ); + const def = symbols.definitions.find((d) => d.name === 'Foo.add'); + expect(def).toBeDefined(); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'key', kind: 'parameter' }), + ); + expect(def!.children).toContainEqual( + expect.objectContaining({ name: 'value', kind: 'parameter' }), + ); + }); + }); }); From 893af0faad84ed14754164c1daac06bdaf9e8ecc Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 21:29:57 -0600 Subject: [PATCH 2/4] fix: eliminate super-dispatch self-loops in CHA post-pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runChaPostPass (helpers.ts, WASM path) was emitting self-loop edges like B.m → B.m when expanding class A's implementors: it found B.m via findMethodStmt but had no guard against the case where methodNode.id equals the source_id (the calling method itself). Add the missing guard. runPostNativeThisDispatch (native-orchestrator.ts) similarly lacked a self-loop guard in its targets loop — add it to keep the native path consistent. Also aligns runPostNativeCha to use file-pair-aware confidence (computeConfidence - CHA_DISPATCH_PENALTY) instead of a hardcoded 0.8, matching the WASM path. Gate B now checks the same kind set as Gate A for future-proofing. Remaining jelly-micro divergences filed as separate issues: - #1510: receiver-callee-mixup attribution (findCaller tie-break for same-line defs) - #1511: dynamic pts cross-file edges (fun.js, classes2.js apply/call) - #1512: native missing f.h→f.g this-dispatch for func-prop methods - #1513: prototypes.js receiver edge non-determinism docs check acknowledged: no language, feature, or architecture changes. Closes #1472 --- src/domain/graph/builder/helpers.ts | 1 + .../builder/stages/native-orchestrator.ts | 23 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/domain/graph/builder/helpers.ts b/src/domain/graph/builder/helpers.ts index dd94ea2b..6b92d44d 100644 --- a/src/domain/graph/builder/helpers.ts +++ b/src/domain/graph/builder/helpers.ts @@ -511,6 +511,7 @@ export function runChaPostPass(db: BetterSqlite3Database): number { const qualifiedName = `${cls}.${methodSuffix}`; const methodNodes = findMethodStmt.all(qualifiedName) as Array<{ id: number }>; for (const methodNode of methodNodes) { + if (methodNode.id === source_id) continue; // skip self-loops const key = `${source_id}|${methodNode.id}`; if (seen.has(key)) continue; seen.add(key); diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index 0fe4555c..2872e3f0 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -523,7 +523,10 @@ function runPostNativeCha( if (row) gateAFired = true; } - // Gate B: calls from changed-file sources to class-kind targets? + // Gate B: calls from changed-file sources to instantiable-kind targets? + // Checks the same kind set as Gate A (class/interface/trait/struct/record) + // so that future CHA extensions to struct/record kinds correctly trigger + // the full scan when RTA evidence grows in a changed file. let gateBFired = false; if (!gateAFired) { for (let i = 0; i < changedFiles.length && !gateBFired; i += CHUNK_SIZE) { @@ -534,7 +537,8 @@ function runPostNativeCha( `SELECT 1 FROM edges e JOIN nodes src ON e.source_id = src.id JOIN nodes tgt ON e.target_id = tgt.id - WHERE e.kind = 'calls' AND tgt.kind = 'class' + WHERE e.kind = 'calls' + AND tgt.kind IN ('class', 'interface', 'trait', 'struct', 'record') AND src.file IN (${ph}) LIMIT 1`, ) @@ -556,8 +560,8 @@ function runPostNativeCha( } // Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork'). - // Include caller_file and method_file so affectedFiles can be populated for - // incremental role reclassification; confidence is hardcoded 0.8 matching runChaPostPass. + // Include the caller node's file so confidence can be computed file-pair-aware, + // matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula. // When scopeToChangedFiles is true, restrict to call sites in the changed files // (safe because no hierarchy or RTA evidence changed outside those files). let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>; @@ -653,10 +657,12 @@ function runPostNativeCha( const key = `${source_id}|${methodNode.id}`; if (seen.has(key)) continue; seen.add(key); - // Use the same hardcoded 0.8 that runChaPostPass (helpers.ts) uses for - // DB-level CHA dispatch edges. This aligns the native orchestrator path - // with the WASM and hybrid paths, which both go through runChaPostPass. - const conf = 0.8; + // Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY) + // Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour. + const conf = + computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) - + CHA_DISPATCH_PENALTY; + if (conf <= 0) continue; newEdges.push([source_id, methodNode.id, 'calls', conf, 0, 'cha']); newEdgeCount++; if (caller_file) affectedFiles.add(caller_file); @@ -900,6 +906,7 @@ async function runPostNativeThisDispatch( ); for (const t of targets) { + if (t.id === callerRow.id) continue; // skip self-loops const key = `${callerRow.id}|${t.id}`; if (seen.has(key)) continue; seen.add(key); From fc6bc92bb5982bce0cf5b4969c6998a9b0c276c8 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Fri, 12 Jun 2026 22:20:19 -0600 Subject: [PATCH 3/4] fix(cha): add self-loop guard to runPostNativeCha and TODO for confidence alignment (#1514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runPostNativeCha's BFS inner loop was missing the same self-loop guard already added to runChaPostPass and runPostNativeThisDispatch, allowing B.m→B.m edges to persist on the native CHA path when super-dispatch triggers CHA expansion. Also adds a TODO comment on the hardcoded 0.8 confidence in runChaPostPass noting it needs alignment with the formula used in runPostNativeCha (requires extending findMethodStmt to fetch file). --- src/domain/graph/builder/helpers.ts | 2 +- src/domain/graph/builder/stages/native-orchestrator.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/graph/builder/helpers.ts b/src/domain/graph/builder/helpers.ts index 6b92d44d..22b953cb 100644 --- a/src/domain/graph/builder/helpers.ts +++ b/src/domain/graph/builder/helpers.ts @@ -515,7 +515,7 @@ export function runChaPostPass(db: BetterSqlite3Database): number { const key = `${source_id}|${methodNode.id}`; if (seen.has(key)) continue; seen.add(key); - newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']); + newEdges.push([source_id, methodNode.id, 'calls', 0.8, 0, 'cha']); // TODO: align with runPostNativeCha: computeConfidence(callerFile, methodFile) - CHA_DISPATCH_PENALTY (requires extending findMethodStmt to fetch file) } } diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index 2872e3f0..935f7786 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -654,6 +654,7 @@ function runPostNativeCha( method_file: string | null; }>; for (const methodNode of methodNodes) { + if (methodNode.id === source_id) continue; // skip self-loops const key = `${source_id}|${methodNode.id}`; if (seen.has(key)) continue; seen.add(key); From 5905bcc963123128002bb83eea4a306ef38637a9 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 13 Jun 2026 00:14:41 -0600 Subject: [PATCH 4/4] fix(comments): correct inaccurate WASM-parity claims in runPostNativeCha (#1514) The comments said the confidence formula "mirrors the WASM path" but the WASM path (runChaPostPass in helpers.ts) still uses hardcoded 0.8, not computeConfidence - CHA_DISPATCH_PENALTY. Correct both comments to accurately describe the native formula and cross-reference the TODO in helpers.ts for the pending alignment. --- src/domain/graph/builder/stages/native-orchestrator.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/graph/builder/stages/native-orchestrator.ts b/src/domain/graph/builder/stages/native-orchestrator.ts index 935f7786..fbc7b92e 100644 --- a/src/domain/graph/builder/stages/native-orchestrator.ts +++ b/src/domain/graph/builder/stages/native-orchestrator.ts @@ -561,7 +561,8 @@ function runPostNativeCha( // Find existing call edges targeting qualified methods (e.g., 'IWorker.doWork'). // Include the caller node's file so confidence can be computed file-pair-aware, - // matching the WASM path's computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY formula. + // using computeConfidence(callerFile, targetFile, null) - CHA_DISPATCH_PENALTY. + // (The WASM path in runChaPostPass still uses a hardcoded 0.8 — see TODO there.) // When scopeToChangedFiles is true, restrict to call sites in the changed files // (safe because no hierarchy or RTA evidence changed outside those files). let callToMethods: Array<{ source_id: number; method_name: string; caller_file: string | null }>; @@ -658,7 +659,8 @@ function runPostNativeCha( const key = `${source_id}|${methodNode.id}`; if (seen.has(key)) continue; seen.add(key); - // Compute confidence file-pair-aware (mirrors WASM path: computeConfidence - CHA_DISPATCH_PENALTY) + // Compute confidence file-pair-aware using computeConfidence - CHA_DISPATCH_PENALTY. + // (WASM path runChaPostPass uses 0.8; alignment tracked in helpers.ts TODO.) // Skip zero-confidence edges to match buildFileCallEdges / buildChaPostPass behaviour. const conf = computeConfidence(caller_file ?? '', methodNode.method_file ?? '', null) -