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:
co-authored by
Claude Opus 4.8
parent
a56d9e6941
commit
fd03f31b2c
@@ -45,6 +45,56 @@ function extractCppReceiverType(node: SyntaxNode, source: string): string | unde
|
||||
return parts.length > 1 ? parts.slice(0, -1).join('::') : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Built-in / non-class return types that can never be a method receiver. We
|
||||
* store no `returnType` for these so resolution never tries to resolve a method
|
||||
* on `void` / `int` / etc.
|
||||
*/
|
||||
const CPP_NON_CLASS_RETURN = new Set([
|
||||
'void', 'bool', 'char', 'short', 'int', 'long', 'float', 'double', 'unsigned',
|
||||
'signed', 'size_t', 'ssize_t', 'auto', 'wchar_t', 'char8_t', 'char16_t',
|
||||
'char32_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t',
|
||||
'uint32_t', 'uint64_t', 'intptr_t', 'uintptr_t', 'nullptr_t',
|
||||
]);
|
||||
|
||||
/**
|
||||
* Normalize a C++ return type to the bare class name a method could be called
|
||||
* on. Unwraps smart-pointer / optional wrappers to their element type
|
||||
* (`std::unique_ptr<Widget>` → `Widget`) so a factory's `->method()` resolves on
|
||||
* the pointee. Strips cv-qualifiers, `&`/`*`, namespace qualifiers, and other
|
||||
* template args. Returns undefined for primitives / void / `auto` / empty.
|
||||
*/
|
||||
export function normalizeCppReturnType(raw: string): string | undefined {
|
||||
let t = raw.trim();
|
||||
if (!t) return undefined;
|
||||
// Unwrap smart pointers / optional to their pointee (the thing you call `->` on).
|
||||
const wrapper = t.match(/\b(?:std\s*::\s*)?(?:unique_ptr|shared_ptr|weak_ptr|optional)\s*<\s*([^,>]+?)\s*>/);
|
||||
if (wrapper && wrapper[1]) t = wrapper[1];
|
||||
t = t
|
||||
.replace(/\b(?:const|volatile|typename|struct|class|enum)\b/g, ' ')
|
||||
.replace(/<[^>]*>/g, ' ')
|
||||
.replace(/[*&]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (!t) return undefined;
|
||||
const last = t.split('::').filter(Boolean).pop();
|
||||
if (!last) return undefined;
|
||||
if (CPP_NON_CLASS_RETURN.has(last)) return undefined;
|
||||
if (!/^[A-Za-z_]\w*$/.test(last)) return undefined;
|
||||
return last;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function/method's return type lives in the `function_definition`'s `type`
|
||||
* field (`Metrics& Metrics::instance()` → `Metrics`). Constructors, destructors,
|
||||
* and conversion operators have no `type` field → undefined.
|
||||
*/
|
||||
function extractCppReturnType(node: SyntaxNode, source: string): string | undefined {
|
||||
const typeNode = getChildByField(node, 'type');
|
||||
if (!typeNode) return undefined;
|
||||
return normalizeCppReturnType(getNodeText(typeNode, source));
|
||||
}
|
||||
|
||||
export const cExtractor: LanguageExtractor = {
|
||||
functionTypes: ['function_definition'],
|
||||
classTypes: [],
|
||||
@@ -60,6 +110,7 @@ export const cExtractor: LanguageExtractor = {
|
||||
nameField: 'declarator',
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
getReturnType: extractCppReturnType,
|
||||
resolveTypeAliasKind: (node, _source) => {
|
||||
// C typedef: `typedef enum { ... } name;` or `typedef struct { ... } name;`
|
||||
// The inner enum_specifier/struct_specifier is anonymous, but we want the typedef name
|
||||
@@ -107,6 +158,7 @@ export const cppExtractor: LanguageExtractor = {
|
||||
paramsField: 'parameters',
|
||||
resolveName: extractCppQualifiedMethodName,
|
||||
getReceiverType: extractCppReceiverType,
|
||||
getReturnType: extractCppReturnType,
|
||||
getVisibility: (node) => {
|
||||
// Check for access specifier in parent
|
||||
const parent = node.parent;
|
||||
|
||||
Reference in New Issue
Block a user