mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
feat: add Objective-C ingestion support
This commit is contained in:
parent
97c1f85e87
commit
5808b5bff5
11 changed files with 318 additions and 0 deletions
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
17
gitnexus/package-lock.json
generated
17
gitnexus/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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). */
|
||||
|
|
|
|||
106
gitnexus/src/core/ingestion/languages/objc.ts
Normal file
106
gitnexus/src/core/ingestion/languages/objc.ts
Normal 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,
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
34
gitnexus/test/fixtures/sample-code/simple.m
vendored
Normal file
34
gitnexus/test/fixtures/sample-code/simple.m
vendored
Normal 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];
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue