fix(cpp): resolve calls through singletons/factories/chained getters (#645) (#742)

A C++ method call whose receiver is another call's result — `Foo::instance().bar()`,
`WidgetFactory::create().draw()`, `openSession()->run()`, or the same stored in an
`auto` local first — lost the receiver's type during extraction. The callee degraded
to a bare method name, so when two classes shared a method name the call silently
resolved to whichever was indexed first (or not at all), corrupting callers / impact /
trace with a plausible-but-wrong edge.

Three parts:
- Capture C++ return types (new nodes.return_type column, schema v5): the
  function_definition's `type` field, normalized — smart-pointer pointee unwrapped,
  void/primitives dropped.
- Preserve the inner-call receiver in extraction: a C/C++ field_expression whose
  receiver is itself a call is encoded `inner().method` instead of dropping to the
  bare name. Other languages keep the existing behavior.
- New resolution strategy (matchCppCallChain): infer the receiver's class from the
  inner call's return type, then resolve AND validate the method on it. Handles
  singletons/accessors, factories returning a different type, free-function
  factories, make_unique/make_shared/new/direct construction, single-level member
  chains, and namespace-qualified inner calls. A wrong inference yields no edge,
  never a wrong one.

EXTRACTION_VERSION 2->3 (re-index to populate return types).

Validated on the issue repro + spdlog: node count stable (no explosion),
deterministic, and ~100 pre-existing wrong `.size()`-style edges removed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-08 20:18:17 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent a56d9e6941
commit fd03f31b2c
14 changed files with 421 additions and 8 deletions
+35
View File
@@ -2369,6 +2369,41 @@ end
});
});
describe('C/C++ return type capture (#645)', () => {
it('captures the normalized return type of a C++ method/function', () => {
const code = `
struct Widget { void draw(); };
class Factory { public: static Widget create(); };
Widget Factory::create() { return Widget(); }
void doNothing() {}
`;
const result = extractFromSource('f.cpp', code);
const create = result.nodes.find(
(n) => n.name === 'create' && (n.kind === 'method' || n.kind === 'function')
);
expect(create?.returnType).toBe('Widget');
// A `void` return records no type, so resolution never tries to resolve a
// method on it.
const doNothing = result.nodes.find((n) => n.name === 'doNothing');
expect(doNothing).toBeDefined();
expect(doNothing?.returnType).toBeUndefined();
});
it('unwraps a smart-pointer return type to its pointee', () => {
const code = `
#include <memory>
struct Widget {};
std::unique_ptr<Widget> makeWidget() { return nullptr; }
`;
const result = extractFromSource('f.cpp', code);
const make = result.nodes.find((n) => n.name === 'makeWidget');
expect(make?.returnType).toBe('Widget');
});
});
describe('C/C++ imports', () => {
it('should extract system include', () => {
const code = `#include <iostream>`;
+1 -1
View File
@@ -242,7 +242,7 @@ describe('Database Connection', () => {
const version = db.getSchemaVersion();
expect(version).not.toBeNull();
expect(version?.version).toBe(4);
expect(version?.version).toBe(5);
db.close();
});
+1 -1
View File
@@ -299,7 +299,7 @@ describe('Best-Candidate Resolution', () => {
describe('Schema v2 Migration', () => {
it.skipIf(!HAS_SQLITE)('should have correct current schema version', async () => {
const { CURRENT_SCHEMA_VERSION } = await import('../src/db/migrations');
expect(CURRENT_SCHEMA_VERSION).toBe(4);
expect(CURRENT_SCHEMA_VERSION).toBe(5);
});
it.skipIf(!HAS_SQLITE)('should have migration for version 2', async () => {
+108
View File
@@ -1918,4 +1918,112 @@ func main() {
}
});
});
describe('C++ chained-call receiver resolution (#645)', () => {
async function indexCpp(files: Record<string, string>): Promise<void> {
for (const [name, content] of Object.entries(files)) {
fs.writeFileSync(path.join(tempDir, name), content);
}
cg = await CodeGraph.init(tempDir, { index: true });
}
function callerNamesOf(qualifiedName: string): string[] {
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
if (!target) return [];
const names = cg
.getIncomingEdges(target.id)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.source)?.name)
.filter((n): n is string => !!n);
return [...new Set(names)].sort();
}
it('resolves singleton chains and auto locals to the right class, never the first-sorted one', async () => {
// Two classes share writeLog; Logger sorts first so it wins any name-only
// tie. All three call forms target Metrics.
await indexCpp({
'logger.hpp': `#pragma once
#include <string>
class Logger { public: static Logger& instance(); void writeLog(const std::string&); };
class Metrics { public: static Metrics& instance(); void writeLog(const std::string&); };
`,
'impl.cpp': `#include "logger.hpp"
Logger& Logger::instance() { static Logger l; return l; }
Metrics& Metrics::instance() { static Metrics m; return m; }
void Logger::writeLog(const std::string&) {}
void Metrics::writeLog(const std::string&) {}
`,
'app.cpp': `#include "logger.hpp"
void a() { Metrics::instance().writeLog("x"); } // chained singleton
void b() { auto& m = Metrics::instance(); m.writeLog("x"); } // stored in auto
void c() { Metrics& m = Metrics::instance(); m.writeLog("x"); } // explicit type
`,
});
expect(callerNamesOf('Metrics::writeLog')).toEqual(['a', 'b', 'c']);
expect(callerNamesOf('Logger::writeLog')).toEqual([]);
});
it('resolves factories, free-function factories, and member chains via the inner call return type', async () => {
await indexCpp({
'types.hpp': `#pragma once
#include <memory>
struct Widget { void draw(); };
struct Session { void run(); };
struct View { void render(); };
class WidgetFactory { public: static Widget create(); };
class Manager { public: View view(); };
Session* openSession();
// Decoy that sorts first and has all three methods — must never win.
struct Aaa { void draw(); void run(); void render(); };
`,
'impl.cpp': `#include "types.hpp"
void Widget::draw() {}
void Session::run() {}
void View::render() {}
void Aaa::draw() {}
void Aaa::run() {}
void Aaa::render() {}
Widget WidgetFactory::create() { return Widget(); }
View Manager::view() { return View(); }
Session* openSession() { return nullptr; }
`,
'app.cpp': `#include "types.hpp"
void factory() { WidgetFactory::create().draw(); } // -> Widget::draw
void freefunc() { openSession()->run(); } // -> Session::run
void member() { Manager mgr; mgr.view().render(); } // -> View::render
void makeUnique() { auto w = std::make_unique<Widget>(); w->draw(); } // -> Widget::draw
`,
});
expect(callerNamesOf('Widget::draw')).toEqual(['factory', 'makeUnique']);
expect(callerNamesOf('Session::run')).toEqual(['freefunc']);
expect(callerNamesOf('View::render')).toEqual(['member']);
// The first-sorted decoy never captures any of them.
expect(callerNamesOf('Aaa::draw')).toEqual([]);
expect(callerNamesOf('Aaa::run')).toEqual([]);
expect(callerNamesOf('Aaa::render')).toEqual([]);
});
it('creates NO edge when the inferred type lacks the method (silent miss, not a wrong edge)', async () => {
await indexCpp({
'types.hpp': `#pragma once
struct Widget { void draw(); };
struct Other { void onlyOther(); };
class WidgetFactory { public: static Widget create(); };
`,
'impl.cpp': `#include "types.hpp"
void Widget::draw() {}
void Other::onlyOther() {}
Widget WidgetFactory::create() { return Widget(); }
`,
'app.cpp': `#include "types.hpp"
// Widget has no onlyOther() — must produce NO edge, never a wrong one to Other.
void wrong() { WidgetFactory::create().onlyOther(); }
`,
});
expect(callerNamesOf('Other::onlyOther')).toEqual([]);
});
});
});