fix(objective-c): complete provider follow-ups

This commit is contained in:
ximengkai 2026-09-05 22:35:42 +08:00
parent 27c0e5d9ee
commit 6054ed5304
13 changed files with 113 additions and 8 deletions

View file

@ -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':

View file

@ -135,6 +135,8 @@ export const FILTERABLE_LABELS: NodeLabel[] = [
'Folder',
'File',
'Class',
'Protocol',
'Category',
'Interface',
'Enum',
'Type',

View file

@ -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', () => {

View file

@ -7,6 +7,8 @@ const LEGEND_LABELS: NodeLabel[] = [
'Folder',
'File',
'Class',
'Protocol',
'Category',
'Interface',
'Enum',
'Type',
@ -20,6 +22,8 @@ const ICON_MAP: Record<string, string> = {
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',

View file

@ -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,

View file

@ -112,8 +112,6 @@ export const isShortLabel = (label: string): boolean =>
*/
export const STRUCTURAL_LABELS: ReadonlySet<string> = new Set([
LABEL_CLASS,
LABEL_PROTOCOL,
LABEL_CATEGORY,
LABEL_STRUCT,
LABEL_INTERFACE,
]);

View file

@ -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 &&

View file

@ -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? |
* |---------------|---------------------------------------------------|--------------|----------------|

View file

@ -55,5 +55,4 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
[SupportedLanguages.Vue, vueScopeResolver],
[SupportedLanguages.Zig, zigScopeResolver],
[SupportedLanguages.ObjectiveC, objectiveCScopeResolver],
[SupportedLanguages.Zig, zigScopeResolver],
]);

View file

@ -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 <Runnable, Observable>', '- (void)run;', '@end'].join('\n');
const protocolNameStart = content.indexOf('Worker');

View file

@ -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);
});
});
// ────────────────────────────────────────────────────────────────────────────

View file

@ -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`, () => {

View file

@ -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));
});
});