Skip to content

Commit dc1c8de

Browse files
committed
fix(extractors): handle parameterized_arguments for ObjC protocol adoption (#1106)
The JS extractor was looking up 'protocol_qualifiers' which doesn't exist in tree-sitter-objc v3 (the grammar wraps adopted protocols in 'parameterized_arguments' instead). As a result every '@interface Foo : NSObject <Bar, Baz>' silently dropped its 'implements' relations on the JS side, while the Rust extractor correctly extracted them. Mirror handle_class_interface in crates/codegraph-core/src/extractors/objc.rs (parameterized_arguments + the type_name > type_identifier nesting) and add a regression test.
1 parent 66296d4 commit dc1c8de

2 files changed

Lines changed: 34 additions & 4 deletions

File tree

src/extractors/objc.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,24 @@ function handleClassInterface(node: TreeSitterNode, ctx: ExtractorOutput): void
114114
ctx.classes.push({ name, extends: superclass.text, line: node.startPosition.row + 1 });
115115
}
116116

117-
// Protocols
118-
const protocols = findChild(node, 'protocol_qualifiers');
117+
// Adopted protocols. tree-sitter-objc v3 wraps the adopted-protocol list in
118+
// `parameterized_arguments` (not `protocol_qualifiers`, which was the v2
119+
// grammar shape). Each child is wrapped in `type_name > type_identifier`;
120+
// fall back to a bare `identifier`/`type_identifier` for older grammars.
121+
const protocols = findChild(node, 'parameterized_arguments');
119122
if (protocols) {
120123
for (let i = 0; i < protocols.childCount; i++) {
121124
const proto = protocols.child(i);
122-
if (proto && proto.type === 'identifier') {
123-
ctx.classes.push({ name, implements: proto.text, line: node.startPosition.row + 1 });
125+
if (!proto) continue;
126+
let protoName: string | null = null;
127+
if (proto.type === 'type_name') {
128+
const inner = findChild(proto, 'type_identifier') || findChild(proto, 'identifier');
129+
if (inner) protoName = inner.text;
130+
} else if (proto.type === 'identifier' || proto.type === 'type_identifier') {
131+
protoName = proto.text;
132+
}
133+
if (protoName) {
134+
ctx.classes.push({ name, implements: protoName, line: node.startPosition.row + 1 });
124135
}
125136
}
126137
}

tests/parsers/objc.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,25 @@ describe('Objective-C parser', () => {
111111
]);
112112
});
113113

114+
it('extracts adopted protocols as implements relations', () => {
115+
// tree-sitter-objc v3 wraps the adopted-protocol list in
116+
// `parameterized_arguments` (not the legacy `protocol_qualifiers`). The
117+
// JS extractor must mirror `handle_class_interface` in
118+
// `crates/codegraph-core/src/extractors/objc.rs`, otherwise every
119+
// `implements` relation for an ObjC class interface is silently dropped.
120+
const symbols = parseObjC(`@interface Foo : NSObject <Bar, Baz>
121+
@end`);
122+
expect(symbols.classes).toContainEqual(
123+
expect.objectContaining({ name: 'Foo', extends: 'NSObject' }),
124+
);
125+
expect(symbols.classes).toContainEqual(
126+
expect.objectContaining({ name: 'Foo', implements: 'Bar' }),
127+
);
128+
expect(symbols.classes).toContainEqual(
129+
expect.objectContaining({ name: 'Foo', implements: 'Baz' }),
130+
);
131+
});
132+
114133
it('extracts @property names nested under struct_declarator', () => {
115134
// The v3 grammar does not expose `name` as a named field on
116135
// `property_declaration`; the identifier nests under

0 commit comments

Comments
 (0)