From 204315274e7902c713ba3ad61e8df511aa634b10 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 20 Oct 2026 01:52:07 +0000 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + .../cpp-listed-declarator-receiver.test.ts | 363 ++++++++++++++++++ src/resolution/name-matcher.ts | 254 +++++++++++- 3 files changed, 614 insertions(+), 4 deletions(-) create mode 100644 __tests__/cpp-listed-declarator-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 717d34ead..6fd53c5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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(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 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 diff --git a/__tests__/cpp-listed-declarator-receiver.test.ts b/__tests__/cpp-listed-declarator-receiver.test.ts new file mode 100644 index 000000000..4e426d930 --- /dev/null +++ b/__tests__/cpp-listed-declarator-receiver.test.ts @@ -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 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): Promise { + 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 ', + '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 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` 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 ', + '#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 ', + '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 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(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index ad1d3f1c2..7cece60a9 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -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 = 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 = 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 = 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(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>`, `::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::template Rebind` + 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 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 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') {