Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,40 @@ func main() {
expect(callTargets('useB')).toContain(logB.id);
});

it('resolves an explicit a.operator+(b) call to the operator+ method (#1247 case 1)', async () => {
// The repro from #1247: a call to an operator-overload method in its
// explicit `a.operator+(b)` form never recorded a call edge. The `operator+`
// method node IS indexed (definition side is fine), but the resolver's
// method-call pattern `recv.method` used `\w` for the method segment, which
// excludes `+` — so `a.operator+` matched no pattern and `matchByExactName`
// couldn't bridge `a.operator+` to a node named `operator+`. With a receiver
// whose type is locally declared (`Vec a;`), the inference chain already
// works; only the pattern gate stopped it. This is the explicit-form case
// only — infix `a + b` and subscript `a[i]` parse as binary_expression /
// subscript_expression (not call_expression) and are tracked separately in
// #1258 (they need receiver type inference from an expression, not a name).
fs.writeFileSync(
path.join(tempDir, 'vec.cpp'),
'class Vec { public: Vec operator+(const Vec& o) const { return o; } };\n'
+ 'Vec add(Vec a, Vec b) { return a.operator+(b); }\n',
);

cg = await CodeGraph.init(tempDir, { index: true });
cg.resolveReferences();

const opPlus = cg.getNodesByKind('method').find((n) => n.name === 'operator+');
const add = cg.getNodesByKind('function').find((n) => n.name === 'add');
expect(opPlus).toBeDefined();
expect(add).toBeDefined();

const callTargets = cg
.getOutgoingEdges(add!.id)
.filter((e) => e.kind === 'calls')
.map((e) => e.target);
// `add` is reported as a caller of the `operator+` method — the missing edge.
expect(callTargets).toContain(opPlus!.id);
});

