diff --git a/gitnexus-shared/src/language-detection.ts b/gitnexus-shared/src/language-detection.ts index d31f9d58d..3169261bb 100644 --- a/gitnexus-shared/src/language-detection.ts +++ b/gitnexus-shared/src/language-detection.ts @@ -43,6 +43,7 @@ const EXTENSION_MAP: Record = { [SupportedLanguages.Dart]: ['.dart'], [SupportedLanguages.Vue]: ['.vue'], [SupportedLanguages.Cobol]: ['.cbl', '.cob', '.cpy', '.cobol'], + [SupportedLanguages.ObjectiveC]: ['.m', '.mm'], } satisfies Record; // Ensure exhaustiveness /** Pre-built reverse lookup: extension → language (built once at module load). */ @@ -101,6 +102,7 @@ const SYNTAX_MAP: Record = { [SupportedLanguages.Dart]: 'dart', [SupportedLanguages.Vue]: 'typescript', [SupportedLanguages.Cobol]: 'cobol', + [SupportedLanguages.ObjectiveC]: 'objc', } satisfies Record; // Ensure exhaustiveness /** Non-code file extensions → Prism-compatible syntax identifiers */ diff --git a/gitnexus-shared/src/languages.ts b/gitnexus-shared/src/languages.ts index 29edb8d1e..268e1b0ea 100644 --- a/gitnexus-shared/src/languages.ts +++ b/gitnexus-shared/src/languages.ts @@ -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', } diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index 38659fd74..eb4e696c8 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -33,6 +33,7 @@ "tree-sitter-go": "^0.23.0", "tree-sitter-java": "^0.23.5", "tree-sitter-javascript": "^0.23.0", + "tree-sitter-objc": "^3.0.2", "tree-sitter-php": "^0.23.0", "tree-sitter-python": "0.23.4", "tree-sitter-ruby": "^0.23.1", @@ -5258,6 +5259,54 @@ "license": "MIT", "optional": true }, + "node_modules/tree-sitter-objc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tree-sitter-objc/-/tree-sitter-objc-3.0.2.tgz", + "integrity": "sha512-Hs0ohmx1u5M+0K7efoW+dv/corhBsfjftfIYLtp7dSGeJ+Zj4c33tDIboBYLs6qijRlz6wtHFxa0YX+FibLulA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4", + "tree-sitter-c": "^0.23.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-objc/node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/tree-sitter-objc/node_modules/tree-sitter-c": { + "version": "0.23.6", + "resolved": "https://registry.npmjs.org/tree-sitter-c/-/tree-sitter-c-0.23.6.tgz", + "integrity": "sha512-0dxXKznVyUA0s6PjNolJNs2yF87O5aL538A/eR6njA5oqX3C3vH4vnx3QdOKwuUdpKEcFdHuiDpRKLLCA/tjvQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } + } + }, "node_modules/tree-sitter-php": { "version": "0.23.12", "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index e752bf9d1..2cfa32a1a 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -75,6 +75,7 @@ "tree-sitter-go": "^0.23.0", "tree-sitter-java": "^0.23.5", "tree-sitter-javascript": "^0.23.0", + "tree-sitter-objc": "^3.0.2", "tree-sitter-php": "^0.23.0", "tree-sitter-python": "0.23.4", "tree-sitter-ruby": "^0.23.1", @@ -88,7 +89,6 @@ "tree-sitter-swift": "^0.6.0" }, "devDependencies": { - "gitnexus-shared": "file:../gitnexus-shared", "@types/cli-progress": "^3.11.6", "@types/cors": "^2.8.17", "@types/express": "^4.17.21", @@ -96,6 +96,7 @@ "@types/node": "^20.0.0", "@types/uuid": "^10.0.0", "@vitest/coverage-v8": "^4.0.18", + "gitnexus-shared": "file:../gitnexus-shared", "tsx": "^4.0.0", "typescript": "^5.4.5", "vitest": "^4.0.18" diff --git a/gitnexus/src/core/ingestion/entry-point-scoring.ts b/gitnexus/src/core/ingestion/entry-point-scoring.ts index f8d85ae48..10a501484 100644 --- a/gitnexus/src/core/ingestion/entry-point-scoring.ts +++ b/gitnexus/src/core/ingestion/entry-point-scoring.ts @@ -228,6 +228,7 @@ export const ENTRY_POINT_PATTERNS = { ], [SupportedLanguages.Vue]: [], // Vue uses TypeScript queries — entry points handled via TS patterns [SupportedLanguages.Cobol]: [], // Standalone regex processor — no tree-sitter entry points + [SupportedLanguages.ObjectiveC]: [], // ObjC has no special entry point patterns } satisfies Record; /** Pre-computed merged patterns (universal + language-specific) to avoid per-call array allocation. */ diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 9ea43800c..f175dee13 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -893,6 +893,7 @@ export const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE = { ], [SupportedLanguages.Vue]: [], // Vue uses TypeScript AST framework detection [SupportedLanguages.Cobol]: [], // Standalone regex processor — no AST framework patterns + [SupportedLanguages.ObjectiveC]: [], // ObjC has no dedicated AST framework patterns } satisfies Record; /** Pre-lowercased patterns for O(1) pattern matching at runtime */ diff --git a/gitnexus/src/core/ingestion/languages/index.ts b/gitnexus/src/core/ingestion/languages/index.ts index 041af775d..17b146392 100644 --- a/gitnexus/src/core/ingestion/languages/index.ts +++ b/gitnexus/src/core/ingestion/languages/index.ts @@ -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; /** Get provider by language enum (always succeeds for SupportedLanguages). */ diff --git a/gitnexus/src/core/ingestion/languages/objc.ts b/gitnexus/src/core/ingestion/languages/objc.ts new file mode 100644 index 000000000..156fb8e91 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/objc.ts @@ -0,0 +1,91 @@ +/** + * 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 { resolveCImport } 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'; + +const OBJC_BUILT_INS: ReadonlySet = 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', +]); + +export const objcProvider = defineLanguage({ + id: SupportedLanguages.ObjectiveC, + extensions: ['.m', '.mm'], + treeSitterQueries: OBJ_C_QUERIES, + typeConfig: cCppConfig, + exportChecker: cCppExportChecker, + importResolver: resolveCImport, + importSemantics: 'wildcard', + heritageDefaultEdge: 'EXTENDS', + fieldExtractor: createFieldExtractor(cFieldConfig), + methodExtractor: createMethodExtractor(cMethodConfig), + builtInNames: OBJC_BUILT_INS, +}); diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index d9913e671..7764894fb 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -318,6 +318,13 @@ const processParsingSequential = async ( isVueSetup = extracted.isSetup; } + // ObjC: strip NS_ASSUME_NONNULL_* macros that break class_interface/protocol parsing + if (language === SupportedLanguages.ObjectiveC) { + parseContent = parseContent + .replace(/NS_ASSUME_NONNULL_BEGIN\s*/g, '') + .replace(/NS_ASSUME_NONNULL_END\s*/g, ''); + } + try { await loadLanguage(language, file.path); } catch { diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index a6c8d74b4..42f5654b5 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -950,6 +950,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 ) +; . 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 (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 @@ -1193,4 +1269,5 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Dart]: DART_QUERIES, [SupportedLanguages.Vue]: TYPESCRIPT_QUERIES, // Vue