fix(extraction): index C++ pure virtual methods as nodes (#1727) (#1758)

Pure-virtual declarations (`virtual int read(int key) = 0;`) parse as
field_declaration, not function_definition, so they minted no method node —
calls through an abstract base and cpp-override synthesis had nothing to
attach to. Mirror Java interface methods: mint the node (TS + kernel), mark
isAbstract, and cover with extraction/e2e/parity fixtures.

Co-authored-by: Colby McHenry <colbymchenry@users.noreply.github.com>
This commit is contained in:
Colby Mchenry
2026-09-08 02:04:12 -05:00
committed by GitHub
co-authored by Colby McHenry
parent 8df9ecac9d
commit 2f1a99d34c
7 changed files with 240 additions and 6 deletions
+68
View File
@@ -5868,6 +5868,74 @@ end
});
});
describe('C++ pure-virtual method nodes (#1727)', () => {
// Pure-virtual methods are field_declarations (`virtual int read(int key) = 0;`),
// not function_definitions — they previously minted no method node, so calls
// through an abstract base and cpp-override synthesis had nothing to attach to.
// Java interface methods already get nodes; C++ should behave similarly.
it('indexes Store::read from the issue fixture and records the call', () => {
const code = `
class Store {
public:
virtual ~Store() {}
virtual int read(int key) = 0;
};
class DiskStore : public Store {
public:
int read(int key) override { return key + 1; }
};
class MemStore : public Store {
public:
int read(int key) override { return key + 2; }
};
int fetch(Store* s, int k) {
return s->read(k);
}
`;
const result = extractFromSource('store.cc', code);
const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.qualifiedName);
expect(methods).toContain('Store::read');
expect(methods).toContain('DiskStore::read');
expect(methods).toContain('MemStore::read');
const baseRead = result.nodes.find((n) => n.qualifiedName === 'Store::read');
expect(baseRead?.isAbstract).toBe(true);
// Call site unresolved ref targets the method name (resolver types the receiver).
expect(
result.unresolvedReferences.some(
(r) => r.referenceKind === 'calls' && (r.referenceName === 'read' || r.referenceName.endsWith('.read') || r.referenceName.endsWith('->read') || r.referenceName === 's.read')
)
).toBe(true);
});
it('indexes pure virtuals with pointer/reference return types and operators', () => {
const code = `
class Cloneable {
public:
virtual Cloneable* clone() = 0;
virtual const Foo& get() = 0;
virtual Cloneable& operator=(const Cloneable&) = 0;
int notPure(int x);
int data = 0;
};
`;
const result = extractFromSource('clone.hpp', code);
const methods = result.nodes.filter((n) => n.kind === 'method').map((n) => n.name);
expect(methods).toContain('clone');
expect(methods).toContain('get');
expect(methods).toContain('operator=');
// Non-pure prototype and data member must NOT become methods here.
expect(methods).not.toContain('notPure');
expect(methods).not.toContain('data');
expect(result.nodes.find((n) => n.name === 'clone')?.isAbstract).toBe(true);
});
});
describe('C++ free-function name extraction', () => {
let tempDir: string;
let cg: CodeGraph;
@@ -39,6 +39,8 @@ class Session {
public:
void open();
virtual ~Session() {}
// #1727 — pure virtual must mint a method node (parity between wasm + kernel).
virtual int read(int key) = 0;
};
void Session::open() {}
} // namespace app::net
+55
View File
@@ -353,6 +353,61 @@ describe('C++ end-to-end — virtual override synthesis', () => {
cg.close();
});
it('indexes pure-virtual base methods and bridges overrides (#1727)', async () => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-pure-'));
fs.writeFileSync(
path.join(tmpDir, 'store.cc'),
'class Store {\n' +
'public:\n' +
' virtual ~Store() {}\n' +
' virtual int read(int key) = 0;\n' +
'};\n' +
'class DiskStore : public Store {\n' +
'public:\n' +
' int read(int key) override { return key + 1; }\n' +
'};\n' +
'class MemStore : public Store {\n' +
'public:\n' +
' int read(int key) override { return key + 2; }\n' +
'};\n' +
'int fetch(Store* s, int k) {\n' +
' return s->read(k);\n' +
'}\n'
);
const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();
const storeRead = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'Store::read');
expect(storeRead, 'Store::read pure virtual must be a method node').toBeDefined();
expect(storeRead!.isAbstract).toBe(true);
const diskRead = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'DiskStore::read');
const memRead = cg
.getNodesByKind('method')
.find((n) => n.qualifiedName === 'MemStore::read');
expect(diskRead).toBeDefined();
expect(memRead).toBeDefined();
// cpp-override synthesis: base pure virtual → each override
const out = cg.getOutgoingEdges(storeRead!.id).filter((e) => e.kind === 'calls');
const targets = out.map((e) => e.target);
expect(targets).toContain(diskRead!.id);
expect(targets).toContain(memRead!.id);
// Call through abstract base resolves onto Store::read
const fetch = cg.getNodesByKind('function').find((n) => n.name === 'fetch');
expect(fetch).toBeDefined();
const callees = cg.getCallees(fetch!.id).map((c) => c.node.qualifiedName);
expect(callees).toContain('Store::read');
cg.close();
});
});
describe('Java end-to-end — field-injected bean trace (issue #389)', () => {