diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index b5a28a34..57c6a11e 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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)); + }); +}); diff --git a/__tests__/kernel-ccpp-parity.test.ts b/__tests__/kernel-ccpp-parity.test.ts index 2908d423..c05a15d6 100644 --- a/__tests__/kernel-ccpp-parity.test.ts +++ b/__tests__/kernel-ccpp-parity.test.ts @@ -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'); diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index 88141aba..bd7c8dbd 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -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]|[^"\\])*(?:"|$)|(? 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)))) ) ) )