diff --git a/CHANGELOG.md b/CHANGELOG.md index 44d0122a0..c0c23bf39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -229,6 +229,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Rust, a type alias, struct, enum or function declared without `pub` now links only from its own module and the modules inside it, and a name a `use` brings in from one of your workspace's crates, like `use tokio_test::task::spawn` or `use crate::error::Error`, links only to an item of that crate or of a crate it depends on. Before, a file's `use crate::Result` could link its `Result<…>` types to another module's private `type Result`, as in dioxus's CLI, and a call to an imported function could link to another crate's function of the same name, such as tokio's test `spawn(…)` calls to one in its examples, or to nothing because a private function of that name was in the way. Re-index Rust projects after upgrading. - In Rust, a name written on its own, like an `Err(…)` call or a `Response` return type, or through a module, like `io::Error` or `use crate::de::Error`, no longer links to a `type` or `const` declared inside an `impl`, such as a `FromStr` impl's `type Err` or a `Service` impl's `type Response`: only a path like `Self::Err` names those, and they are now listed under their type, as `ValueHint::Err`. Before, serde's and ripgrep's `Ok(…)` and `Err(…)` calls linked to a serializer's or parser's `type Ok` or `type Err`, axum's handlers' `Response` to a `Service` impl's, and ripgrep's `impl Captures for RegexCaptures` to another impl's `type Captures` instead of the `Captures` trait. Re-index Rust projects after upgrading. - In C++, every name a declaration lists now has the declaration's type, like `strings_found` in google/breakpad's `ByteBuffer symbols_found, strings_found;` or `data_` in a class's `Table *index_, *data_;`, so calls on it link to that class's methods. Before, only the first name had its type read, and a call on another name linked to nothing or to the class of an earlier variable with the same name. +- In C++, a call on a parameter, a local lambda or functor, a lambda's parameter or a range-`for` variable no longer links to a function or method elsewhere that only shares its name, like protocolbuffers/protobuf's code generators calling `format(…)` on their `Formatter& format` parameter, which linked to the conformance suite's `TestResult::format`, or facebook/rocksdb's tests calling a local `auto key = [](int i) {…};`. C++ finds the local first, so that is what the call calls. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/cpp-listed-declarator-receiver.test.ts b/__tests__/cpp-listed-declarator-receiver.test.ts index 4e426d930..f576d366f 100644 --- a/__tests__/cpp-listed-declarator-receiver.test.ts +++ b/__tests__/cpp-listed-declarator-receiver.test.ts @@ -274,7 +274,7 @@ describe('a C++ receiver declared later in a list of names', () => { it('a bare call on a name declared later in a list is a local\'s, as on the first name', async () => { // C++ lookup finds the local, not a member of the method's class, so a - // bare call on either name is left to the name strategies: `failed()` + // bare call on either name calls the local and links nothing: `failed()` // reached Runner::failed as a call on `this`, while `done()` was a local's. const cg = await indexed({ 'jobs/runner.h': lines( @@ -310,6 +310,7 @@ describe('a C++ receiver declared later in a list of names', () => { run.find((c) => c.startsWith(`jobs::Runner::${method} `))?.split('@')[1]; expect(run).not.toContain('jobs::Runner::failed @0.9'); expect(howLinked('failed')).toBe(howLinked('done')); + expect(run).toEqual([]); } finally { cg.close(); } diff --git a/__tests__/cpp-local-callable-calls.test.ts b/__tests__/cpp-local-callable-calls.test.ts new file mode 100644 index 000000000..f5d96f899 --- /dev/null +++ b/__tests__/cpp-local-callable-calls.test.ts @@ -0,0 +1,209 @@ +/** + * A C++ call written by its bare name, `format(…)`, on a name the calling + * function binds itself — a parameter, a lambda's parameter, a local lambda or + * functor, a range-`for` variable — calls that local. C++ lookup finds the + * local before any function or method of that name, so no project function or + * method is what it calls. + * + * protocolbuffers/protobuf's code generators take a `const Formatter& format` + * and call `format("…")`. The call went to the one other `format` the project + * has, the conformance suite's `TestResult::format`, by name. facebook/rocksdb's + * tests call local lambdas, `auto key = [](int i) {…};` then `key(10)`, and + * those went to `ArenaWrappedDBIter::key`. + * + * A name the function declares as a function (`void helper(int);`) or a type + * still links as before, and so does a local whose block closed before the + * call, or an expression that only reads like a declaration (`!force && + * MaybeInsertDummy(key)`). + */ +import { describe, it, expect, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import type { Node } from '../src/types'; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true }); +}); + +async function indexed(files: Record): Promise { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-local-callable-')); + roots.push(root); + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + return CodeGraph.init(root, { index: true }); +} + +function callable(cg: CodeGraph, qualifiedName: string): Node { + const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName); + if (!node) throw new Error(`no function or method ${qualifiedName}`); + return node; +} + +/** `calls` callees of a function or method, constructors aside, as `qualifiedName`, one per callee. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .filter((r) => { + const [owner, name] = r.node.qualifiedName.split('::').slice(-2); + return owner !== name; + }) + .map((r) => r.node.qualifiedName) + .sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** The project's other `format`, `key`, `append`, `fn` and `Value`: what a call on the local could be mistaken for. */ +const ELSEWHERE = { + 'conformance/testee.h': lines( + 'namespace conformance {', + 'class TestResult {', + ' public:', + ' int format() const { return 0; }', + '};', + 'class Iterator {', + ' public:', + ' int key() const { return 1; }', + '};', + 'class LeftoverBuffer {', + ' public:', + ' void append(const char* s) {}', + '};', + 'class Runner {', + ' public:', + ' void fn() {}', + '};', + '} // namespace conformance', + ), +}; + +describe('a C++ bare call on a name the calling function binds', () => { + it('reproduction: protobuf\'s `format(…)` on a `const Formatter& format` parameter is no TestResult::format', async () => { + const cg = await indexed({ + ...ELSEWHERE, + 'compiler/cpp/message.cc': lines( + '#include ', + 'namespace compiler {', + 'class Formatter {', + ' public:', + ' void operator()(const char* text) const {}', + '};', + 'class MessageGenerator {', + ' public:', + ' void GenerateFieldNames(const Formatter& format);', + '};', + 'void MessageGenerator::GenerateFieldNames(const Formatter& format) {', + ' format("static const char kNames[] = {");', + ' format("};");', + '}', + '} // namespace compiler', + ), + }); + try { + expect(calls(cg, 'compiler::MessageGenerator::GenerateFieldNames')).toEqual([]); + expect(cg.getCallers(callable(cg, 'conformance::TestResult::format').id).filter((r) => r.edge.kind === 'calls')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a local lambda, a local functor, a lambda\'s parameter and a range-for variable', async () => { + const cg = await indexed({ + ...ELSEWHERE, + 'db/iter_test.cc': lines( + '#include ', + '#include ', + '#include ', + 'namespace db {', + 'class Formatter {', + ' public:', + ' explicit Formatter(int indent) {}', + ' void operator()(const char* text) const {}', + '};', + 'int Lambda() {', + ' auto key = [](int i) { return i * 2; };', + ' return key(10) + key(90);', + '}', + 'void Functor(int indent) {', + ' Formatter format(indent);', + ' format("x");', + '}', + 'void LambdaParameter(const std::function)>& with) {', + ' with([&](auto append) {', + ' append("tail");', + ' });', + '}', + 'void Loop(const std::vector>& callbacks) {', + ' for (auto& fn : callbacks) fn();', + '}', + '} // namespace db', + ), + }); + try { + expect(calls(cg, 'db::Lambda')).toEqual([]); + expect(calls(cg, 'db::Functor')).toEqual([]); + expect(calls(cg, 'db::LambdaParameter')).toEqual([]); + expect(calls(cg, 'db::Loop')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a function the body declares, a local out of scope and an expression still link', async () => { + const cg = await indexed({ + 'cache/helpers.h': lines( + 'namespace cache {', + 'int helper(int n);', + 'int key(int i);', + '} // namespace cache', + ), + 'cache/helpers.cc': lines( + '#include "cache/helpers.h"', + 'namespace cache {', + 'int helper(int n) { return n; }', + 'int key(int i) { return i; }', + '} // namespace cache', + ), + 'cache/secondary_cache.cc': lines( + '#include "cache/helpers.h"', + 'namespace cache {', + 'int Declared() {', + ' int helper(int n);', + ' return helper(1);', + '}', + 'int OutOfScope(bool shadow) {', + ' if (shadow) {', + ' auto key = [](int i) { return -i; };', + ' return key(1);', + ' }', + ' return key(2);', + '}', + 'class SecondaryCache {', + ' public:', + ' bool Insert(int k, bool force_insert);', + ' bool MaybeInsertDummy(int k) { return k > 0; }', + '};', + 'bool SecondaryCache::Insert(int k, bool force_insert) {', + ' if (!force_insert && MaybeInsertDummy(k)) {', + ' return false;', + ' }', + ' return true;', + '}', + '} // namespace cache', + ), + }); + try { + expect(calls(cg, 'cache::Declared')).toEqual(['cache::helper']); + expect(calls(cg, 'cache::OutOfScope')).toEqual(['cache::key']); + expect(calls(cg, 'cache::SecondaryCache::Insert')).toEqual(['cache::SecondaryCache::MaybeInsertDummy']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index b9207d375..3d43d4504 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -10730,7 +10730,11 @@ function cppDeclaratorsAfterFirst(code: string, start: number, inHeader: boolean * brace, or opens a `for`, `if` or `switch` header. */ function matchCppListedDeclarator(line: string, receiverName: string, listedRegex: RegExp): RegExpExecArray | null { - if (!listedRegex.test(line)) return null; + return listedRegex.test(line) ? cppListedDeclaratorOn(line, receiverName) : null; +} + +/** matchCppListedDeclarator without its prefilter, for a line already known to name the receiver after a comma. */ +function cppListedDeclaratorOn(line: string, receiverName: string): RegExpExecArray | null { for (let i = -1; i < line.length; i++) { const c = line[i]; const header = c === '(' && /^(?:for|if|switch)$/.test(cppWordBefore(line, i)); @@ -13347,6 +13351,243 @@ function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context: return false; } +// What may open a local declaration before its type or `auto`. +const CPP_LOCAL_SPECIFIERS = String.raw`(?:(?:const|constexpr|static|volatile|thread_local|register)\s+)*`; +// A type a local is declared with, as written: `ns::Formatter`, `std::function`, `::Foo`. Its +// template arguments may hold a function type's parentheses, which CPP_TEMPLATE_ARGUMENTS leaves out. +const CPP_LOCAL_TEMPLATE_ARGUMENTS = String.raw`<(?:[^<>;={}]|<(?:[^<>;={}]|<[^<>;={}]*>)*>)*>`; +const CPP_LOCAL_TYPE = String.raw`(?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*(?:\s*${CPP_LOCAL_TEMPLATE_ARGUMENTS}(?:\s*::\s*[A-Za-z_]\w*)*)?`; +// Where a statement starts on a line of code: the line's start, or after `;`, `{`, `}`, or a `for`/`if` header's `(`. +const CPP_STATEMENT_START = String.raw`(?:^|[;{}]|\b(?:for|if)\s*\()\s*`; +// Declarations that start a statement, the declared name captured: `auto key = …`, `Formatter format(p);`, +// `const Formatter& format = …`, and the local types `using V = …;`, `typedef … V;`, `struct Getter {`. +const CPP_AUTO_LOCAL = new RegExp(String.raw`${CPP_STATEMENT_START}${CPP_LOCAL_SPECIFIERS}auto(?:\s+|\s*[*&]+\s*)([A-Za-z_]\w*)\s*(?:=|\{|\()`, 'g'); +const CPP_TYPED_LOCAL = new RegExp(String.raw`${CPP_STATEMENT_START}${CPP_LOCAL_SPECIFIERS}(${CPP_LOCAL_TYPE})(\s+|\s*[*&]+\s*)(?:const\s+)?([A-Za-z_]\w*)\s*([=({;[,])`, 'g'); +const CPP_LOCAL_TYPE_DECLARATION = new RegExp(String.raw`${CPP_STATEMENT_START}(?:using\s+([A-Za-z_]\w*)\s*=|typedef\b[^;]*?\b([A-Za-z_]\w*)\s*;|(?:struct|class|union|enum)(?:\s+class)?\s+([A-Za-z_]\w*)\b)`, 'g'); +// A lambda's head, its parameters captured: `[&](const Foo& f) mutable -> bool {`. +const CPP_LAMBDA_HEAD = /\]\s*\(([^()]*)\)\s*(?:mutable\s*)?(?:->[^{;]*)?\{/g; + +/** Does `text` hold `name` as a whole word? */ +function cppHasWord(text: string, name: string): boolean { + for (let at = text.indexOf(name); at >= 0; at = text.indexOf(name, at + 1)) { + if (!/\w/.test(text[at - 1] ?? '') && !/\w/.test(text[at + name.length] ?? '')) return true; + } + return false; +} + +/** A declaration of a name in a C++ function's own code, at a 0-based line. */ +interface CppLocalBinding { + line: number; + /** + * What the name is from there: a parameter (in scope in the whole body), a + * variable, or a type or a function the body declares, which a call + * constructs or calls instead. + */ + kind: 'parameter' | 'variable' | 'type' | 'function'; + /** Where its scope starts: the statement's column, or just inside the `{` of a lambda whose parameter it is. */ + column: number; + /** For a range-`for` variable: the column of the loop header's `(`, whose body is its scope. */ + loopParen?: number; +} + +const CPP_LOCAL_BINDINGS = new WeakMap>(); +const CPP_CALLER_HEADS = new WeakMap; body: number }>>(); + +/** + * The names a C++ parameter list declares: `const Formatter& format, int n + * = 0` → `format`, `n`. A parameter without a name (`const Foo&`, `int`, + * `const Foo`) declares none. + */ +function cppParameterNames(list: string): string[] { + const names: string[] = []; + for (const parameter of splitCppTopLevel(list)) { + const declarator = parameter.replace(/=[^]*$/, '').trim(); + const m = /([A-Za-z_]\w*)\s*(?:\[[^\]]*\])*$/.exec(declarator); + if (!m) continue; + const before = declarator.slice(0, m.index).replace(/\b(?:const|volatile|struct|class|enum|union|typename|signed|unsigned)\b/g, '').trim(); + if (before !== '' && !/::\s*$/.test(before)) names.push(m[1]!); + } + return names; +} + +/** The text inside the balanced `(…)` opening at line `line`, column `column`, of `codeLine`'s lines, within a dozen lines; null when it doesn't close. */ +function cppParenTextFrom(codeLine: (line: number) => string, line: number, column: number): string | null { + let depth = 0; + let text = ''; + for (let l = line; l < line + 12; l++) { + const code = codeLine(l); + for (let c = l === line ? column : 0; c < code.length; c++) { + const ch = code[c]!; + if (ch === '(') depth++; + else if (ch === ')' && --depth === 0) return text.slice(1); + text += ch; + } + text += ' '; + } + return null; +} + +/** Does a `T name(args)` local's argument list declare parameters — `void helper(int);`, `Foo make();`, `Foo bar(Baz qux);` — so it declares a function? */ +function cppDeclaresLocalFunction(type: string, args: string): boolean { + if (type === 'void' || args.trim() === '') return true; + return splitCppTopLevel(args).some((a) => + /^(?:int|char|bool|float|double|long|short|unsigned|signed|void|size_t)$/.test(a) || + (/^(?:(?:const|volatile|unsigned|signed)\s+)*[A-Za-z_][\w:]*(?:\s*<[^>]*>)?\s*[*&]*\s*(?:[A-Za-z_]\w*)?$/.test(a) && /\s|[*&]/.test(a))); +} + +/** + * The C++ function `caller`'s parameter names, read from the list after its + * own name on its head, and the line (0-based) its head's name is on, where + * its body's code begins to be read. + */ +function cppCallerHead(caller: Node, codeLine: (line: number) => string, lineCount: number, context: ResolutionContext): { parameters: Set; body: number } { + let memo = CPP_CALLER_HEADS.get(context); + if (!memo) CPP_CALLER_HEADS.set(context, (memo = new Map())); + const hit = memo.get(caller.id); + if (hit) return hit; + const first = caller.startLine - 1; + let head = { parameters: new Set(), body: first }; + const named = new RegExp(`\\b${caller.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`); + for (let l = first; l < Math.min(lineCount, first + 13); l++) { + const at = named.exec(codeLine(l)); + if (!at) continue; + const list = cppParenTextFrom(codeLine, l, at.index + at[0].length - 1); + head = { parameters: new Set(list === null ? [] : cppParameterNames(list)), body: l }; + break; + } + memo.set(caller.id, head); + return head; +} + +/** + * The declarations of `name` in the C++ function `caller`'s own code, read + * through `codeLine` (a 0-based line as code): a parameter, then each + * statement that declares the name — `auto key = [](int i) {…};`, + * `Formatter format(p);`, `const Formatter& format = …`, a later name in a + * list, a range-`for` variable, a lambda's parameter. A local type (`using V + * = …;`, `struct Getter {`) or a function declared in the body (`void + * helper(int);`) is recorded as what the name is there. An expression that + * reads like a declaration — `a && f(x)`, `a * f(x)` — declares nothing, and + * neither does a line that continues one from the line above. + */ +function cppLocalBindings( + name: string, + caller: Node, + head: { parameters: Set; body: number }, + codeLine: (line: number) => string, + rawLines: readonly string[], +): CppLocalBinding[] { + const first = caller.startLine - 1; + const last = Math.min(rawLines.length - 1, (caller.endLine ?? caller.startLine) - 1); + const bindings: CppLocalBinding[] = head.parameters.has(name) ? [{ line: head.body, column: 0, kind: 'parameter' }] : []; + // Whether the code above line `l` ends a statement, so a declaration may start the line. + const startsStatement = (l: number): boolean => { + for (let k = l - 1; k >= first; k--) { + const above = codeLine(k); + if (above.trim() !== '') return /[;{}:]\s*$/.test(above) || /^\s*#/.test(above) || k < head.body; + } + return true; + }; + for (let l = head.body; l <= last; l++) { + // (Blanking literals and comments only takes text away: a line without the name raw has none as code.) + if (!cppHasWord(rawLines[l] ?? '', name)) continue; + const text = codeLine(l); + if (!cppHasWord(text, name)) continue; + const starts = (index: number): boolean => index > 0 || l === head.body || startsStatement(l); + for (const m of text.matchAll(CPP_LOCAL_TYPE_DECLARATION)) { + if ((m[1] ?? m[2] ?? m[3]) === name && starts(m.index)) bindings.push({ line: l, column: m.index, kind: 'type' }); + } + for (const m of text.matchAll(CPP_AUTO_LOCAL)) { + if (m[1] === name && starts(m.index)) bindings.push({ line: l, column: m.index, kind: 'variable' }); + } + for (const m of text.matchAll(CPP_TYPED_LOCAL)) { + const type = m[1]!; + if (m[3] !== name || !starts(m.index) || CPP_NOT_A_DECLARED_TYPE.has(type.split(/[\s<:]/)[0]!) || + /^(?:using|typedef|struct|class|union|enum|auto)$/.test(type)) continue; + let kind: CppLocalBinding['kind'] = 'variable'; + if (m[4] === '(') { + // `a && f(x)` and `a * f(x)` are expressions. + if (/[*&]/.test(m[2]!)) continue; + if (cppDeclaresLocalFunction(type, cppParenTextFrom(codeLine, l, m.index + m[0].length - 1) ?? '')) kind = 'function'; + } + bindings.push({ line: l, column: m.index, kind }); + } + if (text.includes(',')) { + const m = cppListedDeclaratorOn(text, name); + if (m && starts(m.index)) bindings.push({ line: l, column: m.index, kind: 'variable' }); + } + const loop = cppRangeForDeclaration(text, name); + if (loop) bindings.push({ line: l, column: loop.paren, kind: 'variable', loopParen: loop.paren }); + for (const m of text.matchAll(CPP_LAMBDA_HEAD)) { + if (cppParameterNames(m[1]!).includes(name)) bindings.push({ line: l, column: m.index + m[0].length, kind: 'variable' }); + } + } + return bindings; +} + +/** Is the code at (`toLine`, `toColumn`) inside the brace block open at (`line`, `column`) — no `}` between them closes it? */ +function cppBlockReaches(codeLine: (line: number) => string, line: number, column: number, toLine: number, toColumn: number): boolean { + let depth = 0; + for (let l = line; l <= toLine; l++) { + const text = codeLine(l); + const end = l === toLine ? Math.min(toColumn, text.length) : text.length; + for (let c = l === line ? column : 0; c < end; c++) { + if (text[c] === '{') depth++; + else if (text[c] === '}' && --depth < 0) return false; + } + } + return true; +} + +/** + * Whether a C++ call written by its bare name calls a local of the calling + * function — a parameter, a lambda's parameter, a range-`for` variable, or a + * variable declared above the call in a block still open there — which C++ + * lookup finds before any function or method of that name. protobuf's code + * generators call `format(…)` on a `const Formatter& format` parameter, + * which linked to the conformance suite's `TestResult::format`, and rocksdb's + * tests call local lambdas (`auto key = [](int i) {…};`) that linked to + * `ArenaWrappedDBIter::key`. A name the function declares as a type (`using + * V = std::vector;`) or a function (`void helper(int);`) is not one: + * that call constructs the type or calls the function. + */ +function isCppLocalCallableCall(ref: UnresolvedRef, context: ResolutionContext): boolean { + const name = ref.referenceName; + if (!/^[A-Za-z_]\w*$/.test(name) || cppImplicitReceiver(ref, context) !== 'bare') return false; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!caller || (caller.kind !== 'function' && caller.kind !== 'method') || caller.filePath !== ref.filePath || + caller.name === name || ref.line < caller.startLine || ref.line > (caller.endLine ?? caller.startLine)) return false; + const rawLines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/); + if (!rawLines) return false; + const nonCode = cppNonCodeLines(ref.filePath, context); + const codeLines = new Map(); + const codeLine = (l: number): string => { + let code = codeLines.get(l); + if (code === undefined) codeLines.set(l, (code = cppCodeOf(cppLiteralsBlanked(rawLines[l] ?? '', l + 1, nonCode)) ?? '')); + return code; + }; + let memo = CPP_LOCAL_BINDINGS.get(context); + if (!memo) CPP_LOCAL_BINDINGS.set(context, (memo = new Map())); + const key = `${caller.id}\u0000${name}`; + let bindings = memo.get(key); + if (!bindings) { + const head = cppCallerHead(caller, codeLine, rawLines.length, context); + memo.set(key, (bindings = cppLocalBindings(name, caller, head, codeLine, rawLines))); + } + const callLine = ref.line - 1; + // The nearest declaration above the call whose scope holds it decides. + let decided: CppLocalBinding | null = null; + for (const b of bindings) { + if (b.line > callLine || (b.line === callLine && b.column >= ref.column)) continue; + const inScope = b.kind === 'parameter' || + (b.loopParen !== undefined + ? cppForBodyEncloses(rawLines, b.line, b.loopParen, callLine, ref.column) + : cppBlockReaches(codeLine, b.line, b.column, callLine, ref.column)); + if (inScope && (!decided || b.line > decided.line || (b.line === decided.line && b.column > decided.column))) decided = b; + } + return decided?.kind === 'parameter' || decided?.kind === 'variable'; +} + /** * A C++ source line as code, for reading declarations: null for a `// …` * line, else the line with its comments blanked, columns kept. Receiver @@ -14448,6 +14689,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_TYPE_DECLS.delete(context); CPP_INCLUDE_LINES.delete(context); CPP_UNNAMED_NAMESPACES.delete(context); + CPP_LOCAL_BINDINGS.delete(context); + CPP_CALLER_HEADS.delete(context); RUST_TRAIT_IMPL_MEMO.delete(context); RUST_USES.delete(context); RUST_CRATES.delete(context); @@ -19348,6 +19591,17 @@ export function matchReference( // selected = useStore(s => s.reset); selected()` is the store's `reset`.) if (target && target.name === ref.referenceName && isOutsideJsLocal(target, ref, context)) return null; } + // So does one a C++ function binds — a parameter, a lambda's parameter, a + // local lambda or functor: protobuf's generators' `format(…)` on a `const + // Formatter& format` is no `TestResult::format`. (Something the function + // itself declares, which the index may hold, is what the name is.) + if (result && ref.language === 'cpp' && ref.referenceKind === 'calls') { + const target = context.getNodeById?.(result.targetNodeId); + const caller = context.getNodeById?.(ref.fromNodeId); + const own = target && caller && target.filePath === caller.filePath && + target.startLine >= caller.startLine && target.startLine <= (caller.endLine ?? caller.startLine); + if (!own && isCppLocalCallableCall(ref, context)) return null; + } // R looks a call's name up among FUNCTIONS only, skipping other bindings: // ggplot2's tests' `c <- data_frame(b = 3)` is never what `c(1, 2)` calls — // base R's `c` is. (A project binding made by a function factory, ggplot2's