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:
co-authored by
Colby McHenry
parent
8df9ecac9d
commit
2f1a99d34c
@@ -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
|
||||
|
||||
@@ -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)', () => {
|
||||
|
||||
Reference in New Issue
Block a user