fix(cpp): every name a declaration lists has its type, like strings_found in ByteBuffer symbols_found, strings_found; (#2698)

C++ receiver inference read a declaration with a regex that wants the type
right before the name. A declaration that lists several names writes the type
once, before the first, so the later names were never read: google/breakpad's
`strings_found.Size()` after `ByteBuffer symbols_found, strings_found;`
linked nothing, and a later name could take the type of an earlier variable
of the same name further up.

matchCppListedDeclarator reads such a declaration from its start (specifiers,
the type with its template arguments paired, then each declarator with its
own pointer and reference marks, qualifiers, annotations, array bounds and
initializer) and gives a later name the declaration's type plus its own `*`s
and `&`s: `Foo *a, b;` declares one pointer. Names are told apart only by
commas at the statement's own depth, so template arguments, a call's or a
constructor's arguments, an initializer list or an expression list no name.
It runs only where the declarator regex finds nothing on a line, in all three
declaration readers: the receiver scan (and its header scan), a class's data
members (`Table *index_, *data_;`), and the check that a bare call names a
local.

A/B on main 036832d3 with current upstream clones: breakpad +30 added,
5 overload retargets wrong to right, 66 guesses now typed; protobuf +97,
1 retarget wrong to right, 9 removed (5 wrong, 4 single-candidate guesses on
message classes the C++ index lacks, now like the first name in their list);
rocksdb +43, 5 wrong removed; leveldb +4. glog and redis byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-20 01:52:07 +00:00
committed by GitHub
parent ed8957710f
commit 204315274e
3 changed files with 614 additions and 4 deletions
+1
View File
@@ -213,6 +213,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Swift, a call on a local created through `try`, `try!`, `try?` or `await`, or with a generic type's constructor, now links to that type's method: after `let storage = try! DiskStorage.Backend<String>(config: config)`, `storage.store(…)` links to `Backend`'s `store`. Before, such a local had no known type, so the call was left unlinked or matched by name to another type's method. Re-index Swift projects after upgrading.
- In Rust, a call like `String::from("…")`, `Vec::from([…])` or `NonNull::from(x)` no longer links to your project's own `impl From<YourType> for String` (or for `Vec`, `NonNull`, …) unless what it passes is a `YourType`. A method call on a standard-library value, like `files.iter()` or `buf.extend(…)`, links to your trait's implementation for that type only where the trait is imported. Before, every `String::from("…")` call in dioxus's CLI linked to one such conversion, and tokio's `vec.iter()` calls to an internal trait's `Vec` impl. Re-index Rust projects after upgrading.
- 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 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.
## [1.6.2] - 2026-10-03
@@ -0,0 +1,363 @@
/**
* A C++ declaration that lists several names — `ByteBuffer symbols_found,
* strings_found;` — gives every one of them its type.
*
* google/breakpad's macho_reader_unittest.cc declares `ByteBuffer
* symbols_found, strings_found;` and calls `symbols_found.Size()` and
* `strings_found.Size()`. Receiver inference reads a declaration with a
* pattern that wants the type right before the name, and a list writes the
* type once, before the first name. So only `symbols_found` was typed:
* `strings_found.Size()` went on to a guess by the receiver's name, which
* found nothing to tell ByteBuffer's `Size` from test_assembler.h's
* `Section::Size`, or, when an earlier function declared a variable of the
* same name, took that variable's type.
*
* A name later in a list has the type the declaration opens with, plus its
* own `*`s and `&`s: `T *a, *b;` declares two pointers, `T *a, b;` one. The
* names are told apart by the commas between them only: a comma inside
* template arguments (`std::map<K, V> m;`), a call's or a constructor's
* arguments, a constructor's initializer list or an expression lists no
* name, and the receiver's own declaration further up still decides.
*/
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-listed-'));
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 @confidence`, one per callee. A guess can land on the right
* method by elimination, so the confidence shows how the edge was made: 0.9
* is the declared type's.
*/
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} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** breakpad's ByteBuffer, and test_assembler.h's Section with a `Size` of its own. */
const BREAKPAD = {
'src/common/byte_cursor.h': lines(
'namespace google_breakpad {',
'class ByteBuffer {',
' public:',
' unsigned Size() const { return end - start; }',
' const unsigned char* start;',
' const unsigned char* end;',
'};',
'} // namespace google_breakpad',
),
'src/common/test_assembler.h': lines(
'namespace google_breakpad {',
'namespace test_assembler {',
'class Section {',
' public:',
' unsigned Size() const { return size_; }',
' private:',
' unsigned size_;',
'};',
'} // namespace test_assembler',
'} // namespace google_breakpad',
),
};
/** Three classes with a `Get` each, so a receiver given the wrong one of them goes to the wrong `Get`. */
const STORE = {
'store/table.h': lines(
'namespace store {',
'class Table {',
' public:',
' Table() {}',
' explicit Table(int shard) {}',
' static Table Open(int shard) { return Table(shard); }',
' int Get(int key) const { return key; }',
'};',
'class Log {};',
'class Writer {',
' public:',
' Writer(Log* log, const Table& table, int level) {}',
' int Get(int key) const { return 0; }',
'};',
'template <typename K, typename V>',
'class Cache {',
' public:',
' V* Get(const K& key) { return nullptr; }',
'};',
'Table Make(int shard, int replicas);',
'Writer Wrap(Log* log, const Table& table, int level);',
'void Use(const Writer& writer, const Table& table, int level);',
'} // namespace store',
),
};
describe('a C++ receiver declared later in a list of names', () => {
it('reproduction: `ByteBuffer symbols_found, strings_found;` types strings_found too', async () => {
const cg = await indexed({
...BREAKPAD,
'src/common/mac/macho_reader_unittest.cc': lines(
'#include "common/byte_cursor.h"',
'#include "common/test_assembler.h"',
'namespace google_breakpad {',
'unsigned Symtab32() {',
' ByteBuffer symbols_found, strings_found;',
' return symbols_found.Size();',
'}',
'unsigned Symtab64() {',
' ByteBuffer symbols_found, strings_found;',
' return strings_found.Size();',
'}',
'} // namespace google_breakpad',
),
});
try {
expect(calls(cg, 'google_breakpad::Symtab32')).toEqual(['google_breakpad::ByteBuffer::Size @0.9']);
expect(calls(cg, 'google_breakpad::Symtab64')).toEqual(['google_breakpad::ByteBuffer::Size @0.9']);
expect(cg.getCallers(callable(cg, 'google_breakpad::test_assembler::Section::Size').id)
.filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});
it('initializers, braces and parentheses, template arguments, `auto`, a `for` header and a member list', async () => {
const cg = await indexed({
...STORE,
'store/store.h': lines(
'#include "store/table.h"',
'namespace store {',
'class Store {',
' public:',
' int Flush();',
' private:',
' Table *index_, *data_;',
'};',
'} // namespace store',
),
'store/store.cc': lines(
'#include "store/store.h"',
'namespace store {',
'int Store::Flush() {',
' return data_->Get(1);',
'}',
'int Initialized() {',
' Table first = Make(1, 2), table;',
' return table.Get(1);',
'}',
'int Constructed() {',
' Table first{1}, second(2);',
' return second.Get(2);',
'}',
'int Templated() {',
' Cache<int, Writer> writers, table;',
' return *table.Get(3);',
'}',
'int Deduced() {',
' auto first = Table::Open(1), second = Table::Open(2);',
' return second.Get(4);',
'}',
'int Looped(Table* head) {',
' int sum = 0;',
' for (Table *t = head, *u = nullptr; t; t = u) {',
' sum += u->Get(5);',
' }',
' return sum;',
'}',
'} // namespace store',
),
});
try {
expect(calls(cg, 'store::Store::Flush')).toEqual(['store::Table::Get @0.9']);
expect(calls(cg, 'store::Initialized')).toContain('store::Table::Get @0.9');
expect(calls(cg, 'store::Constructed')).toEqual(['store::Table::Get @0.9']);
// The comma inside `Cache<int, Writer>` separates no names: `table` is a Cache, not a Writer.
expect(calls(cg, 'store::Templated')).toEqual(['store::Cache::Get @0.9']);
// Each name in an `auto` list has its own initializer's type.
expect(calls(cg, 'store::Deduced')).toContain('store::Table::Get @0.9');
expect(calls(cg, 'store::Looped')).toEqual(['store::Table::Get @0.9']);
expect(cg.getCallers(callable(cg, 'store::Writer::Get').id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});
it('the nearest declaration decides, not one of the same name in an earlier function', async () => {
const cg = await indexed({
...STORE,
'store/scan.cc': lines(
'#include "store/table.h"',
'namespace store {',
'int Earlier(const Writer& table) {',
' return table.Get(1);',
'}',
'int Later() {',
' Table first, table;',
' return table.Get(2);',
'}',
'} // namespace store',
),
});
try {
expect(calls(cg, 'store::Earlier')).toEqual(['store::Writer::Get @0.9']);
expect(calls(cg, 'store::Later')).toEqual(['store::Table::Get @0.9']);
} finally {
cg.close();
}
});
it('a pointer is each name\'s own: `std::string *out, text;` and `std::string text, *out;`', async () => {
// A call on a std::string is the library's own member, and #2413's check
// that says so needs the call's `.` or `->` to fit the declaration:
// `text` is no pointer in either list, `out` is one in both.
const cg = await indexed({
'util/buffers.h': lines(
'namespace util {',
'class LeftoverBuffer {',
' public:',
' void append(const char* s, unsigned n) {}',
'};',
'} // namespace util',
),
'util/encode.cc': lines(
'#include <string>',
'#include "util/buffers.h"',
'namespace util {',
'void Encode(const char* data) {',
' std::string *out, text;',
' text.append(data, 4);',
'}',
'void EncodeTo(const char* data) {',
' std::string text, *out;',
' out->append(data, 4);',
'}',
'} // namespace util',
),
});
try {
expect(calls(cg, 'util::Encode')).toEqual([]);
expect(calls(cg, 'util::EncodeTo')).toEqual([]);
} finally {
cg.close();
}
});
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()`
// reached Runner::failed as a call on `this`, while `done()` was a local's.
const cg = await indexed({
'jobs/runner.h': lines(
'#include <functional>',
'namespace jobs {',
'class Runner {',
' public:',
' void Run();',
' void done() {}',
' void failed() {}',
'};',
'class Job {',
' public:',
' void done() {}',
' void failed() {}',
'};',
'} // namespace jobs',
),
'jobs/runner.cc': lines(
'#include "jobs/runner.h"',
'namespace jobs {',
'void Runner::Run() {',
' std::function<void()> done, failed;',
' done();',
' failed();',
'}',
'} // namespace jobs',
),
});
try {
const run = calls(cg, 'jobs::Runner::Run');
const howLinked = (method: string): string | undefined =>
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'));
} finally {
cg.close();
}
});
it('a comma in arguments, an initializer list or an expression lists no name', async () => {
const cg = await indexed({
...STORE,
'store/reader.h': lines(
'#include "store/table.h"',
'namespace store {',
'class Reader {',
' public:',
' Reader(Writer* w, Table* t);',
' int Run();',
' private:',
' Writer* writer_;',
' Table* table_;',
'};',
'} // namespace store',
),
'store/reader.cc': lines(
'#include "store/reader.h"',
'namespace store {',
'Reader::Reader(Writer* w, Table* t) : writer_(w), table_(t) {}',
'int Reader::Run() {',
' return table_->Get(1);',
'}',
'int Arguments(Log* log) {',
' Table table;',
' Writer writer(log, table, 3);',
' Writer copy = Wrap(log, table, 3);',
' Use(writer, table, 3);',
' return table.Get(2);',
'}',
'int Parameters(Writer writer, Table table) {',
' return table.Get(3);',
'}',
'} // namespace store',
),
});
try {
expect(calls(cg, 'store::Reader::Run')).toEqual(['store::Table::Get @0.9']);
expect(calls(cg, 'store::Arguments')).toContain('store::Table::Get @0.9');
expect(calls(cg, 'store::Parameters')).toEqual(['store::Table::Get @0.9']);
expect(cg.getCallers(callable(cg, 'store::Writer::Get').id).filter((r) => r.edge.kind === 'calls')).toEqual([]);
} finally {
cg.close();
}
});
});
+250 -4
View File
@@ -9877,6 +9877,243 @@ function cppDeclaratorType(text: string): string {
return dropped ? out.replace(/\s+/g, ' ').trim() : text;
}
// The receiver after a comma, as a name later in a declaration's list is:
// `ByteBuffer symbols_found, strings_found;`, `Foo *a, *const b;`. The
// declarator regex wants the type right before the name, and a list writes
// it once, before the first name; matchCppListedDeclarator reads the others.
function buildListedDeclaratorRegex(escapedReceiver: string): RegExp {
return new RegExp(String.raw`,(?:\s|[*&]|${CPP_DECLARATOR_CV}|${CPP_DECLARATOR_ANNOTATION})*\b${escapedReceiver}\b`);
}
// Words a C++ declaration may open with before its type.
const CPP_DECL_SPECIFIERS: ReadonlySet<string> = new Set([
'static', 'extern', 'inline', 'constexpr', 'constinit', 'thread_local', 'register', 'mutable',
'const', 'volatile', 'typename', 'struct', 'class', 'union', 'enum',
]);
// Words that never are the type a declaration of variables opens with.
const CPP_NOT_A_DECLARED_TYPE: ReadonlySet<string> = new Set([
...CPP_NON_TYPE_TOKENS, 'typedef', 'using', 'namespace', 'template', 'friend', 'operator',
'public', 'private', 'protected', 'virtual', 'explicit', 'static_assert', 'asm', 'try', 'catch',
'this', 'true', 'false', 'nullptr',
]);
// Nor a declared variable's name: those, the specifiers and the built-in
// types. `const Message& message, int, int)` ends a parameter list.
const CPP_NOT_A_DECLARED_NAME: ReadonlySet<string> = new Set([
...CPP_NOT_A_DECLARED_TYPE, ...CPP_DECL_SPECIFIERS, 'auto', 'void', 'bool', 'char', 'wchar_t',
'char8_t', 'char16_t', 'char32_t', 'short', 'int', 'long', 'signed', 'unsigned', 'float', 'double',
]);
const CPP_DECLARATOR_CV_WORD = new RegExp(`^${CPP_DECLARATOR_CV}$`);
const CPP_TRAILING_ANNOTATION_AT = new RegExp(CPP_TRAILING_ANNOTATION, 'y');
/** The identifier at `code[i]`, or null. */
function cppWordAt(code: string, i: number): string | null {
CPP_WORD_AT.lastIndex = i;
return CPP_WORD_AT.exec(code)?.[0] ?? null;
}
/** Past the whitespace at `code[i]`. */
function cppSkipSpace(code: string, i: number): number {
while (i < code.length && /\s/.test(code[i]!)) i++;
return i;
}
/** The word that ends where the spaces before `code[i]` begin (`for` before `for (`). */
function cppWordBefore(code: string, i: number): string {
let end = i;
while (end > 0 && /\s/.test(code[end - 1]!)) end--;
let start = end;
while (start > 0 && /\w/.test(code[start - 1]!)) start--;
return code.slice(start, end);
}
/** Does a string or character literal open at `code[i]`? (A `'` after a digit separates digits: `1'000'000`.) */
function cppOpensLiteral(code: string, i: number): boolean {
return code[i] === '"' || (code[i] === '\'' && !/\d/.test(code[i - 1] ?? ''));
}
/**
* Past the bracket that closes the `(`, `[` or `{` at `code[i]`, over what
* is nested or quoted inside: the line's length when the line ends first,
* -1 when a bracket closes out of turn.
*/
function cppPastBrackets(code: string, i: number): number {
const closers: string[] = [];
for (; i < code.length; i++) {
const c = code[i]!;
if (c === '(') closers.push(')');
else if (c === '[') closers.push(']');
else if (c === '{') closers.push('}');
else if (c === ')' || c === ']' || c === '}') {
if (closers.pop() !== c) return -1;
if (closers.length === 0) return i + 1;
} else if (cppOpensLiteral(code, i)) i = cppLiteralEnd(code, i);
}
return code.length;
}
/**
* Where the initializer starting at `code[i]` ends: at the first `,`, `;`
* or closing bracket outside its own brackets, or the line's end; -1 when
* its brackets don't match. A `<` is not taken for a bracket — it may be
* `a < b` — so a comma in template arguments (`f<A, B>(x)`) ends it, and
* what follows then reads as no declarator.
*/
function cppPastInitializer(code: string, i: number): number {
while (i < code.length) {
const c = code[i]!;
if (c === ',' || c === ';' || c === ')' || c === ']' || c === '}') return i;
if (c === '(' || c === '[' || c === '{') {
i = cppPastBrackets(code, i);
if (i < 0) return -1;
} else if (cppOpensLiteral(code, i)) i = cppLiteralEnd(code, i) + 1;
else i++;
}
return i;
}
/** Past the template argument list opening at `code[i]` (`<`), or -1 when it doesn't close on the line. */
function cppPastTemplateArguments(code: string, i: number): number {
let angles = 0;
let parens = 0;
for (; i < code.length; i++) {
const c = code[i]!;
if (c === '(') parens++;
else if (c === ')') {
if (--parens < 0) return -1;
} else if (parens > 0) continue;
else if (c === '<') angles++;
else if (c === '>') {
if (--angles === 0) return i + 1;
} else if (c === ';' || c === '{' || c === '}') return -1;
}
return -1;
}
/** Past the type a C++ declaration opens with at `code[i]` — `ns::Table<int, Box<int>>`, `::Foo` — or -1. */
function cppPastDeclaredType(code: string, i: number): number {
if (code.startsWith('::', i)) i = cppSkipSpace(code, i + 2);
for (let segment = 0; ; segment++) {
let word = cppWordAt(code, i);
// `typename Traits<T>::template Rebind<U>`
if (word === 'template' && segment > 0) {
i = cppSkipSpace(code, i + word.length);
word = cppWordAt(code, i);
}
if (word === null || CPP_NOT_A_DECLARED_TYPE.has(word)) return -1;
i += word.length;
let next = cppSkipSpace(code, i);
if (code[next] === '<') {
i = cppPastTemplateArguments(code, next);
if (i < 0) return -1;
next = cppSkipSpace(code, i);
}
if (!code.startsWith('::', next)) return i;
i = cppSkipSpace(code, next + 2);
}
}
/** A name a C++ declaration lists after its first, as cppDeclaratorsAfterFirst reads it. */
interface CppListedName {
name: string;
/** The type the declaration opens with plus this name's own `*`s and `&`s. */
type: string;
/** Where the declaration's type starts. */
typeAt: number;
/** Where the name ends, an annotation after it (`GUARDED_BY(mu_)`) included. */
end: number;
}
/**
* The names after the first that the C++ declaration starting at
* `code[start]` lists, or null when no declaration starts there. In a `for`,
* `if` or `switch` header a `)` may end it; one that runs on past the line
* ends with it. A declarator's `*`s and `&`s are its own, and so are the
* cv-qualifiers and annotations the declarator regex reads past: `Arena*
* PROTOBUF_NULLABLE a, * PROTOBUF_NULLABLE b`.
*/
function cppDeclaratorsAfterFirst(code: string, start: number, inHeader: boolean): CppListedName[] | null {
let i = cppSkipSpace(code, start);
for (let word = cppWordAt(code, i); word !== null && CPP_DECL_SPECIFIERS.has(word); word = cppWordAt(code, i)) {
i = cppSkipSpace(code, i + word.length);
}
const typeAt = i;
i = cppPastDeclaredType(code, i);
if (i < 0) return null;
const type = code.slice(typeAt, i);
const listed: CppListedName[] = [];
for (let first = true; ; first = false) {
let pointer = '';
for (i = cppSkipSpace(code, i); ; i = cppSkipSpace(code, i)) {
const c = code[i];
if (c === '*' || c === '&') {
pointer += c;
i++;
continue;
}
const word = cppWordAt(code, i);
if (word === null) break;
// An annotation word after a `*` or `&` is one only when the name still follows it.
const after = cppSkipSpace(code, i + word.length);
if (!CPP_DECLARATOR_CV_WORD.test(word) &&
!(pointer && CPP_DECLARATOR_ANNOTATION_WORD.test(word) && (/[*&]/.test(code[after] ?? '') || cppWordAt(code, after) !== null))) break;
i += word.length;
}
// `Foo a,` with the rest of the list on the next line.
if (i >= code.length && !first && !pointer) return listed;
const name = cppWordAt(code, i);
if (name === null || CPP_NOT_A_DECLARED_NAME.has(name)) return null;
i = cppSkipSpace(code, i + name.length);
// `Foo Bar::baz` defines a member of Bar.
if (code.startsWith('::', i)) return null;
for (CPP_TRAILING_ANNOTATION_AT.lastIndex = i; CPP_TRAILING_ANNOTATION_AT.exec(code); CPP_TRAILING_ANNOTATION_AT.lastIndex = i) {
i = cppSkipSpace(code, CPP_TRAILING_ANNOTATION_AT.lastIndex);
}
if (!first) listed.push({ name, type: type + pointer, typeAt, end: i });
while (code[i] === '[') {
i = cppPastBrackets(code, i);
if (i < 0) return null;
i = cppSkipSpace(code, i);
}
if (code[i] === '(' || code[i] === '{') i = cppPastBrackets(code, i);
else if (code[i] === '=' && code[i + 1] !== '=') i = cppPastInitializer(code, i + 1);
if (i < 0) return null;
i = cppSkipSpace(code, i);
if (i >= code.length || code[i] === ';' || (inHeader && code[i] === ')')) return listed;
if (code[i] !== ',') return null;
i++;
}
}
/**
* The declaration of `receiverName` on the code `line` when the name follows
* another in the declaration's list — `ByteBuffer symbols_found,
* strings_found;`, `Foo* a = nullptr, *b;`, `Bar a{1}, b(2);` — shaped like
* matchCppDeclarator's match: the declaration's type from `index`, through
* the name at [0], and at [1] the type as the declarator regex reads one,
* this name's own `*`s and `&`s on the type the declaration opens with, since
* `Foo *a, b;` declares one pointer. A comma in template arguments
* (`std::map<K, V> m;`), in a call's or a constructor's arguments (`Writer
* w(log, table, 3);`, `: a_(x), b_(y)`) or in an expression (`x = f(a, b);`)
* lists no name. A declaration opens the line, follows a statement or a
* 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;
for (let i = -1; i < line.length; i++) {
const c = line[i];
const header = c === '(' && /^(?:for|if|switch)$/.test(cppWordBefore(line, i));
if (i >= 0 && c !== ';' && c !== '{' && c !== '}' && !header) continue;
const named = cppDeclaratorsAfterFirst(line, i + 1, header)?.find((d) => d.name === receiverName);
if (named) {
return Object.assign([line.slice(named.typeAt, named.end), named.type], { index: named.typeAt, input: line }) as RegExpExecArray;
}
}
return null;
}
// A range-based for's declaration, `for (const Foo& name :`, up to the single
// colon (not `::`) that ends it. What may precede the type — `const`, a
// global-scope `::` — is never an initializer, a call or a member access.
@@ -10252,6 +10489,10 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext):
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const word = new RegExp(`\\b${escaped}\\b`);
const declarator = buildDeclaratorRegex(escaped);
const listed = buildListedDeclaratorRegex(escaped);
// `Table *index_, *data_;` declares both members.
const declared = (text: string): RegExpExecArray | null =>
matchCppDeclarator(text, declarator) ?? matchCppListedDeclarator(text, name, listed);
const end = Math.min(decl.endLine ?? decl.startLine, lines.length);
const nested = cppNestedSpans(decl, end, context);
const rawLines = cppNonCodeLines(decl.filePath, context);
@@ -10265,13 +10506,13 @@ function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext):
// above `cfh_iter_pairs_;`), so a default member initializer continued
// on the next line (`bool ready_ = flag_ &&` above `it_;`) isn't one.
let from = l;
let m = code ? matchCppDeclarator(code, declarator) : null;
let m = code ? declared(code) : null;
if (!m && code && l > decl.startLine) {
const above = cppCodeOf(cppLiteralsBlanked(lines[l - 2]!, l - 1, rawLines));
if (above && CPP_TYPE_LINE.test(above)) {
code = `${above} ${code}`;
from = l - 1;
m = matchCppDeclarator(code, declarator);
m = declared(code);
}
}
const rest = m ? code!.slice(m.index + m[0].length) : '';
@@ -12457,6 +12698,7 @@ function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context:
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const word = new RegExp(`\\b${escaped}\\b`);
const declarator = buildDeclaratorRegex(escaped);
const listed = buildListedDeclaratorRegex(escaped);
const nonCode = cppNonCodeLines(caller.filePath, context);
for (let i = Math.max(0, caller.startLine - 1); i < Math.min(lines.length, ref.line); i++) {
if (!word.test(lines[i]!)) continue;
@@ -12469,6 +12711,9 @@ function isCppLocalName(name: string, caller: Node, ref: UnresolvedRef, context:
// `Owner::name(` is a definition's own declarator; `return name(` no declaration.
if (type !== undefined && !/::\s*$/.test(type) && normalizeCppTypeName(type) !== null) return true;
}
// `std::function<void()> done, failed;` declares `failed` too.
const listedType = matchCppListedDeclarator(code, name, listed)?.[1];
if (listedType !== undefined && normalizeCppTypeName(listedType) !== null) return true;
}
return false;
}
@@ -12543,6 +12788,7 @@ function inferCppReceiverType(
const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
const declaratorRegex = buildDeclaratorRegex(escapedReceiver);
const listedRegex = buildListedDeclaratorRegex(escapedReceiver);
// A declaration in a string literal is text: protobuf's upb generator
// writes `upb_MiniTableField field = $field_init$;` in the raw string it
// emits, above calls on its own `upb::FieldDefPtr field` parameter.
@@ -12564,7 +12810,7 @@ function inferCppReceiverType(
const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, rawLines));
if (!line || !receiverPattern.test(line)) continue;
let declaratorMatch = matchCppDeclarator(line, declaratorRegex);
let declaratorMatch = matchCppDeclarator(line, declaratorRegex) ?? matchCppListedDeclarator(line, receiverName, listedRegex);
// A parameter named like the function it belongs to is declared after
// the function's own name on its head: protobuf's `std::string
// u64(uint64_t u64) {` gives `u64` a `uint64_t`, not a `std::string`.
@@ -12674,7 +12920,7 @@ function inferCppReceiverType(
if (!receiverPattern.test(text)) continue;
const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, headerRawLines));
if (!line) continue;
const declaratorMatch = matchCppDeclarator(line, declaratorRegex);
const declaratorMatch = matchCppDeclarator(line, declaratorRegex) ?? matchCppListedDeclarator(line, receiverName, listedRegex);
if (!declaratorMatch) continue;
const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
if (normalized && normalized !== 'auto') {