feat(extraction): add Objective-C language support (#165)
Adds tree-sitter-objc extractor for `.m`/`.mm` files and `.h` files that content-sniff as Objective-C (`@interface`/`@implementation`/`@protocol`/`@synthesize`). Extraction covers: - `@interface` / `@implementation` (deduplicated into a single class node) - `@protocol` (as `protocol` nodes via new `interfaceKind` config) - Methods with full multi-part selectors (`doThing:with:`, not just `doThing`), including `+`/`-` static distinction - `@property` declarations - Inheritance (`extends`) and protocol conformance (`implements`) - C-style `function_definition` and `#import` (both `<system>` and `"local"` forms) - Call edges from both `call_expression` and `message_expression`, with `self`/`super` skipped on qualified callee names Two new generic hooks on `LanguageExtractor` (`resolveName`, `extractPropertyName`) handle the cases where the default name walk doesn't fit; usable by future languages with similar shape. Import resolver tries `.h`, `.m`, `.mm` for `objc` imports. Validated on AFNetworking (84 files, 100% file coverage), RestKit (282 files, 99.6%), and Texture (926 files, 100%, heavy `.mm` content) — multi-keyword selectors preserved up to 7 parts, no parse failures on ObjC++. Known limitations (disclosed in README): - Categories produce duplicate class nodes (one per category file) - Chained/nested message sends record only the innermost method - `[Class alloc]` patterns don't emit `instantiates` edges - `@protocol Foo <Bar>` refinement lists not yet wired to `implements` - Heavy C++ in `.mm` files may parse incompletely under the ObjC grammar
This commit is contained in:
@@ -93,6 +93,14 @@ describe('Language Detection', () => {
|
||||
expect(detectLanguage('main.dart')).toBe('dart');
|
||||
});
|
||||
|
||||
it('should detect Objective-C files', () => {
|
||||
expect(detectLanguage('AppDelegate.m')).toBe('objc');
|
||||
expect(detectLanguage('ViewController.mm')).toBe('objc');
|
||||
const objcHeader = '@interface Foo : NSObject\n@end\n';
|
||||
expect(detectLanguage('Foo.h', objcHeader)).toBe('objc');
|
||||
expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c');
|
||||
});
|
||||
|
||||
it('should return unknown for unsupported extensions', () => {
|
||||
expect(detectLanguage('styles.css')).toBe('unknown');
|
||||
expect(detectLanguage('data.json')).toBe('unknown');
|
||||
@@ -3900,3 +3908,103 @@ local count = 0
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// Objective-C
|
||||
// =============================================================================
|
||||
|
||||
describe('Objective-C Extraction', () => {
|
||||
const sample = `
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MyClass.h"
|
||||
|
||||
@interface MyClass : NSObject <NSCopying>
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
- (void)greet;
|
||||
- (void)doThing:(id)x with:(id)y;
|
||||
+ (instancetype)shared;
|
||||
@end
|
||||
|
||||
@implementation MyClass
|
||||
|
||||
- (void)greet {
|
||||
NSLog(@"Hello");
|
||||
[self doWork];
|
||||
}
|
||||
|
||||
- (void)doThing:(id)x with:(id)y {
|
||||
[self notify:x];
|
||||
}
|
||||
|
||||
+ (instancetype)shared {
|
||||
return [[MyClass alloc] init];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void helperFunction(int count) {
|
||||
MyClass *obj = [MyClass shared];
|
||||
[obj greet];
|
||||
}
|
||||
`;
|
||||
|
||||
it('should extract classes, methods, functions, and imports', () => {
|
||||
const result = extractFromSource('App.m', sample);
|
||||
|
||||
const classes = result.nodes.filter((n) => n.kind === 'class');
|
||||
expect(classes.filter((c) => c.name === 'MyClass')).toHaveLength(1);
|
||||
|
||||
const methods = result.nodes.filter((n) => n.kind === 'method');
|
||||
expect(methods.map((m) => m.name).sort()).toEqual(['doThing:with:', 'greet', 'shared']);
|
||||
|
||||
const shared = methods.find((m) => m.name === 'shared');
|
||||
expect(shared?.isStatic).toBe(true);
|
||||
|
||||
const properties = result.nodes.filter((n) => n.kind === 'property');
|
||||
expect(properties.some((p) => p.name === 'name')).toBe(true);
|
||||
|
||||
const functions = result.nodes.filter((n) => n.kind === 'function');
|
||||
expect(functions.some((f) => f.name === 'helperFunction')).toBe(true);
|
||||
|
||||
const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name);
|
||||
expect(imports).toContain('Foundation/Foundation.h');
|
||||
expect(imports).toContain('MyClass.h');
|
||||
});
|
||||
|
||||
it('should record inheritance and protocol conformance', () => {
|
||||
const result = extractFromSource('App.m', sample);
|
||||
const extendsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends');
|
||||
const implementsRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'implements');
|
||||
expect(extendsRefs.map((r) => r.referenceName)).toContain('NSObject');
|
||||
expect(implementsRefs.map((r) => r.referenceName)).toContain('NSCopying');
|
||||
});
|
||||
|
||||
it('should record message sends and C calls', () => {
|
||||
const result = extractFromSource('App.m', sample);
|
||||
const calls = result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'calls')
|
||||
.map((r) => r.referenceName);
|
||||
expect(calls).toEqual(expect.arrayContaining(['NSLog', 'doWork', 'MyClass.shared', 'obj.greet']));
|
||||
});
|
||||
|
||||
it('should not classify pure C headers with @end in comments as objc', () => {
|
||||
const cHeader = '/* @end of file */\n#ifndef STDIO_H\nvoid printf(const char *);\n#endif\n';
|
||||
expect(detectLanguage('stdio.h', cHeader)).toBe('c');
|
||||
});
|
||||
|
||||
it('should extract protocol declarations', () => {
|
||||
const code = `
|
||||
@protocol DataSource <NSObject>
|
||||
- (NSInteger)numberOfItems;
|
||||
@end
|
||||
`;
|
||||
const result = extractFromSource('DataSource.h', code);
|
||||
const protocol = result.nodes.find((n) => n.kind === 'protocol' && n.name === 'DataSource');
|
||||
expect(protocol).toBeDefined();
|
||||
});
|
||||
|
||||
it('should report Objective-C as supported', () => {
|
||||
expect(isLanguageSupported('objc')).toBe(true);
|
||||
expect(getSupportedLanguages()).toContain('objc');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user