mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 19:30:10 +08:00
fix(cpp): a bare call on a parameter, local lambda or functor links nothing, not a function elsewhere of its name (#2708)
A C++ call written by its bare name 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 it before any function
or method of that name. The name strategies linked such calls by name:
protocolbuffers/protobuf's code generators' `format(...)` on their
`Formatter& format` parameter went to the conformance suite's
`TestResult::format`, and facebook/rocksdb's tests' local
`auto key = [](int i) {...};` calls to `ArenaWrappedDBIter::key`.
isCppLocalCallableCall, checked in matchReference beside the JS/TS rule,
reads the caller's declarations of the name: its parameters, a declaration
that starts a statement (`auto x = ...`, `T x = ...`, `T x(args)`, a later
name in a list), a range-for variable or a lambda's parameter, the nearest one
in a block still open at the call deciding. A local type or a function the
body declares is not a local callable (the call constructs it or calls the
function), and an expression that reads like a declaration (`a && f(x)`)
declares nothing. A local's type may carry a function type in its template
arguments (`std::function<void()> done, failed;`), so the first name of such
a list counts as the later ones do, and the #2698 test of that list now
expects no edge for either. The looser isCppLocalName used by implicit-this
is unchanged.
A/B on main 5530676f: protobuf 269 and rocksdb 51 edges removed, all calls
on locals, nothing else changed; glog, leveldb, breakpad and redis
byte-identical. The check costs 3.2 s of about 450 s CPU on protobuf.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
01cd287786
commit
90e2456b43
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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<string, string>): Promise<CodeGraph> {
|
||||
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 <string>',
|
||||
'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 <functional>',
|
||||
'#include <string>',
|
||||
'#include <vector>',
|
||||
'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<void(std::function<void(const char*)>)>& with) {',
|
||||
' with([&](auto append) {',
|
||||
' append("tail");',
|
||||
' });',
|
||||
'}',
|
||||
'void Loop(const std::vector<std::function<void()>>& 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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<void(int)>`, `::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<ResolutionContext, Map<string, CppLocalBinding[]>>();
|
||||
const CPP_CALLER_HEADS = new WeakMap<ResolutionContext, Map<string, { parameters: Set<string>; 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<string>; 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<string>(), 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<string>; 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<int>;`) 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<number, string>();
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user