feat: add Objective-C ingestion support

This commit is contained in:
xue.yunqiang 2026-05-29 01:44:17 +08:00
parent 97c1f85e87
commit 5808b5bff5
11 changed files with 318 additions and 0 deletions

View file

@ -43,6 +43,7 @@ const EXTENSION_MAP: Record<SupportedLanguages, readonly string[]> = {
[SupportedLanguages.Dart]: ['.dart'],
[SupportedLanguages.Vue]: ['.vue'],
[SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'],
[SupportedLanguages.ObjectiveC]: ['.m', '.mm'],
} satisfies Record<SupportedLanguages, readonly string[]>; // Ensure exhaustiveness
/** Pre-built reverse lookup: extension → language (built once at module load). */
@ -111,6 +112,7 @@ const SYNTAX_MAP: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: 'dart',
[SupportedLanguages.Vue]: 'typescript',
[SupportedLanguages.Cobol]: 'cobol',
[SupportedLanguages.ObjectiveC]: 'objc',
} satisfies Record<SupportedLanguages, string>; // Ensure exhaustiveness
/** Non-code file extensions → Prism-compatible syntax identifiers */

View file

@ -22,4 +22,6 @@ export enum SupportedLanguages {
Vue = 'vue',
/** Standalone regex processor — no tree-sitter, no LanguageProvider. */
Cobol = 'cobol',
/** Objective-C: uses tree-sitter-objc for .m/.mm implementation files. */
ObjectiveC = 'objectivec',
}

View file

@ -39,6 +39,7 @@ export const LanguageClassifications: Readonly<Record<SupportedLanguages, Langua
[SupportedLanguages.Kotlin]: 'production',
[SupportedLanguages.Swift]: 'production',
[SupportedLanguages.Dart]: 'production',
[SupportedLanguages.ObjectiveC]: 'experimental',
[SupportedLanguages.Vue]: 'experimental',
[SupportedLanguages.Cobol]: 'experimental',
};

View file

