From aec4f43bbc42ca96e91834c6ae08c7aa1e5c016d Mon Sep 17 00:00:00 2001 From: aznikline Date: Sun, 12 Jul 2026 12:05:24 +0800 Subject: [PATCH] fix(extract): resolve explicit C++ operator-overload calls (#1247 case 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the explicit-form half of #1247. tree-sitter-cpp can't parse the `.operator` member access in `a.operator+(b)` / `a.operator[](i)` / `a.operator==(b)` 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 — so the extractor took the receiver as the callee (`calls:"a"`) and the `operator+` method node never gained a caller. Two-part recovery, both C/C++-only: - extractor: read the `operator_name` out of the ERROR subtree and qualify it with a simple-identifier receiver (`a.operator+`) so receiver-type inference routes it to the right overload; bare `operator+` fallback for non-identifier receivers resolves via exact-name match when unique. - resolver: widen the `recv.method` / `Class::method` method-segment gate to also accept `operator` + an overload-symbol tail; `+`/`[`/`(` aren't in \w so the old pattern dropped these refs before inference could run. 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. Test: `resolves an explicit a.operator+(b) call to the operator+ method`. Full suite green (2363 tests). --- __tests__/resolution.test.ts | 34 ++++++++++++++ src/extraction/tree-sitter.ts | 83 ++++++++++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 19 +++++++- 3 files changed, 134 insertions(+), 2 deletions(-) diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 4d440e6dc..9f045cc72 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -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'); diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index e6520cc0f..8bed72a6c 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -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` 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` 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` overload. + // A non-identifier receiver (call result, pointer deref, …) has no static name + // to route through inference; emit the bare `operator` 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; @@ -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` 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 diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 075bb10bf..24a6aafc9 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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