mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 08:47:04 +08:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user