@ -39,6 +39,7 @@
"tree-sitter-go": "^0.23.0",
"tree-sitter-java": "^0.23.5",
"tree-sitter-javascript": "^0.23.0",
"tree-sitter-objc": "^2.1.0",
"tree-sitter-php": "^0.23.0",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "^0.23.1",
@ -3770,6 +3771,12 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/nan": {
"version": "2.27.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz",
"integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==",
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
@ -4935,6 +4942,16 @@
"license": "MIT",
"optional": true
},
"node_modules/tree-sitter-objc": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tree-sitter-objc/-/tree-sitter-objc-2.1.0.tgz",
"integrity": "sha512-qAeeaZS5sYSB8JYoKpE8GbvtUQFZqDckegU4ntVtpdQPLffjF/6ANgzqbXPWl9YaK2WBZtBFeRF7OPvh7nIErA==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"nan": "^2.17.0"
}
},
"node_modules/tree-sitter-php": {
"version": "0.23.12",
"resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz",

View file

@ -84,6 +84,7 @@
"tree-sitter-go": "^0.23.0",
"tree-sitter-java": "^0.23.5",
"tree-sitter-javascript": "^0.23.0",
"tree-sitter-objc": "^2.1.0",
"tree-sitter-php": "^0.23.0",
"tree-sitter-python": "0.23.4",
"tree-sitter-ruby": "^0.23.1",

View file

@ -25,6 +25,7 @@ import { swiftProvider } from './swift.js';
import { dartProvider } from './dart.js';
import { vueProvider } from './vue.js';
import { cobolProvider } from './cobol.js';
import { objcProvider } from './objc.js';
export const providers = {
[SupportedLanguages.JavaScript]: javascriptProvider,
@ -43,6 +44,7 @@ export const providers = {
[SupportedLanguages.Dart]: dartProvider,
[SupportedLanguages.Vue]: vueProvider,
[SupportedLanguages.Cobol]: cobolProvider,
[SupportedLanguages.ObjectiveC]: objcProvider,
} satisfies Record<SupportedLanguages, LanguageProvider>;
/** Get provider by language enum (always succeeds for SupportedLanguages). */

View file

@ -0,0 +1,106 @@
/**
* Objective-C Language Provider
*
* Assembles all Objective-C-specific ingestion capabilities into a single
* LanguageProvider, following the Strategy pattern used by the pipeline.
*
* Key Objective-C traits:
* - importSemantics: 'wildcard' (ObjC imports entire modules via #import)
* - heritageDefaultEdge: 'EXTENDS' (single class inheritance, multiple protocol adoption)
* - ObjC uses the same type config and export checker as C++ since they share
* similar declaration patterns for functions and global state.
* - message_expression nodes are captured as CALLS (e.g., [self doSomething])
* - class_interface / class_implementation / protocol_declaration captured as definitions.
*/
import { SupportedLanguages } from 'gitnexus-shared';
import { defineLanguage } from '../language-provider.js';
import { typeConfig as cCppConfig } from '../type-extractors/c-cpp.js';
import { cCppExportChecker } from '../export-detection.js';
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
import { createStandardStrategy } from '../import-resolvers/standard.js';
import { OBJ_C_QUERIES } from '../tree-sitter-queries.js';
import { createFieldExtractor } from '../field-extractors/generic.js';
import { cConfig as cFieldConfig } from '../field-extractors/configs/c-cpp.js';
import { createMethodExtractor } from '../method-extractors/generic.js';
import { cMethodConfig } from '../method-extractors/configs/c-cpp.js';
import { createCallExtractor } from '../call-extractors/generic.js';
import { cCallConfig } from '../call-extractors/configs/c-cpp.js';
import { createHeritageExtractor } from '../heritage-extractors/generic.js';
const OBJC_BUILT_INS: ReadonlySet<string> = new Set([
'NSLog',
'NSLogv',
'dispatch_async',
'dispatch_sync',
'dispatch_once',
'dispatch_after',
'dispatch_group_async',
'objc_getClass',
'objc_getMetaClass',
'objc_msgSend',
'objc_msgSendSuper',
'objc_msgSend_stret',
'objc_msgSendSuper_stret',
'sel_registerName',
'protocol_getName',
'class_getName',
'class_getSuperclass',
'object_getClass',
'object_getInstanceSize',
'class_addMethod',
'class_replaceMethod',
'class_getInstanceMethod',
'class_getClassMethod',
'method_exchangeImplementations',
'imp_implementationWithBlock',
'imp_getBlock',
'imp_removeBlock',
'objc_setAssociatedObject',
'objc_getAssociatedObject',
'objc_removeAssociatedObjects',
'class_copyPropertyList',
'class_copyMethodList',
'class_copyIvarList',
'property_getName',
'ivar_getName',
'ivar_getTypeEncoding',
'method_getName',
'method_getTypeEncoding',
'method_getReturnType',
'method_getNumberOfArguments',
'method_getArgumentType',
'class_isMetaClass',
'object_isClass',
'class_respondsToSelector',
'instancesRespondToSelector',
'conformsToProtocol',
'CFRetain',
'CFRelease',
'CFAutorelease',
]);
const stripObjcNullabilityMacros = (sourceText: string): string =>
sourceText
.replace(/NS_ASSUME_NONNULL_BEGIN/g, (m) => ' '.repeat(m.length))
.replace(/NS_ASSUME_NONNULL_END/g, (m) => ' '.repeat(m.length));
export const objcProvider = defineLanguage({
id: SupportedLanguages.ObjectiveC,
extensions: ['.m', '.mm'],
treeSitterQueries: OBJ_C_QUERIES,
preprocessSource: stripObjcNullabilityMacros,
typeConfig: cCppConfig,
exportChecker: cCppExportChecker,
importResolver: createImportResolver({
language: SupportedLanguages.ObjectiveC,
strategies: [createStandardStrategy(SupportedLanguages.C)],
}),
importSemantics: 'wildcard-transitive',
heritageDefaultEdge: 'EXTENDS',
callExtractor: createCallExtractor(cCallConfig),
fieldExtractor: createFieldExtractor(cFieldConfig),
methodExtractor: createMethodExtractor(cMethodConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.ObjectiveC),
builtInNames: OBJC_BUILT_INS,
});

View file

@ -1242,6 +1242,82 @@ export const KOTLIN_QUERIES = `
`;
// Objective-C queries - works with tree-sitter-objc
export const OBJ_C_QUERIES = `
; Class declarations - use anchor to capture only the first identifier (class name)
; . anchor ensures we get the FIRST identifier (class name), not superclass or others
(class_interface
.
(identifier) @name) @definition.class
; Class implementations - @implementation MyClass (methods only, no heritage)
(class_implementation
.
(identifier) @name) @definition.class
; Protocol declarations - anchor to first identifier (protocol name)
(protocol_declaration
.
(identifier) @name) @definition.interface
; Categories - class extension (anonymous category = extension)
(class_interface
.
(identifier) @name
category: (identifier) @category) @definition.class
; Heritage - superclass (e.g., @interface Foo : Bar)
; . matches the FIRST identifier (Foo = class name); then explicit superclass identifier
(class_interface
.
(identifier) @heritage.class
(identifier) @heritage.extends) @heritage
; Heritage - protocol adoptions (e.g., @interface Foo <Proto1, Proto2>)
; . anchors to class name identifier; parameterized_arguments follows as sibling
(class_interface
.
(identifier) @heritage.class
(parameterized_arguments
(type_name
(type_identifier) @heritage.implements))) @heritage.impl
; Method definitions - capture only the first identifier after +/- and method_type
; Use ["+" "-"] to match both class (+) and instance (-) methods
(method_declaration
["+" "-"]
(method_type)
(identifier) @name) @definition.method
; Calls - ObjC message expressions (e.g., [self doSomething], [calc add:5 to:3])
; Use method: field to capture the method selector name(s)
(message_expression
method: (identifier) @call.name) @call
; Calls - C-style function calls (e.g., NSLog(...))
(call_expression
(identifier) @call.name) @call
; Property declarations (e.g., @property (nonatomic, strong) NSString *name)
(property_declaration
(struct_declaration
(_)
(struct_declarator
(pointer_declarator
declarator: (identifier) @name)))) @definition.property
; Instance variable declarations inside @interface { } block
(instance_variable
(struct_declaration
(_)
(struct_declarator
(pointer_declarator
declarator: (identifier) @name)))) @definition.property
; Imports - #import "path" or #import <path> (preproc_include is used by tree-sitter-objc for #import)
(preproc_include path: (_) @import.source) @import
`;
// Swift queries - works with tree-sitter-swift
export const SWIFT_QUERIES = `
; Classes
@ -1543,4 +1619,5 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Dart]: DART_QUERIES,
[SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue <script> blocks are parsed as TypeScript
[SupportedLanguages.Cobol]: '', // Standalone regex processor — no tree-sitter queries
[SupportedLanguages.ObjectiveC]: OBJ_C_QUERIES,
};

View file

@ -111,6 +111,12 @@ const SOURCES: Record<string, GrammarSource> = {
unavailableNote:
'Vue parsing piggybacks on `tree-sitter-typescript`. Check the install and native binding.',
},
[SupportedLanguages.ObjectiveC]: {
load: () => _require('tree-sitter-objc'),
unavailableNote:
'Objective-C parsing requires `tree-sitter-objc`. ' +
'Check that the package and its native binding installed cleanly (`npm ci`).',
},
// tree-sitter-c is a required dependency, but its native binding has
// historically been ABI-incompatible with the bundled tree-sitter@0.21.1

View file

@ -0,0 +1,34 @@
#import <Foundation/Foundation.h>
@interface Calculator : NSObject
@property (nonatomic, strong) NSString *name;
- (void)reset;
- (NSInteger)add:(NSInteger)a to:(NSInteger)b;
- (NSInteger)multiply:(NSInteger)a with:(NSInteger)b;
+ (instancetype)sharedCalculator;
@end
@implementation Calculator
- (void)reset {
NSLog(@"reset");
}
- (NSInteger)add:(NSInteger)a to:(NSInteger)b {
return a + b;
}
- (NSInteger)multiply:(NSInteger)a with:(NSInteger)b {
return a * b;
}
+ (instancetype)sharedCalculator {
Calculator *calc = [Calculator alloc];
return [calc init];
}
@end
void runCalculatorSample(void) {
Calculator *calc = [Calculator sharedCalculator];
[calc reset];
[calc add:5 to:3];
[calc multiply:2 with:4];
}

View file

@ -378,6 +378,76 @@ describe('Tree-sitter multi-language parsing', () => {
});
});
describe('Objective-C', () => {
it('parses class, method declarations if tree-sitter-objc is available', async () => {
try {
await loadLanguage(SupportedLanguages.ObjectiveC, 'simple.m');
} catch {
// tree-sitter-objc not installed — skip
return;
}
const content = readFixture('simple.m');
const provider = getProvider(SupportedLanguages.ObjectiveC);
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
const defs = extractDefinitions(matches);
expect(defs.length).toBeGreaterThan(0);
const names = defs.map((d) => d.name);
// Class names
expect(names).toContain('Calculator');
// Method names (keyword selectors captured as separate identifiers: add, to, multiply, with)
expect(names).toContain('reset');
expect(names).toContain('add');
expect(names).toContain('to');
expect(names).toContain('multiply');
expect(names).toContain('with');
expect(names).toContain('sharedCalculator');
});
it('captures class interface and implementation', async () => {
try {
await loadLanguage(SupportedLanguages.ObjectiveC, 'simple.m');
} catch {
return;
}
const content = readFixture('simple.m');
const provider = getProvider(SupportedLanguages.ObjectiveC);
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
const defs = extractDefinitions(matches);
const defTypes = defs.map((d) => d.type);
expect(defTypes).toContain('definition.class');
});
it('captures ObjC message expressions as calls', async () => {
try {
await loadLanguage(SupportedLanguages.ObjectiveC, 'simple.m');
} catch {
return;
}
const content = readFixture('simple.m');
const provider = getProvider(SupportedLanguages.ObjectiveC);
const { matches } = parseAndQuery(parser, content, provider.treeSitterQueries);
const calls: string[] = [];
for (const match of matches) {
for (const capture of match.captures) {
if (capture.name === 'call.name') calls.push(capture.node.text);
}
}
// ObjC message calls capture method names (selectors), not receivers
// [calc reset] -> reset, [calc add:5 to:3] -> add, to, [Calculator sharedCalculator] -> sharedCalculator
// C function calls like NSLog() are also captured
expect(calls).toContain('reset');
expect(calls).toContain('sharedCalculator');
expect(calls).toContain('NSLog');
expect(calls).toContain('alloc');
expect(calls).toContain('init');
});
});
describe('Swift', () => {
it('parses class, struct, protocol, and function if tree-sitter-swift is available', async () => {
try {