Skip to content

Commit 37bb91a

Browse files
committed
fix(wasm): port computed methods, class expressions, array destructuring, prototype params 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.
1 parent d87c1ee commit 37bb91a

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)