From 6054ed5304d6e7a2424080cedf72af6cfb5ef3f4 Mon Sep 17 00:00:00 2001 From: ximengkai Date: Sat, 5 Sep 2026 22:35:42 +0800 Subject: [PATCH] fix(objective-c): complete provider follow-ups --- gitnexus-web/src/components/FileTreePanel.tsx | 4 ++ gitnexus-web/src/lib/constants.ts | 2 + gitnexus-web/test/unit/constants.test.ts | 2 + gitnexus-web/test/unit/filter-panel.test.ts | 8 ++++ gitnexus/src/core/embeddings/chunker.ts | 5 +- gitnexus/src/core/embeddings/types.ts | 2 - .../objective-c/macro-marker-preprocess.ts | 12 ++++- .../ingestion/model/registration-table.ts | 2 +- .../scope-resolution/pipeline/registry.ts | 1 - gitnexus/test/unit/chunker.test.ts | 47 +++++++++++++++++++ gitnexus/test/unit/embedding-pipeline.test.ts | 8 ++++ .../unit/model/registration-table.test.ts | 13 ++++- ...bjective-c-macro-marker-preprocess.test.ts | 15 ++++++ 13 files changed, 113 insertions(+), 8 deletions(-) diff --git a/gitnexus-web/src/components/FileTreePanel.tsx b/gitnexus-web/src/components/FileTreePanel.tsx index a3c7487a9..3004e9353 100644 --- a/gitnexus-web/src/components/FileTreePanel.tsx +++ b/gitnexus-web/src/components/FileTreePanel.tsx @@ -186,6 +186,10 @@ const getNodeTypeIcon = (label: NodeLabel) => { return FileCode; case 'Class': return Box; + case 'Protocol': + return Hash; + case 'Category': + return Box; case 'Function': return Braces; case 'Method': diff --git a/gitnexus-web/src/lib/constants.ts b/gitnexus-web/src/lib/constants.ts index 43ebd1425..8e5259741 100644 --- a/gitnexus-web/src/lib/constants.ts +++ b/gitnexus-web/src/lib/constants.ts @@ -135,6 +135,8 @@ export const FILTERABLE_LABELS: NodeLabel[] = [ 'Folder', 'File', 'Class', + 'Protocol', + 'Category', 'Interface', 'Enum', 'Type', diff --git a/gitnexus-web/test/unit/constants.test.ts b/gitnexus-web/test/unit/constants.test.ts index 10fe238b9..216a3d52f 100644 --- a/gitnexus-web/test/unit/constants.test.ts +++ b/gitnexus-web/test/unit/constants.test.ts @@ -65,6 +65,8 @@ describe('FILTERABLE_LABELS', () => { expect(FILTERABLE_LABELS).toContain('Type'); expect(FILTERABLE_LABELS).toContain('Decorator'); expect(FILTERABLE_LABELS).toContain('Variable'); + expect(FILTERABLE_LABELS).toContain('Protocol'); + expect(FILTERABLE_LABELS).toContain('Category'); }); it('every filterable label has a defined color in NODE_COLORS', () => { diff --git a/gitnexus-web/test/unit/filter-panel.test.ts b/gitnexus-web/test/unit/filter-panel.test.ts index 67ab3397c..9bc9c5132 100644 --- a/gitnexus-web/test/unit/filter-panel.test.ts +++ b/gitnexus-web/test/unit/filter-panel.test.ts @@ -7,6 +7,8 @@ const LEGEND_LABELS: NodeLabel[] = [ 'Folder', 'File', 'Class', + 'Protocol', + 'Category', 'Interface', 'Enum', 'Type', @@ -20,6 +22,8 @@ const ICON_MAP: Record = { Folder: 'Folder', File: 'FileCode', Class: 'Box', + Protocol: 'Hash', + Category: 'Box', Function: 'Braces', Method: 'Braces', Interface: 'Hash', @@ -62,6 +66,8 @@ describe('color legend', () => { expect(LEGEND_LABELS).toContain('Type'); expect(LEGEND_LABELS).toContain('Decorator'); expect(LEGEND_LABELS).toContain('Variable'); + expect(LEGEND_LABELS).toContain('Protocol'); + expect(LEGEND_LABELS).toContain('Category'); }); it('every legend label has a color defined', () => { @@ -76,6 +82,8 @@ describe('color legend', () => { 'Folder', 'File', 'Class', + 'Protocol', + 'Category', 'Interface', 'Enum', 'Type', diff --git a/gitnexus/src/core/embeddings/chunker.ts b/gitnexus/src/core/embeddings/chunker.ts index f6216e1b1..367767786 100644 --- a/gitnexus/src/core/embeddings/chunker.ts +++ b/gitnexus/src/core/embeddings/chunker.ts @@ -163,7 +163,10 @@ const FIELD_LIKE_MEMBER_TYPES = new Set([ 'enum_assignment', ]); -const DECLARATION_MEMBER_WRAPPER_TYPES = new Set(['qualified_protocol_interface_declaration']); +const DECLARATION_MEMBER_WRAPPER_TYPES = new Set([ + 'qualified_protocol_interface_declaration', + 'instance_variables', +]); const declarationChunk = async ( content: string, diff --git a/gitnexus/src/core/embeddings/types.ts b/gitnexus/src/core/embeddings/types.ts index e564e734a..608e07582 100644 --- a/gitnexus/src/core/embeddings/types.ts +++ b/gitnexus/src/core/embeddings/types.ts @@ -112,8 +112,6 @@ export const isShortLabel = (label: string): boolean => */ export const STRUCTURAL_LABELS: ReadonlySet = new Set([ LABEL_CLASS, - LABEL_PROTOCOL, - LABEL_CATEGORY, LABEL_STRUCT, LABEL_INTERFACE, ]); diff --git a/gitnexus/src/core/ingestion/languages/objective-c/macro-marker-preprocess.ts b/gitnexus/src/core/ingestion/languages/objective-c/macro-marker-preprocess.ts index a0d61b34a..0409423a9 100644 --- a/gitnexus/src/core/ingestion/languages/objective-c/macro-marker-preprocess.ts +++ b/gitnexus/src/core/ingestion/languages/objective-c/macro-marker-preprocess.ts @@ -17,6 +17,7 @@ interface ScanState { inBlockComment: boolean; + inLineCommentContinuation: boolean; inPreprocessorDirective: boolean; quote: '"' | "'" | undefined; braceDepth: number; @@ -66,6 +67,10 @@ function startsPreprocessorDirective(line: string): boolean { } function scanLine(line: string, state: ScanState): void { + if (state.inLineCommentContinuation) { + state.inLineCommentContinuation = hasEscapedLineEnding(line); + return; + } if (state.inPreprocessorDirective) { state.inPreprocessorDirective = hasEscapedLineEnding(line); return; @@ -96,7 +101,10 @@ function scanLine(line: string, state: ScanState): void { continue; } - if (code === 0x2f && next === 0x2f) return; + if (code === 0x2f && next === 0x2f) { + state.inLineCommentContinuation = hasEscapedLineEnding(line); + return; + } if (code === 0x2f && next === 0x2a) { state.inBlockComment = true; index++; @@ -123,6 +131,7 @@ function scanLine(line: string, state: ScanState): void { export function preprocessObjectiveCMacroMarkers(source: string, _filePath?: string): string { const state: ScanState = { inBlockComment: false, + inLineCommentContinuation: false, inPreprocessorDirective: false, quote: undefined, braceDepth: 0, @@ -134,6 +143,7 @@ export function preprocessObjectiveCMacroMarkers(source: string, _filePath?: str const line = segments[index]; if ( !state.inBlockComment && + !state.inLineCommentContinuation && !state.inPreprocessorDirective && state.quote === undefined && state.braceDepth === 0 && diff --git a/gitnexus/src/core/ingestion/model/registration-table.ts b/gitnexus/src/core/ingestion/model/registration-table.ts index a62943607..718079a95 100644 --- a/gitnexus/src/core/ingestion/model/registration-table.ts +++ b/gitnexus/src/core/ingestion/model/registration-table.ts @@ -15,7 +15,7 @@ * └── symbols (SymbolTable) ← owns fileIndex + callableByName, * calls dispatch() in add() * - * ## Behavior groups (5 hooks, 13 table entries) + * ## Behavior groups (4 hooks, 12 table entries) * * | Group | NodeLabel values | Hook | Skip callable? | * |---------------|---------------------------------------------------|--------------|----------------| diff --git a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts index 2b4004ab3..ee98e3807 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/pipeline/registry.ts @@ -55,5 +55,4 @@ export const SCOPE_RESOLVERS: ReadonlyMap = n [SupportedLanguages.Vue, vueScopeResolver], [SupportedLanguages.Zig, zigScopeResolver], [SupportedLanguages.ObjectiveC, objectiveCScopeResolver], - [SupportedLanguages.Zig, zigScopeResolver], ]); diff --git a/gitnexus/test/unit/chunker.test.ts b/gitnexus/test/unit/chunker.test.ts index 84911c2ef..513ab4a67 100644 --- a/gitnexus/test/unit/chunker.test.ts +++ b/gitnexus/test/unit/chunker.test.ts @@ -412,6 +412,53 @@ describe('chunkNode', () => { expect(createParserForLanguage).toHaveBeenCalledWith('objective-c', 'ProtocolSections.m'); }); + it('expands Objective-C instance variables before chunking a class declaration', async () => { + const content = [ + '@interface Worker {', + ' id _first;', + ' id _second;', + '}', + '- (void)run;', + '@end', + ].join('\n'); + const firstIvar = 'id _first;'; + const secondIvar = 'id _second;'; + const method = '- (void)run;'; + const firstIvarStart = content.indexOf(firstIvar); + const secondIvarStart = content.indexOf(secondIvar); + const methodStart = content.indexOf(method); + const instanceVariables = makeFakeNode( + 'instance_variables', + content.indexOf('{'), + content.indexOf('}') + 1, + [ + makeFakeNode('field_definition', firstIvarStart, firstIvarStart + firstIvar.length), + makeFakeNode('field_definition', secondIvarStart, secondIvarStart + secondIvar.length), + ], + ); + const declaration = makeFakeNode('class_interface', 0, content.length, [ + makeFakeNode('identifier', content.indexOf('Worker'), content.indexOf('Worker') + 'Worker'.length), + instanceVariables, + makeFakeNode('method_declaration', methodStart, methodStart + method.length), + ]); + createParserForLanguage.mockResolvedValue({ + parse: vi.fn().mockReturnValue({ + rootNode: makeFakeNode('program', 0, content.length, [declaration]), + }), + }); + + const result = await chunkNode('Class', content, 'Worker.m', 1, 6, 48, 0); + const combined = result.map((chunk) => chunk.text).join('\n'); + + expect(result.length).toBeGreaterThan(1); + expect(combined).toContain(firstIvar); + expect(combined).toContain(secondIvar); + expect(combined).toContain(method); + expect(result.some((chunk) => chunk.text.includes(firstIvar) && chunk.text.includes(secondIvar))).toBe( + true, + ); + }); + it('keeps Objective-C protocol inheritance in the declaration prefix', async () => { const content = ['@protocol Worker ', '- (void)run;', '@end'].join('\n'); const protocolNameStart = content.indexOf('Worker'); diff --git a/gitnexus/test/unit/embedding-pipeline.test.ts b/gitnexus/test/unit/embedding-pipeline.test.ts index c00e57d6d..2ace47b4f 100644 --- a/gitnexus/test/unit/embedding-pipeline.test.ts +++ b/gitnexus/test/unit/embedding-pipeline.test.ts @@ -13,6 +13,7 @@ import { LABEL_CATEGORY, LABEL_PROTOCOL, LABELS_WITH_EXPORTED, + STRUCTURAL_LABELS, } from '../../src/core/embeddings/types.js'; import { STALE_HASH_SENTINEL } from '../../src/core/lbug/schema.js'; @@ -24,6 +25,13 @@ describe('embedding schema column contracts', () => { expect(LABELS_WITH_EXPORTED.has(LABEL_PROTOCOL)).toBe(false); expect(LABELS_WITH_EXPORTED.has(LABEL_CATEGORY)).toBe(false); }); + + it('keeps Objective-C protocol/category declaration chunking without unsupported structural extraction', () => { + expect(STRUCTURAL_LABELS.has(LABEL_PROTOCOL)).toBe(false); + expect(STRUCTURAL_LABELS.has(LABEL_CATEGORY)).toBe(false); + expect(EMBEDDABLE_LABELS).toContain(LABEL_PROTOCOL); + expect(EMBEDDABLE_LABELS).toContain(LABEL_CATEGORY); + }); }); // ──────────────────────────────────────────────────────────────────────────── diff --git a/gitnexus/test/unit/model/registration-table.test.ts b/gitnexus/test/unit/model/registration-table.test.ts index 0d2ebc54f..d6ccc4100 100644 --- a/gitnexus/test/unit/model/registration-table.test.ts +++ b/gitnexus/test/unit/model/registration-table.test.ts @@ -140,8 +140,17 @@ describe('BasicBlock taint/PDG substrate label (issue #2080)', () => { // reference-equality assertions on the hook functions themselves. // --------------------------------------------------------------------------- -describe('class-like behavior group — all 6 labels route to types.registerClass', () => { - const CLASS_LIKE_LABELS = ['Class', 'Struct', 'Interface', 'Enum', 'Record', 'Trait'] as const; +describe('class-like behavior group — all 8 labels route to types.registerClass', () => { + const CLASS_LIKE_LABELS = [ + 'Class', + 'Protocol', + 'Category', + 'Struct', + 'Interface', + 'Enum', + 'Record', + 'Trait', + ] as const; for (const label of CLASS_LIKE_LABELS) { it(`${label} writes to types.registerClass`, () => { diff --git a/gitnexus/test/unit/objective-c-macro-marker-preprocess.test.ts b/gitnexus/test/unit/objective-c-macro-marker-preprocess.test.ts index 9a76836ad..904e67645 100644 --- a/gitnexus/test/unit/objective-c-macro-marker-preprocess.test.ts +++ b/gitnexus/test/unit/objective-c-macro-marker-preprocess.test.ts @@ -48,4 +48,19 @@ describe('preprocessObjectiveCMacroMarkers', () => { expect(preprocessObjectiveCMacroMarkers(source, 'Example.m')).toBe(source); }); + + it('does not rewrite markers inside a continued line comment', () => { + const source = [ + '// The following token remains part of this comment \\', + 'RCT_EXTERN_C_END', + 'RCT_EXTERN_C_BEGIN', + '', + ].join('\n'); + + const normalized = preprocessObjectiveCMacroMarkers(source, 'CommentedMarker.h'); + + expect(normalized).toHaveLength(source.length); + expect(normalized).toContain('RCT_EXTERN_C_END'); + expect(normalized.split('\n')[2]).toBe(' '.repeat('RCT_EXTERN_C_BEGIN'.length)); + }); });