mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
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:
co-authored by
Claude Opus 5.5
parent
87e79c6ee8
commit
7678e6ed93
@@ -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));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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))))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user