mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
fix(objective-c): normalize bare macro markers
This commit is contained in:
parent
c80193e1a3
commit
60980b7ced
9 changed files with 260 additions and 4 deletions
|
|
@ -96,12 +96,13 @@ Implemented in the branch:
|
|||
- Vendored `tree-sitter-objc` grammar, registered through the existing Tree-sitter loader.
|
||||
- `.m` and `.mm` language mapping plus content-based `.h` classification so plain C/C++ headers are not unconditionally claimed.
|
||||
- LanguageProvider extraction for classes, protocols, categories, extensions, methods, properties, ivars, C functions, imports, unresolved message evidence, stable Objective-C qualified names, and provider/grammar metadata.
|
||||
- Length-preserving preprocessing of bare, file-scope all-caps macro markers before Tree-sitter parsing. This recovers declarations after wrappers such as `RCT_EXTERN_C_BEGIN` / `RCT_EXTERN_C_END` without expanding macros or adding framework-specific rules.
|
||||
- ScopeResolver edges for imports, inheritance, protocol conformance, category host membership, implementation evidence, and conservative static message sends.
|
||||
- Persisted query/context support for Objective-C class and method nodes, including implementation evidence via `DECLARES`.
|
||||
- Regression tests for grammar loading, `.h` classification, stable identities, conservative calls, metadata feature mismatch, persisted query/context behavior, and incremental-vs-force parity for Objective-C fixture edits.
|
||||
|
||||
Known limits of this MVP:
|
||||
|
||||
- The first version does not perform full Objective-C runtime dispatch, swizzling, dynamic selector construction, macro expansion, or `id` flow inference.
|
||||
- The first version does not perform full Objective-C runtime dispatch, swizzling, dynamic selector construction, macro expansion, or `id` flow inference. Bare file-scope marker macros are elided only to preserve parser recovery; their expansion semantics are not interpreted.
|
||||
- Protocol receiver handling records the protocol method and candidate implementation evidence, but candidate implementations are not emitted as certain call edges.
|
||||
- Objective-C++ `.mm` files are parsed with the Objective-C grammar path for this MVP; deep C++ semantic extraction inside Objective-C++ bodies remains outside this provider.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { ImportResolverFn } from '../import-resolvers/types.js';
|
|||
import { getLanguageGrammar } from '../../tree-sitter/parser-loader.js';
|
||||
import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
|
||||
import { assertCloneable } from '../workers/clone-safety.js';
|
||||
import { preprocessObjectiveCMacroMarkers } from './objective-c/macro-marker-preprocess.js';
|
||||
import {
|
||||
buildObjectiveCSemanticGraph,
|
||||
buildObjectiveCScopeCaptures,
|
||||
|
|
@ -99,7 +100,13 @@ export function classifyObjectiveCFileContent(filePath: string, sourceText: stri
|
|||
function parseObjectiveCSource(sourceText: string): Parser.Tree {
|
||||
const parser = new Parser();
|
||||
parser.setLanguage(getLanguageGrammar(SupportedLanguages.ObjectiveC));
|
||||
return parseSourceSafe(parser, sourceText, undefined, undefined, 'Objective-C source');
|
||||
return parseSourceSafe(
|
||||
parser,
|
||||
preprocessObjectiveCMacroMarkers(sourceText),
|
||||
undefined,
|
||||
undefined,
|
||||
'Objective-C source',
|
||||
);
|
||||
}
|
||||
|
||||
function treeFromCachedOrSource(cachedTree: unknown, sourceText: string): Parser.Tree {
|
||||
|
|
@ -157,6 +164,7 @@ export const objectiveCProvider = defineLanguage({
|
|||
importResolver: noImportResolution,
|
||||
classifyFileContent: classifyObjectiveCFileContent,
|
||||
shouldClassifyFileContent: isHeaderPath,
|
||||
preprocessSource: preprocessObjectiveCMacroMarkers,
|
||||
importsExecuteWhereWritten: false,
|
||||
|
||||
emitScopeCaptures: (sourceText, filePath, cachedTree): readonly CaptureMatch[] => {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
} from '../../language-provider.js';
|
||||
import { nodeToCapture, walkNamedTree, type SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
|
||||
export const OBJECTIVE_C_PROVIDER_VERSION = '0.1.0';
|
||||
export const OBJECTIVE_C_PROVIDER_VERSION = '0.1.1';
|
||||
export const OBJECTIVE_C_GRAMMAR_PACKAGE = 'tree-sitter-objc';
|
||||
export const OBJECTIVE_C_GRAMMAR_VERSION = '3.0.2';
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
/**
|
||||
* Normalize bare file-scope Objective-C macro markers before parsing.
|
||||
*
|
||||
* Headers commonly use macro pairs such as `RCT_EXTERN_C_BEGIN` and
|
||||
* `RCT_EXTERN_C_END` around C declarations. tree-sitter-objc does not expand
|
||||
* those macros; a bare invocation can put the parser into error recovery and
|
||||
* hide every Objective-C declaration that follows it. These markers do not
|
||||
* contribute syntax on their own, so we replace only the narrow, generic form
|
||||
* with spaces before parsing.
|
||||
*
|
||||
* This is deliberately not macro expansion or a framework-specific allowlist:
|
||||
* a candidate must be a whole, all-caps identifier at file scope. Function-like
|
||||
* macros, directives, statements, strings, and comments remain untouched.
|
||||
* Replacement preserves UTF-16 length and line endings exactly. Candidates are
|
||||
* ASCII-only, so their byte offsets are preserved as well.
|
||||
*/
|
||||
|
||||
interface ScanState {
|
||||
inBlockComment: boolean;
|
||||
inPreprocessorDirective: boolean;
|
||||
quote: '"' | "'" | undefined;
|
||||
braceDepth: number;
|
||||
}
|
||||
|
||||
function isAsciiHorizontalWhitespace(code: number): boolean {
|
||||
return code === 0x20 || code === 0x09;
|
||||
}
|
||||
|
||||
function isBareMarkerIdentifier(line: string): boolean {
|
||||
let index = 0;
|
||||
while (index < line.length && isAsciiHorizontalWhitespace(line.charCodeAt(index))) index++;
|
||||
|
||||
const identifierStart = index;
|
||||
let hasUppercaseLetter = false;
|
||||
while (index < line.length) {
|
||||
const code = line.charCodeAt(index);
|
||||
if (code >= 0x41 && code <= 0x5a) {
|
||||
hasUppercaseLetter = true;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if ((code >= 0x30 && code <= 0x39) || code === 0x5f) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (index === identifierStart || !hasUppercaseLetter) return false;
|
||||
|
||||
while (index < line.length && isAsciiHorizontalWhitespace(line.charCodeAt(index))) index++;
|
||||
return index === line.length;
|
||||
}
|
||||
|
||||
function hasEscapedLineEnding(line: string): boolean {
|
||||
let trailingBackslashes = 0;
|
||||
for (let index = line.length - 1; index >= 0 && line.charCodeAt(index) === 0x5c; index--) {
|
||||
trailingBackslashes++;
|
||||
}
|
||||
return trailingBackslashes % 2 === 1;
|
||||
}
|
||||
|
||||
function startsPreprocessorDirective(line: string): boolean {
|
||||
let index = 0;
|
||||
while (index < line.length && isAsciiHorizontalWhitespace(line.charCodeAt(index))) index++;
|
||||
return line.charCodeAt(index) === 0x23;
|
||||
}
|
||||
|
||||
function scanLine(line: string, state: ScanState): void {
|
||||
if (state.inPreprocessorDirective) {
|
||||
state.inPreprocessorDirective = hasEscapedLineEnding(line);
|
||||
return;
|
||||
}
|
||||
if (startsPreprocessorDirective(line)) {
|
||||
state.inPreprocessorDirective = hasEscapedLineEnding(line);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index = 0; index < line.length; index++) {
|
||||
const code = line.charCodeAt(index);
|
||||
const next = line.charCodeAt(index + 1);
|
||||
|
||||
if (state.inBlockComment) {
|
||||
if (code === 0x2a && next === 0x2f) {
|
||||
state.inBlockComment = false;
|
||||
index++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.quote !== undefined) {
|
||||
if (code === 0x5c) {
|
||||
index++;
|
||||
} else if (line[index] === state.quote) {
|
||||
state.quote = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (code === 0x2f && next === 0x2f) return;
|
||||
if (code === 0x2f && next === 0x2a) {
|
||||
state.inBlockComment = true;
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (code === 0x22 || code === 0x27) {
|
||||
state.quote = line[index] as '"' | "'";
|
||||
continue;
|
||||
}
|
||||
if (code === 0x7b) state.braceDepth++;
|
||||
else if (code === 0x7d) state.braceDepth = Math.max(0, state.braceDepth - 1);
|
||||
}
|
||||
|
||||
if (state.quote !== undefined && !hasEscapedLineEnding(line)) state.quote = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elide bare, file-scope macro markers while preserving source positions.
|
||||
*
|
||||
* `_filePath` is accepted for the LanguageProvider hook signature. The
|
||||
* transform is based only on source syntax and deliberately has no framework
|
||||
* or repository-specific configuration.
|
||||
*/
|
||||
export function preprocessObjectiveCMacroMarkers(source: string, _filePath?: string): string {
|
||||
const state: ScanState = {
|
||||
inBlockComment: false,
|
||||
inPreprocessorDirective: false,
|
||||
quote: undefined,
|
||||
braceDepth: 0,
|
||||
};
|
||||
const segments = source.split(/(\r\n|\n|\r)/);
|
||||
let changed = false;
|
||||
|
||||
for (let index = 0; index < segments.length; index += 2) {
|
||||
const line = segments[index];
|
||||
if (
|
||||
!state.inBlockComment &&
|
||||
!state.inPreprocessorDirective &&
|
||||
state.quote === undefined &&
|
||||
state.braceDepth === 0 &&
|
||||
isBareMarkerIdentifier(line)
|
||||
) {
|
||||
segments[index] = ' '.repeat(line.length);
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
scanLine(line, state);
|
||||
}
|
||||
|
||||
return changed ? segments.join('') : source;
|
||||
}
|
||||
|
|
@ -735,7 +735,10 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|||
// v93: Zig call captures inside a comptime-false branch carry
|
||||
// `@reference.static-gated` (feat/zig-static-gated-edges); the site gains
|
||||
// `staticGated` and the CALLS edge a BOOLEAN column.
|
||||
const SCHEMA_BUMP = 93;
|
||||
// v94: Objective-C now elides bare, file-scope macro markers before parsing.
|
||||
// A warm v93 cache can retain error-recovered trees and provider facts that
|
||||
// omit Objective-C declarations following markers such as RCT_EXTERN_C_END.
|
||||
const SCHEMA_BUMP = 94;
|
||||
const GITNEXUS_PKG_VERSION = (() => {
|
||||
try {
|
||||
// package.json sits at gitnexus/package.json — two levels up from
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#define RCT_EXTERN_C_BEGIN
|
||||
#define RCT_EXTERN_C_END
|
||||
|
||||
RCT_EXTERN_C_BEGIN
|
||||
typedef struct SYModuleMethodInfo {
|
||||
const char *const name;
|
||||
} SYModuleMethodInfo;
|
||||
RCT_EXTERN_C_END
|
||||
|
||||
int SYModuleSupportAdd(int a, int b);
|
||||
|
||||
@protocol SYModuleRunnable <NSObject>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { GraphNode, RelationshipType } from 'gitnexus-shared';
|
|||
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
||||
import { runFullAnalysis } from '../../src/core/run-analyze.js';
|
||||
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
|
||||
import { classifyObjectiveCFileContent } from '../../src/core/ingestion/languages/objective-c.js';
|
||||
import type { PipelineResult } from '../../src/types/pipeline.js';
|
||||
|
||||
const FIXTURE_DIR = path.resolve(
|
||||
|
|
@ -211,6 +212,25 @@ describe('Objective-C provider integration', () => {
|
|||
});
|
||||
}
|
||||
|
||||
it('classifies a macro-wrapped Objective-C header by declarations after the marker', () => {
|
||||
expect(
|
||||
classifyObjectiveCFileContent(
|
||||
'RCTBridgeModule.h',
|
||||
[
|
||||
'RCT_EXTERN_C_BEGIN',
|
||||
'typedef struct RCTMethodInfo {',
|
||||
' const char *const jsName;',
|
||||
'} RCTMethodInfo;',
|
||||
'RCT_EXTERN_C_END',
|
||||
'@protocol RCTBridgeModule <NSObject>',
|
||||
'- (void)run;',
|
||||
'@end',
|
||||
'',
|
||||
].join('\n'),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('indexes Objective-C semantic nodes beyond File nodes', () => {
|
||||
expectNode('objc:protocol:SYModuleRunnable', 'Protocol');
|
||||
expectNode('objc:class:SYBaseCaller', 'Class');
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { preprocessObjectiveCMacroMarkers } from '../../src/core/ingestion/languages/objective-c/macro-marker-preprocess.js';
|
||||
|
||||
describe('preprocessObjectiveCMacroMarkers', () => {
|
||||
it('elides bare file-scope markers and preserves positions', () => {
|
||||
const source = [
|
||||
'#define RCT_EXTERN_C_BEGIN',
|
||||
'#define RCT_EXTERN_C_END',
|
||||
'RCT_EXTERN_C_BEGIN',
|
||||
'typedef struct RCTMethodInfo {',
|
||||
' const char *const jsName;',
|
||||
'} RCTMethodInfo;',
|
||||
'RCT_EXTERN_C_END',
|
||||
'@protocol RCTBridgeModule <NSObject>',
|
||||
'- (void)run;',
|
||||
'@end',
|
||||
'',
|
||||
].join('\r\n');
|
||||
|
||||
const normalized = preprocessObjectiveCMacroMarkers(source, 'RCTBridgeModule.h');
|
||||
|
||||
expect(normalized).toHaveLength(source.length);
|
||||
expect(normalized.split('\r\n')).toHaveLength(source.split('\r\n').length);
|
||||
expect(normalized).toContain('#define RCT_EXTERN_C_BEGIN');
|
||||
expect(normalized).toContain('@protocol RCTBridgeModule <NSObject>');
|
||||
expect(normalized).toContain(' '.repeat('RCT_EXTERN_C_BEGIN'.length));
|
||||
expect(normalized).toContain(' '.repeat('RCT_EXTERN_C_END'.length));
|
||||
expect(preprocessObjectiveCMacroMarkers(normalized, 'RCTBridgeModule.h')).toBe(normalized);
|
||||
});
|
||||
|
||||
it('leaves non-marker syntax, strings, and comments untouched', () => {
|
||||
const source = [
|
||||
'void marker(void) {',
|
||||
' RCT_EXTERN_C_BEGIN',
|
||||
'}',
|
||||
'RCT_EXTERN_C_END()',
|
||||
'RCT_EXTERN_C_END;',
|
||||
'#define RCT_EXTERN_C_END',
|
||||
'#define RCT_MARKER_SEQUENCE \\',
|
||||
'RCT_EXTERN_C_END',
|
||||
'const char *value = "RCT_EXTERN_C_END";',
|
||||
'// RCT_EXTERN_C_END',
|
||||
'/*',
|
||||
'RCT_EXTERN_C_END',
|
||||
'*/',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
expect(preprocessObjectiveCMacroMarkers(source, 'Example.m')).toBe(source);
|
||||
});
|
||||
});
|
||||
|
|
@ -48,6 +48,20 @@ const FIXTURES: Partial<Record<SupportedLanguages, { filePath: string; source: s
|
|||
filePath: 'meters.dart',
|
||||
source: ['extension type Meters(int value) {', ' int get raw => value;', '}', ''].join('\n'),
|
||||
},
|
||||
[SupportedLanguages.ObjectiveC]: {
|
||||
filePath: 'Marker.m',
|
||||
source: [
|
||||
'RCT_EXTERN_C_BEGIN',
|
||||
'typedef struct {',
|
||||
' int value;',
|
||||
'} GNMarker;',
|
||||
'RCT_EXTERN_C_END',
|
||||
'@protocol GNMarkerProtocol',
|
||||
'- (void)run;',
|
||||
'@end',
|
||||
'',
|
||||
].join('\n'),
|
||||
},
|
||||
};
|
||||
|
||||
const languagesWithHook = Object.entries(providers)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue