fix(cpp): recover MSVC COM interface declarations (#1519) (#2018)

MSVC COM interface declarations were parsed as functions, losing their owners and methods.
Normalize declaration-position interface keywords to padded struct keywords when alias or declaration evidence is present.
Preserve source offsets and protected text through the shared native/WASM preprocessing hook.
Cover real-file indexing, inheritance, source retrieval, and parser parity.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-27 13:11:57 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 87e79c6ee8
commit 7678e6ed93
3 changed files with 110 additions and 1 deletions
+67
View File
@@ -13167,3 +13167,70 @@ describe('Unsupported-language projects report what they skipped (#1502)', () =>
expect(stats.unsupportedByExtension.size).toBe(0);
});
});
describe('C++ COM interface declarations (#1519)', () => {
let tempDir: string;
let cg: CodeGraph | undefined;
afterEach(() => {
cg?.close();
cg = undefined;
if (tempDir) cleanupTempDir(tempDir);
});
it.each(['\n', '\r\n'])('indexes COM owners, methods and inheritance with %j line endings', async (eol) => {
const source = [
'#define interface struct',
'struct IParentInterface { virtual void Parent() = 0; };',
'interface IMyComInterface : IParentInterface {',
' virtual void Foo() = 0;',
' virtual void Bar() = 0;',
'};',
'interface IStandalone { virtual void Run() = 0; };',
'',
].join(eol);
expect(detectLanguage('MyInterface.h', source)).toBe('cpp');
tempDir = createTempDir();
fs.writeFileSync(path.join(tempDir, 'MyInterface.h'), source);
cg = CodeGraph.initSync(tempDir);
await cg.indexAll();
cg.resolveReferences();
const nodes = cg.getNodesInFile('MyInterface.h');
const owner = nodes.find((n) => n.name === 'IMyComInterface');
expect(owner).toMatchObject({ kind: 'struct', startLine: 3 });
for (const [name, line] of [['Foo', 4], ['Bar', 5]] as const) {
expect(nodes.find((n) => n.name === name)).toMatchObject({
kind: 'method', qualifiedName: `IMyComInterface::${name}`, isAbstract: true, startLine: line,
});
}
expect(nodes.find((n) => n.name === 'IStandalone')).toMatchObject({ kind: 'struct' });
expect(nodes.find((n) => n.name === 'Run')).toMatchObject({ qualifiedName: 'IStandalone::Run', isAbstract: true });
expect(nodes.filter((n) => n.kind === 'function')).toEqual([]);
const parent = nodes.find((n) => n.name === 'IParentInterface');
expect(cg.getOutgoingEdges(owner!.id)).toContainEqual(expect.objectContaining({ kind: 'extends', target: parent!.id }));
expect(await cg.getCode(owner!.id)).toContain('interface IMyComInterface');
});
it('normalizes declaration evidence without a local alias and preserves all other bytes', async () => {
const { cppExtractor } = await import('../src/extraction/languages/c-cpp');
const source = [
'// interface Comment : Base {};',
'/* interface Block { virtual void Fake() = 0; }; */',
'const char* text = "interface String : Base {};";',
'const char* raw = R"tag(interface Raw : Base {})tag";',
'#define SAMPLE interface Macro : Base {}',
'#define MULTI \\',
'interface Continued : Base {}',
'int interface = 1;',
'void interface();',
'interface value;',
'interface ordinary{};',
'interface IDerived : Base { virtual void Foo() = 0; };',
'interface IStandalone { virtual void Run() = 0; };',
'',
].join('\r\n');
const expected = source.replace('interface IDerived', 'struct IDerived').replace('interface IStandalone', 'struct IStandalone');
expect(cppExtractor.preParse!(source, 'com.hpp')).toBe(expected);
expect(Buffer.byteLength(expected)).toBe(Buffer.byteLength(source));
});
});
+14
View File
@@ -122,6 +122,20 @@ describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => {
expect(names).not.toContain('registration');
});
it.each(['\n', '\r\n'])('COM interface declarations retain native/wasm parity (%j)', (eol) => {
const source = [
'#define interface struct',
'struct IParentInterface { virtual void Parent() = 0; };',
'interface IMyComInterface : IParentInterface {',
' virtual void Foo() = 0;',
' virtual void Bar() = 0;',
'};',
'interface IStandalone { virtual void Run() = 0; };',
'',
].join(eol);
assertParity('MyInterface.h', source, 'cpp', 8);
});
it('torture fixture (c): fn-ptr tables, typedefs, file-scope consts, value-refs', () => {
const file = path.join(FIXTURE_DIR, 'torture.c');
assertParity('fixtures/torture.c', fs.readFileSync(file, 'utf8'), 'c');
+29 -1
View File
@@ -918,6 +918,34 @@ function restoreDirectiveLines(original: string, blanked: string): string {
return changed ? b.join('\n') : blanked;
}
/** Recover MSVC's COM alias only at type-definition sites. Match on a lexical
* mask so comments, literals and directives cannot supply declaration evidence;
* replace just the keyword, preserving source offsets on both parser arms. */
function normalizeCppComInterfaces(source: string): string {
if (!source.includes('interface')) return source;
// Raw strings have already been masked by preParseCppSource.
let code = source.replace(
/\/\/(?:\\\r?\n|[^\r\n])*|\/\*[\s\S]*?(?:\*\/|$)|"(?:\\[\s\S]|[^"\\])*(?:"|$)|(?<!\w)(?:u8|[LuU])?'(?:\\[\s\S]|[^'\\])*(?:'|$)/g,
(m) => m.replace(/[^\r\n]/g, ' ')
);
const hasAlias = /^[ \t]*#[ \t]*define[ \t]+interface[ \t]+struct\b/m.test(code);
code = code.replace(/^[ \t]*#(?:\\\r?\n|[^\r\n])*/gm, (m) => m.replace(/[^\r\n]/g, '\0'));
const declaration = /(^|[;{}])\s*\binterface\s+[A-Za-z_]\w*\s*(:[^;{}]+)?\{/gm;
const offsets: number[] = [];
let match: RegExpExecArray | null;
while ((match = declaration.exec(code)) !== null) {
const bodyEnd = code.indexOf('}', declaration.lastIndex);
const hasVirtual = /\bvirtual\b/.test(code.slice(declaration.lastIndex, bodyEnd < 0 ? code.length : bodyEnd));
if (!hasAlias && !match[2] && !hasVirtual) continue;
offsets.push(match.index + match[0].indexOf('interface'));
declaration.lastIndex--; // allow the opening brace to start a nested declaration
}
for (const offset of offsets) {
source = source.slice(0, offset) + 'struct ' + source.slice(offset + 9);
}
return source;
}
/** C/C++ source pre-processing before tree-sitter: recover macro-annotated class
* definitions, macro-prefixed function definitions, macro-prefixed members, and
* macro-decorated members (Unreal-Engine reflection markup) — plus the non-C++
@@ -935,7 +963,7 @@ function preParseCppSource(source: string, filePath?: string): string {
blankCLeadingAttrMacros(
blankCppAnnotationMacroCalls(
blankCppInlineAnnotationMacros(
blankCppApiPrefixMacros(blankCppInlineMacros(blankCppExportMacros(source)))
blankCppApiPrefixMacros(blankCppInlineMacros(blankCppExportMacros(normalizeCppComInterfaces(source))))
)
)
)