Skip to content

Commit 29ad42a

Browse files
committed
fix(extractors): align JS ObjC engine with native for v3 grammar (#1106)
The v3 tree-sitter-objc grammar emits flat 'identifier' + 'method_parameter' children directly under method nodes (no 'keyword_selector' wrapper) and nests property names under 'struct_declaration > struct_declarator > [pointer_declarator >] identifier' rather than exposing a 'name' field. The JS extractor was still following the old grammar shape, which silently dropped multi-keyword method definitions, never populated parameter children, and omitted all '@Property' members from class children. - buildSelector: assemble selector from flat 'identifier'+'method_parameter' children directly under the method node, matching build_selector in crates/codegraph-core/src/extractors/objc.rs. - extractMethodParams: iterate 'method_parameter' children directly and use the last 'identifier' child as the parameter name (mirrors extract_method_params in the Rust extractor). - collectClassMembers: extract '@Property' names via a deep identifier walk under 'struct_declaration > struct_declarator' (mirrors extract_property_name in the Rust extractor). Added three regression tests covering keyword method definitions with parameters, and pointer + non-pointer '@Property' member names.
1 parent b64a886 commit 29ad42a

2 files changed

Lines changed: 100 additions & 31 deletions

File tree

src/extractors/objc.ts

Lines changed: 60 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -365,29 +365,25 @@ function handleMessageExpr(node: TreeSitterNode, ctx: ExtractorOutput): void {
365365
// ── Helpers ───────────────────────────────────────────────────────────────
366366

367367
function buildSelector(methodNode: TreeSitterNode): string | null {
368-
const selector = methodNode.childForFieldName('selector');
369-
if (selector) return selector.text;
370-
371-
// Build selector from keyword children: initWith:name:
368+
// tree-sitter-objc v3 does not expose a `selector` field; the selector is
369+
// assembled from the leading `identifier` keywords. Multi-keyword forms
370+
// look like `setName:(...)x age:(...)y` and appear as flat
371+
// `identifier` + `method_parameter` children directly under the method
372+
// node (not wrapped in `keyword_selector`). Mirrors `build_selector` in
373+
// `crates/codegraph-core/src/extractors/objc.rs`.
372374
const parts: string[] = [];
375+
let hasParams = false;
373376
for (let i = 0; i < methodNode.childCount; i++) {
374377
const child = methodNode.child(i);
375378
if (!child) continue;
376-
if (child.type === 'keyword_selector') {
377-
for (let j = 0; j < child.childCount; j++) {
378-
const kw = child.child(j);
379-
if (kw && kw.type === 'keyword_declarator') {
380-
const kwName = kw.childForFieldName('keyword');
381-
if (kwName) parts.push(kwName.text);
382-
}
383-
}
384-
}
385-
if (child.type === 'identifier' && i === 1) {
386-
// Simple unary selector
387-
return child.text;
379+
if (child.type === 'identifier') {
380+
parts.push(child.text);
381+
} else if (child.type === 'method_parameter') {
382+
hasParams = true;
388383
}
389384
}
390-
return parts.length > 0 ? `${parts.join(':')}:` : null;
385+
if (parts.length === 0) return null;
386+
return hasParams ? `${parts.join(':')}:` : parts.join(':');
391387
}
392388

393389
function findObjCParentClass(node: TreeSitterNode): string | null {
@@ -440,32 +436,65 @@ function collectClassMembers(classNode: TreeSitterNode): SubDeclaration[] {
440436
}
441437
}
442438
if (child.type === 'property_declaration') {
443-
const propName = child.childForFieldName('name');
439+
const propName = extractPropertyName(child);
444440
if (propName) {
445-
members.push({ name: propName.text, kind: 'property', line: child.startPosition.row + 1 });
441+
members.push({ name: propName, kind: 'property', line: child.startPosition.row + 1 });
446442
}
447443
}
448444
}
449445
return members;
450446
}
451447

448+
/**
449+
* Extract the property name from `@property (...) Type *foo;`. The v3 grammar
450+
* does not expose `name` as a named field on `property_declaration`; instead
451+
* the identifier nests under `struct_declaration > struct_declarator >
452+
* [pointer_declarator >] identifier`. Mirrors `extract_property_name` in
453+
* `crates/codegraph-core/src/extractors/objc.rs`.
454+
*/
455+
function extractPropertyName(propNode: TreeSitterNode): string | null {
456+
const structDecl = findChild(propNode, 'struct_declaration');
457+
if (!structDecl) return null;
458+
for (let i = 0; i < structDecl.childCount; i++) {
459+
const child = structDecl.child(i);
460+
if (!child || child.type !== 'struct_declarator') continue;
461+
const id = findIdentifierDeep(child);
462+
if (id) return id.text;
463+
}
464+
return null;
465+
}
466+
467+
function findIdentifierDeep(node: TreeSitterNode): TreeSitterNode | null {
468+
if (node.type === 'identifier') return node;
469+
for (let i = 0; i < node.childCount; i++) {
470+
const child = node.child(i);
471+
if (!child) continue;
472+
const found = findIdentifierDeep(child);
473+
if (found) return found;
474+
}
475+
return null;
476+
}
477+
452478
function extractMethodParams(methodNode: TreeSitterNode): SubDeclaration[] {
479+
// The v3 grammar emits flat `method_parameter` children under the method
480+
// node; the parameter name is the last `identifier` inside each
481+
// `method_parameter`. Mirrors `extract_method_params` in
482+
// `crates/codegraph-core/src/extractors/objc.rs`.
453483
const params: SubDeclaration[] = [];
454484
for (let i = 0; i < methodNode.childCount; i++) {
455485
const child = methodNode.child(i);
456-
if (!child || child.type !== 'keyword_selector') continue;
486+
if (!child || child.type !== 'method_parameter') continue;
487+
let nameNode: TreeSitterNode | null = null;
457488
for (let j = 0; j < child.childCount; j++) {
458-
const kw = child.child(j);
459-
if (kw && kw.type === 'keyword_declarator') {
460-
const nameNode = kw.childForFieldName('name');
461-
if (nameNode) {
462-
params.push({
463-
name: nameNode.text,
464-
kind: 'parameter',
465-
line: nameNode.startPosition.row + 1,
466-
});
467-
}
468-
}
489+
const inner = child.child(j);
490+
if (inner && inner.type === 'identifier') nameNode = inner;
491+
}
492+
if (nameNode) {
493+
params.push({
494+
name: nameNode.text,
495+
kind: 'parameter',
496+
line: nameNode.startPosition.row + 1,
497+
});
469498
}
470499
}
471500
return params;

tests/parsers/objc.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,44 @@ describe('Objective-C parser', () => {
9090
expect.objectContaining({ name: 'initWithName:age:', receiver: 'obj' }),
9191
);
9292
});
93+
94+
it('extracts keyword-selector method definitions with parameter names', () => {
95+
// The v3 grammar emits flat `identifier`+`method_parameter` children under
96+
// `method_definition` rather than wrapping them in `keyword_selector`. The
97+
// JS extractor must mirror `build_selector` / `extract_method_params` in
98+
// `crates/codegraph-core/src/extractors/objc.rs` so multi-keyword selectors
99+
// like `setName:age:` appear in `definitions` with their parameter names
100+
// populated. Otherwise these methods are silently dropped on the JS side.
101+
const symbols = parseObjC(`@implementation Foo
102+
- (void)setName:(NSString *)name age:(int)age {
103+
}
104+
@end`);
105+
const method = symbols.definitions.find((d) => d.name === 'Foo.setName:age:');
106+
expect(method).toBeDefined();
107+
expect(method?.kind).toBe('method');
108+
expect(method?.children).toEqual([
109+
expect.objectContaining({ name: 'name', kind: 'parameter' }),
110+
expect.objectContaining({ name: 'age', kind: 'parameter' }),
111+
]);
112+
});
113+
114+
it('extracts @property names nested under struct_declarator', () => {
115+
// The v3 grammar does not expose `name` as a named field on
116+
// `property_declaration`; the identifier nests under
117+
// `struct_declaration > struct_declarator > [pointer_declarator >]
118+
// identifier`. Mirror `extract_property_name` in the Rust extractor so
119+
// pointer and non-pointer properties both surface as class children.
120+
const symbols = parseObjC(`@interface Foo : NSObject
121+
@property (nonatomic, strong) NSString *name;
122+
@property (nonatomic) int age;
123+
@end`);
124+
const foo = symbols.definitions.find((d) => d.name === 'Foo');
125+
expect(foo).toBeDefined();
126+
expect(foo?.children).toEqual(
127+
expect.arrayContaining([
128+
expect.objectContaining({ name: 'name', kind: 'property' }),
129+
expect.objectContaining({ name: 'age', kind: 'property' }),
130+
]),
131+
);
132+
});
93133
});

0 commit comments

Comments
 (0)