it('preferCallSiteFile puts same-file candidates first and is otherwise a no-op', () => {
const a = methodNode('m:a', 'a/svc.cpp');
const b = methodNode('m:b', 'b/svc.cpp');
Expand Down
83 changes: 83 additions & 0 deletions src/extraction/tree-sitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,56 @@ function extractName(node: SyntaxNode, source: string, extractor: LanguageExtrac
return extractor.recoverMangledName ? extractor.recoverMangledName(name) : name;
}

/**
* Recover the callee name for a C/C++ explicit operator-overload call —
* `a.operator+(b)`, `a.operator[](i)`, `a.operator==(b)`, … — from the ERROR
* node tree-sitter-cpp wraps the `.operator<sym>` member access in (see the
* guard in `extractCall`). Returns `operator+` for an unresolvable receiver
* (a simple-identifier-unique name still resolves via matchByExactName), or
* `recv.operator+` when the receiver is a plain identifier so the resolver can
* route it to the right class via receiver-type inference. Returns undefined
* when the call_expression carries no `operator_name`, so every other call shape
* falls through to the generic path unchanged.
*
* Safe by construction: it only acts when an `operator_name` actually exists in
* a child (ERROR or otherwise), and only ever emits `operator`-prefixed names —
* a name no ordinary call site produces — so it can't collide with real refs.
*/
function readCppOperatorCallCallee(node: SyntaxNode, source: string): string | undefined {
let operatorName: string | undefined;
for (let i = 0; i < node.namedChildCount; i++) {
const child = node.namedChild(i);
if (!child) continue;
// The `operator_name` is nested under an ERROR node tree-sitter-cpp emits
// around the `.operator<sym>` it can't parse; walk the subtree to find it.
const visit = (n: SyntaxNode | null): void => {
if (!n || operatorName) return;
if (n.type === 'operator_name') {
operatorName = getNodeText(n, source).trim();
return;
}
for (let j = 0; j < n.namedChildCount; j++) visit(n.namedChild(j));
};
visit(child);
if (operatorName) break;
}
if (!operatorName) return undefined;

// Qualify with a plain-identifier receiver when present, so the resolver can
// infer the receiver's type and pick the right `operator<sym>` overload.
// A non-identifier receiver (call result, pointer deref, …) has no static name
// to route through inference; emit the bare `operator<sym>` and let
// matchByExactName link it when the overload name is unique in scope.
const first = node.namedChild(0);
if (first && (first.type === 'identifier' || first.type === 'field_identifier')) {
const recv = getNodeText(first, source);
if (recv && !/^(self|this|super)$/.test(recv)) {
return `${recv}.${operatorName}`;
}
}
return operatorName;
}

function extractNameRaw(node: SyntaxNode, source: string, extractor: LanguageExtractor): string {
const hookName = extractor.resolveName?.(node, source);
if (hookName) return hookName;
Expand Down Expand Up @@ -3625,6 +3675,39 @@ export class TreeSitterExtractor {
const callerId = this.nodeStack[this.nodeStack.length - 1];
if (!callerId) return;

// C/C++ explicit operator-overload call: `a.operator+(b)`, `a.operator[](i)`,
// `a.operator==(b)` … tree-sitter-cpp can't parse the `.operator<sym>` member
// access and lands it in an ERROR node wrapping an `operator_name`, with the
// receiver as the call_expression's first named child and NO `function` field:
// call_expression
// identifier [a] ← namedChild(0) = receiver
// ERROR [.operator+] ← wraps operator_name
// operator_name [operator+]
// argument_list [(b)]
// The generic path below would take `namedChild(0)` (the receiver `a`) as the
// callee, emitting a useless `calls:"a"` ref and never linking the `operator+`
// method (#1247 case 1). Recover here: read the `operator_name` and qualify it
// with a simple-identifier receiver so the resolver routes it to the right
// class via receiver-type inference (same path as any other `recv.method()`).
// Gated to C/C++ — only their grammars produce this ERROR-wrapped shape, and
// the method-segment pattern in name-matcher is C/C++-only too. This covers
// ONLY the explicit form; infix `a + b` and subscript `a[i]` parse as
// binary_expression / subscript_expression (not call_expression) and need
// receiver type inference from an expression — tracked separately in #1258.
if ((this.language === 'cpp' || this.language === 'c') && node.type === 'call_expression') {
const opCallee = readCppOperatorCallCallee(node, this.source);
if (opCallee) {
this.unresolvedReferences.push({
fromNodeId: callerId,
referenceName: opCallee,
referenceKind: 'calls',
line: node.startPosition.row + 1,
column: node.startPosition.column,
});
return;
}
}

// VB.NET: `foo(args)` is syntactically ambiguous between a call and an
// index read, so the grammar parses non-empty parens as
// array_access_expression (field `array`, not `function`) — even Roslyn
Expand Down
19 changes: 17 additions & 2 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1459,8 +1459,23 @@ 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+:)*)$/);
const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
// The method segment is `\w+` with an optional ObjC selector tail (`:`s).
// C/C++ operator-overload call sites arrive as `recv.operator+` /
// `recv.operator[]` etc. (the explicit `a.operator+(b)` form, #1247 case 1);
// `+`/`[`/`(` aren't in `\w`, so a single `\w+` gate dropped them and the call
// edge to the `operator+` method node never resolved. Allow the method segment
// to also be `operator` + an overload-symbol tail for C/C++ only — the symbol
// set is the C++ overloadable operators; an unbounded `[^.]+` would over-match.
const cppOperatorMethod =
'(?:operator(?:[\\[\\]()+\\-*/%^&|~!<>=,]+|new|delete|new\\[\\]|delete\\[\\]))';
const methodSegment =
ref.language === 'cpp' || ref.language === 'c'
? `(?:\\w+:?(?:\\w+:)*|${cppOperatorMethod})`
: '\\w+:?(?:\\w+:)*';
const dotMatch = ref.referenceName.match(new RegExp(`^([\\w.]+)\\.(${methodSegment})$`));
const colonMatch = ref.referenceName.match(
new RegExp(`^(\\w+)::(${methodSegment})$`)
);
// Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
// Recognize these receiver/method separators so local-variable receiver-type
// inference (#1108) applies to them too — extraction already emits the ref in
Expand Down