Skip to content

Commit 7294941

Browse files
committed
fix(extractors): align JS ObjC engine with native for @import and calls
Three engine-parity gaps Greptile flagged on the WASM side that broke the "identical output" goal: - @import statements: tree-sitter-objc v3 emits `module_import` not `import_declaration`, so the JS dispatch arm never matched and every `@import Foundation;` was silently dropped. Accept both node types. - C-style calls (printf, CGContextFillRect, …): the grammar lacks a `function` field on `call_expression`, so the named-field lookup always misses. Rust falls back to the first identifier child; JS did not, so every C call was dropped. Add the same fallback. - Message expressions: the grammar tags each keyword identifier with the `method` field rather than exposing a `selector` field, so the JS selector lookup misfired for multi-keyword selectors. Assemble the selector from `method` children with `:` joining, matching `build_message_selector` in the Rust extractor. Also expose `fieldNameForChild` on the `TreeSitterNode` type and add three JS extractor tests covering the new parity behaviour.
1 parent 3c74d43 commit 7294941

3 files changed

Lines changed: 80 additions & 4 deletions

File tree

src/extractors/objc.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ function walkObjCNode(node: TreeSitterNode, ctx: ExtractorOutput): void {
5555
case 'preproc_import':
5656
handleImport(node, ctx);
5757
break;
58+
// tree-sitter-objc v3 emits `module_import` for `@import Foundation;`
59+
// statements. Older grammar revisions used `import_declaration`, so we
60+
// accept both for forward/backward compatibility and keep behaviour
61+
// aligned with `handle_at_import` on the Rust side.
62+
case 'module_import':
5863
case 'import_declaration':
5964
handleAtImport(node, ctx);
6065
break;
@@ -296,7 +301,20 @@ function handleTypedef(node: TreeSitterNode, ctx: ExtractorOutput): void {
296301
// ── Call handlers ─────────────────────────────────────────────────────────
297302

298303
function handleCCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
299-
const funcNode = node.childForFieldName('function');
304+
// tree-sitter-objc does not expose a `function` field on `call_expression`,
305+
// so the named-field lookup almost always misses. Fall back to the first
306+
// `identifier` / `field_expression` child to mirror `handle_c_call_expr` in
307+
// `crates/codegraph-core/src/extractors/objc.rs` and keep engine parity.
308+
let funcNode = node.childForFieldName('function');
309+
if (!funcNode) {
310+
for (let i = 0; i < node.childCount; i++) {
311+
const child = node.child(i);
312+
if (child && (child.type === 'identifier' || child.type === 'field_expression')) {
313+
funcNode = child;
314+
break;
315+
}
316+
}
317+
}
300318
if (!funcNode) return;
301319
const call: Call = { name: '', line: node.startPosition.row + 1 };
302320
if (funcNode.type === 'field_expression') {
@@ -313,10 +331,33 @@ function handleCCallExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
313331
function handleMessageExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
314332
// [receiver selector:arg ...]
315333
const receiver = node.childForFieldName('receiver');
316-
const selector = node.childForFieldName('selector');
317-
if (!selector) return;
318334

319-
const call: Call = { name: selector.text, line: node.startPosition.row + 1 };
335+
// tree-sitter-objc v3 does not expose a `selector` field on
336+
// `message_expression`; instead every keyword identifier has the `method`
337+
// field. Assemble the selector by joining `method` children with `:`,
338+
// appending a trailing `:` when the message has at least one colon
339+
// (keyword form). Mirrors `build_message_selector` in
340+
// `crates/codegraph-core/src/extractors/objc.rs`.
341+
const parts: string[] = [];
342+
let hasColon = false;
343+
for (let i = 0; i < node.childCount; i++) {
344+
const child = node.child(i);
345+
if (!child) continue;
346+
const fieldName = node.fieldNameForChild(i);
347+
if (fieldName === 'method') parts.push(child.text);
348+
if (child.type === ':') hasColon = true;
349+
}
350+
let name: string;
351+
if (parts.length > 0) {
352+
name = hasColon ? `${parts.join(':')}:` : parts.join(':');
353+
} else {
354+
// Fallback: some grammar revisions expose a `selector` field.
355+
const selector = node.childForFieldName('selector');
356+
if (!selector) return;
357+
name = selector.text;
358+
}
359+
360+
const call: Call = { name, line: node.startPosition.row + 1 };
320361
if (receiver) call.receiver = receiver.text;
321362
ctx.calls.push(call);
322363
}

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,7 @@ export interface TreeSitterNode {
576576
child(index: number): TreeSitterNode | null;
577577
namedChild(index: number): TreeSitterNode | null;
578578
childForFieldName(name: string): TreeSitterNode | null;
579+
fieldNameForChild(index: number): string | null;
579580
parent: TreeSitterNode | null;
580581
previousSibling: TreeSitterNode | null;
581582
nextSibling: TreeSitterNode | null;

tests/parsers/objc.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,38 @@ describe('Objective-C parser', () => {
5656
expect.objectContaining({ name: 'MyView', extends: 'UIView' }),
5757
);
5858
});
59+
60+
it('extracts @import module statements', () => {
61+
// tree-sitter-objc v3 emits `module_import` for `@import` statements.
62+
// The Rust extractor dispatches on this node type and the JS extractor
63+
// must match it to keep engine parity (otherwise every `@import` is
64+
// silently dropped on the JS side).
65+
const symbols = parseObjC(`@import Foundation;`);
66+
expect(symbols.imports).toContainEqual(
67+
expect.objectContaining({ source: 'Foundation', names: ['Foundation'] }),
68+
);
69+
});
70+
71+
it('extracts C-style function calls without a `function` field', () => {
72+
// tree-sitter-objc does not expose a `function` field on `call_expression`,
73+
// so the JS extractor must fall back to the first identifier child —
74+
// matching the Rust side. Otherwise C calls like `printf(...)` are
75+
// silently dropped.
76+
const symbols = parseObjC(`void main() {
77+
printf("hello");
78+
}`);
79+
expect(symbols.calls).toContainEqual(expect.objectContaining({ name: 'printf' }));
80+
});
81+
82+
it('builds keyword-selector calls from message expressions', () => {
83+
// The grammar tags each keyword identifier with the `method` field rather
84+
// than exposing a single `selector` field. Mirror the Rust assembly so
85+
// selectors like `initWithName:age:` are recorded identically.
86+
const symbols = parseObjC(`void main() {
87+
[obj initWithName:@"x" age:10];
88+
}`);
89+
expect(symbols.calls).toContainEqual(
90+
expect.objectContaining({ name: 'initWithName:age:', receiver: 'obj' }),
91+
);
92+
});
5993
});

0 commit comments

Comments
 (0)