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
@@ -4250,6 +4250,54 @@ export class TreeSitterExtractor {
|
||||
} else {
|
||||
const func = getChildByField(node, 'function') || node.namedChild(0);
|
||||
|
||||
// C++ explicit operator call `a.operator+(b)` / `p->operator+(b)` (#1247):
|
||||
// tree-sitter-cpp can't parse an operator_name in field position, so the
|
||||
// callee is NOT a field_expression — the call_expression carries
|
||||
// `function: <receiver>` plus an ERROR child wrapping the operator_name.
|
||||
// Reading the function field alone yields just the receiver (`a`), an
|
||||
// unresolvable ref. Recover `<receiver>.operator+` so it resolves like any
|
||||
// other member call (matchMethodCall admits the operator method part).
|
||||
// The infix forms `a + b` / `a[i]` need receiver type inference and are
|
||||
// tracked separately (#1258).
|
||||
if (this.language === 'cpp' && func) {
|
||||
let operatorName = '';
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child?.type !== 'ERROR') continue;
|
||||
const op = child.namedChildren.find((c: SyntaxNode) => c.type === 'operator_name');
|
||||
if (op) { operatorName = getNodeText(op, this.source); break; }
|
||||
}
|
||||
if (operatorName) {
|
||||
// Call sites may space the symbolic name (nlohmann/json's
|
||||
// `it.operator * ()`, `other.operator < (*this)`) while definitions
|
||||
// index compact (`operator*`) — normalize so they match. The word
|
||||
// forms (`operator new`) keep their space.
|
||||
const sym = operatorName.slice('operator'.length).trim();
|
||||
if (/^[^\w\s]/.test(sym)) operatorName = `operator${sym.replace(/\s+/g, '')}`;
|
||||
// `->` receivers resolve identically to `.` ones. A receiver that
|
||||
// isn't a simple identifier/member chain (`(*it)`, a call result, …)
|
||||
// can't aid type inference, and a bare operator name would fall
|
||||
// through to exact-name matching — which GUESSES among the many
|
||||
// same-named operators (on nlohmann/json it linked a std::map
|
||||
// `object->operator[]` call to an unrelated in-repo operator[]).
|
||||
// Drop the ref: a silent miss, never a wrong edge. `this->` keeps
|
||||
// the bare name, matching how `this.method()` calls are emitted —
|
||||
// the target is on the enclosing class, where exact-name's same-file
|
||||
// preference is reliable.
|
||||
const receiver = getNodeText(func, this.source).replace(/->/g, '.').replace(/\s+/g, '');
|
||||
if (receiver !== 'this' && !/^[A-Za-z_][\w.]*$/.test(receiver)) return;
|
||||
const calleeName = receiver === 'this' ? operatorName : `${receiver}.${operatorName}`;
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: callerId,
|
||||
referenceName: calleeName,
|
||||
referenceKind: 'calls',
|
||||
line: node.startPosition.row + 1,
|
||||
column: node.startPosition.column,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (func) {
|
||||
if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression' || func.type === 'navigation_expression' || func.type === 'field_expression') {
|
||||
// Method call: obj.method() or obj.field.method()
|
||||
|
||||
@@ -1459,7 +1459,18 @@ export function matchMethodCall(
|
||||
// (with its existing single-candidate / receiver-overlap guards). Without this
|
||||
// a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
|
||||
// `Guard.Against.X()`) matched no pattern and never resolved.
|
||||
const dotMatch = ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/);
|
||||
// C++ explicit operator call `a.operator+(b)` reaches the resolver as
|
||||
// `a.operator+` (#1247) — the operator's symbol chars (`+`, `==`, `[]`, `()`)
|
||||
// fail the \w method part of the plain pattern, so admit them explicitly.
|
||||
// Names like `operatorTable` stay on the plain pattern (tried first); the
|
||||
// operator form requires at least one non-word char after `operator`, and
|
||||
// every downstream strategy compares the method part by exact string
|
||||
// equality, so a stray match can't invent an edge.
|
||||
const dotMatch =
|
||||
ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/) ??
|
||||
(ref.language === 'cpp'
|
||||
? ref.referenceName.match(/^([\w.]+)\.(operator[^\w\s.]+)$/)
|
||||
: null);
|
||||
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
|
||||
// Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
|
||||
// Recognize these receiver/method separators so local-variable receiver-type
|
||||
|
||||
Reference in New Issue
Block a user