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
+164 -2
View File
@@ -351,6 +351,7 @@ function inferCppReceiverType(
receiverName: string,
ref: UnresolvedRef,
context: ResolutionContext,
depth = 0,
): string | null {
const source = context.readFile(ref.filePath);
if (!source) return null;
@@ -368,7 +369,15 @@ function inferCppReceiverType(
const declaratorMatch = line.match(declaratorRegex);
if (declaratorMatch) {
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
if (normalized) return normalized;
if (normalized === 'auto') {
// `auto x = Foo::instance();` — the declared type is deduced; recover it
// from the initializer (call return type / construction) (#645).
const initType = inferCppAutoInitializerType(line, receiverName, ref, context, depth);
if (initType) return initType;
// No usable initializer on this line — keep scanning earlier ones.
} else if (normalized) {
return normalized;
}
}
}
@@ -388,13 +397,158 @@ function inferCppReceiverType(
const declaratorMatch = line.match(declaratorRegex);
if (!declaratorMatch) continue;
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
if (normalized) return normalized;
if (normalized && normalized !== 'auto') return normalized;
}
}
return null;
}
/**
* Last `::`-separated segment of a (possibly namespace-qualified) C++ name.
*/
function cppLastSegment(name: string): string {
const parts = name.split('::').filter(Boolean);
return parts[parts.length - 1] ?? name;
}
/**
* Return type captured at extraction for `Class::method` (or a free function),
* read off the indexed node's `returnType` (#645). Null when not indexed or no
* return type was recorded (e.g. a `void`/primitive return).
*/
function lookupCppReturnType(
callee: string,
ref: UnresolvedRef,
context: ResolutionContext,
): string | null {
let method = callee;
let cls: string | null = null;
if (callee.includes('::')) {
const parts = callee.split('::').filter(Boolean);
method = parts[parts.length - 1] ?? callee;
cls = parts.slice(0, -1).join('::');
}
const candidates = context.getNodesByName(method).filter(
(n) =>
(n.kind === 'method' || n.kind === 'function') &&
n.language === ref.language &&
!!n.returnType,
);
if (cls) {
const want = `${cls}::${method}`;
// The call site may name the class with MORE namespace qualification than
// the stored node (`details::registry::instance` at the call vs
// `registry::instance` on the node — the receiver type only carries the
// immediate class), or LESS. Accept an exact match or either being a
// namespace-suffix of the other; the shared `::<class>::<method>` tail keeps
// it specific.
const m = candidates.find(
(n) =>
n.qualifiedName === want ||
n.qualifiedName.endsWith(`::${want}`) ||
want.endsWith(`::${n.qualifiedName}`),
);
return m?.returnType ?? null;
}
return candidates.find((n) => n.kind === 'function')?.returnType ?? null;
}
/** Does the graph contain a class/struct named `name`'s last segment? */
function cppClassExists(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
const last = cppLastSegment(name);
return context
.getNodesByName(last)
.some((n) => (n.kind === 'class' || n.kind === 'struct') && n.language === ref.language);
}
/**
* Infer the class produced by a C++ call/construction expression, using return
* types captured at extraction (#645). Handles, in order:
* - `make_unique<T>()` / `make_shared<T>()` → T
* - single-level member call `recv.method()` → recv's type, then method's return
* - `Class::method()` / free `func()` → the callee's recorded return type
* - direct construction `Type()` / `ns::Type()` → Type
* Returns null when undeterminable. Callers MUST still validate the outer method
* exists on the result before creating an edge, so a wrong guess stays silent.
*/
function resolveCppCallResultType(
inner: string,
ref: UnresolvedRef,
context: ResolutionContext,
depth = 0,
): string | null {
if (depth > 3) return null; // guard against pathological mutual recursion
const expr = inner.trim();
const make = expr.match(/(?:^|::)(?:make_unique|make_shared)\s*<\s*([A-Za-z_]\w*)/);
if (make) return make[1] ?? null;
// Single-level member call `recv.method` (the `manager.view().render()` shape).
const dotIdx = expr.lastIndexOf('.');
if (dotIdx > 0) {
const recv = expr.slice(0, dotIdx);
const method = expr.slice(dotIdx + 1);
if (recv.includes('.') || recv.includes('(') || recv.includes('::')) return null; // single level only
const recvType = inferCppReceiverType(recv, ref, context, depth + 1);
if (!recvType) return null;
return lookupCppReturnType(`${recvType}::${method}`, ref, context);
}
const ret = lookupCppReturnType(expr, ref, context);
if (ret) return ret;
// Direct construction — the callee itself names a class/struct.
if (cppClassExists(expr, ref, context)) return cppLastSegment(expr);
return null;
}
/**
* Recover the type of an `auto`-declared local from its initializer on the
* declaration line — `auto x = Foo::instance();`, `auto w = make_unique<W>();`,
* `auto p = new W();`, `auto w = Widget();` (#645).
*/
function inferCppAutoInitializerType(
line: string,
receiverName: string,
ref: UnresolvedRef,
context: ResolutionContext,
depth: number,
): string | null {
const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const m = line.match(new RegExp(`\\b${escaped}\\b\\s*=\\s*([^;]+)`));
if (!m || !m[1]) return null;
const init = m[1].trim();
const neu = init.match(/^new\s+([A-Za-z_][\w:]*)/);
if (neu && neu[1]) return cppLastSegment(neu[1]);
// A call or construction: `Foo(...)`, `A::b(...)`, `make_unique<T>(...)`.
const call = init.match(/^([A-Za-z_][\w:]*(?:\s*<[^>;]*>)?)\s*\(/);
if (call && call[1]) return resolveCppCallResultType(call[1].replace(/\s+/g, ''), ref, context, depth + 1);
return null;
}
/**
* Resolve a C++ chained call whose receiver is itself a call — encoded by the
* extractor as `<innerCallee>().<method>` (#645). The receiver's type is what
* the inner call returns; the outer method is then resolved and VALIDATED on it
* (resolveMethodOnType requires `cls::method` to exist), so a wrong inference
* produces no edge rather than a wrong one.
*/
export function matchCppCallChain(
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null {
const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
if (!m || !m[1] || !m[2]) return null;
const cls = resolveCppCallResultType(m[1], ref, context);
if (!cls) return null;
return resolveMethodOnType(cls, m[2], ref, context, 0.85, 'instance-method');
}
/**
* Java/Kotlin: infer a receiver's declared type by walking field declarations
* in the class enclosing the call site. The field's `signature` is already in
@@ -809,6 +963,14 @@ export function matchReference(
result = matchByQualifiedName(ref, context);
if (result) return result;
// 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
// encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
// receiver's type from what the inner call returns, then the method on it.
if (ref.language === 'cpp' || ref.language === 'c') {
result = matchCppCallChain(ref, context);
if (result) return result;
}
// 2. Method call pattern
result = matchMethodCall(ref, context);
if (result) return result;