fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1268)
* fix(cpp): resolve explicit operator calls (a.operator+(b)) to the operator method (#1247) tree-sitter-cpp can't parse an operator_name in field position: the call_expression carries `function: <receiver>` plus an ERROR child wrapping the operator_name instead of a field_expression callee, so the extractor emitted a calls ref named just the receiver (`a`) and the edge never resolved — while the operator method itself indexed fine. Two-part fix, scoped to the explicit call form (infix `a + b` / `a[i]` need receiver type inference and are tracked in #1258): - extraction: recover the operator_name from the ERROR child and emit `<receiver>.operator+` (`->` receivers normalized, `this->` emits the bare name), like any other member call - resolution: matchMethodCall's dot pattern now admits an operator method part (cpp-gated; symbol chars failed the \w match), so receiver-type inference + resolveMethodOnType validate the target — a same-named operator on an unrelated class can't capture the edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(cpp): harden explicit operator-call recovery against real-world shapes (#1247) Validated on nlohmann/json (dozens of explicit operator[] / operator* / operator< call sites). Two refinements the synthetic fixtures missed: - normalize spaced call-site operator names (`it.operator * ()`, `other.operator < (*this)`) to the compact form definitions index as - drop the ref for a complex receiver (`obj()->operator+`, member chains ending in a call) instead of emitting a bare operator name: exact-name fallback GUESSED among unrelated same-named operators (linked a std::map operator[] call to an in-repo operator[]) — silent miss, never a wrong edge Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ecc8b307ac
commit
6103f5e228
@@ -3180,6 +3180,76 @@ void wrong() { WidgetFactory::create().onlyOther(); }
|
||||
});
|
||||
});
|
||||
|
||||
describe('C++ explicit operator-call resolution (#1247)', () => {
|
||||
// `a.operator+(b)` produced no calls edge: the operator_name lands in an
|
||||
// ERROR node (never a field_expression callee), so the extractor emitted a
|
||||
// ref named just `a`. With the ERROR-node recovery it emits `a.operator+`,
|
||||
// and matchMethodCall (dot pattern extended to admit operator method parts)
|
||||
// resolves it through receiver-type inference. Infix `a + b` / `a[i]` need
|
||||
// real type inference and are out of scope here (#1258).
|
||||
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 explicit operator calls to the receiver type, never a same-named decoy', async () => {
|
||||
// Aaa sorts first and declares the same operators — only receiver-type
|
||||
// inference (const V& a → V) can pick V, so a name-only tie can't win.
|
||||
await indexCpp({
|
||||
'optest.cpp': `struct Aaa {
|
||||
Aaa operator+(const Aaa& o) const { return o; }
|
||||
Aaa operator[](int i) const { return *this; }
|
||||
};
|
||||
struct V {
|
||||
int x;
|
||||
V operator+(const V& o) const { return V{x + o.x}; }
|
||||
V operator[](int i) const { return V{x + i}; }
|
||||
int get() const { return x; }
|
||||
};
|
||||
int plainCaller(const V& a) { return a.get(); }
|
||||
V explicitCaller(const V& a, const V& b) { return a.operator+(b); }
|
||||
V subscriptCaller(const V& a) { return a.operator[](3); }
|
||||
V pointerCaller(const V* p, const V& b) { return p->operator+(b); }
|
||||
`,
|
||||
});
|
||||
|
||||
expect(callerNamesOf('V::operator+')).toEqual(['explicitCaller', 'pointerCaller']);
|
||||
expect(callerNamesOf('V::operator[]')).toEqual(['subscriptCaller']);
|
||||
expect(callerNamesOf('V::get')).toEqual(['plainCaller']); // control: plain calls unaffected
|
||||
expect(callerNamesOf('Aaa::operator+')).toEqual([]);
|
||||
expect(callerNamesOf('Aaa::operator[]')).toEqual([]);
|
||||
});
|
||||
|
||||
it('resolves an out-of-line operator definition (declaration in header)', async () => {
|
||||
await indexCpp({
|
||||
'v.hpp': `#pragma once
|
||||
struct V { int x; V operator+(const V& o) const; };
|
||||
`,
|
||||
'v.cpp': `#include "v.hpp"
|
||||
V V::operator+(const V& o) const { return V{x + o.x}; }
|
||||
`,
|
||||
'app.cpp': `#include "v.hpp"
|
||||
V add(const V& a, const V& b) { return a.operator+(b); }
|
||||
`,
|
||||
});
|
||||
|
||||
expect(callerNamesOf('V::operator+')).toEqual(['add']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PHP chained static-factory call resolution (#608)', () => {
|
||||
function callerNamesOf(qualifiedName: string): string[] {
|
||||
const target = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
|
||||
|
||||
Reference in New Issue
Block a user