From 35adb8fbec25deb3658f7e4ea617e43177ff7c8c Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Thu, 15 Oct 2026 18:30:46 -0500 Subject: [PATCH] fix(cpp): a call on an element read by subscript, `statuses[i].IsNotFound()`, calls the element's method The extractor records a call on a subscripted receiver, like `statuses[i].IsNotFound()`, `buffers[key].Flush()` or `iters[i]->Valid()`, by its bare name, which went to whichever class had a method of that name, and a local an element initializes (`auto& s = statuses[i]`) had no type. - C++ expression typing reads a subscript: an array's element, what a pointer points at, a library map's mapped value, a sequence's element, or what a class's `operator[]` returns. - Declarations record their array dimensions (locals, parameters and data members, also through an alias), so a subscript, `*arr`, `arr->` and a range-for over an array read the type the array is declared with. - The bare-name receiver reader takes subscripts after a name or `(*name)`, reading the line with its literals blanked. - Overload arguments use the same subscript typing: a map's `values[key]` is its value type, no longer a `std::pair`. A/B vs main, every changed site triaged: leveldb 12 removed, 14 added, 1 retargeted, 11 metadata-only; fmt 6/4/10/2; redis identical; rocksdb 513/292/843/807; protobuf 58/5/34/66. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 +- __tests__/cpp-element-access-calls.test.ts | 231 +++++++++++++++++++++ src/resolution/name-matcher.ts | 199 ++++++++++++++---- 3 files changed, 388 insertions(+), 46 deletions(-) create mode 100644 __tests__/cpp-element-access-calls.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b6b02cfe..f75d3503bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,7 +95,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a variable, parameter or member declared as a standard-library or other outside type with a lowercase name, like `std::string`, `std::vector`, `std::set` or `absl::flat_hash_set`, no longer links to a project method that only shares the method's name, even one of a project class in another namespace with the type's name, like your own `util::flat_hash_set`. A variable declared outside the calling function, like another function's or a file's `std::string text`, no longer links its calls to such a class either. In google/leveldb, `saved_key_.clear()` on a `std::string saved_key_;` reached `Slice::clear`, and in protocolbuffers/protobuf, `proto.append(…)` on a `std::string` reached an unrelated `LeftoverBuffer::append`. The same goes for `->` through a raw pointer like `std::string* out`, while `->` through a smart pointer, iterator or optional still links to the type it holds. Comments also no longer pass for a variable's declaration, so more calls now link to the method of the type the variable is really declared with. Re-index C++ projects after upgrading. - In C++, a class written with attribute macros the parser can't read now keeps its members. That covers several macros between `class` and the class name, like protocolbuffers/protobuf's generated `class PROTOBUF_EXPORT PROTOBUF_FUTURE_ADD_EARLY_WARN_UNUSED Any final`, a macro with arguments there, like RocksDB's `struct ALIGN_AS(64U) HandleImpl`, or one before a template specialization, and macros on a member: in front of it (`PROTOBUF_FUTURE_ADD_EARLY_NODISCARD`), between a pointer and its name (`PROTOBUF_NONNULL`), after its parameters or its name (`ABSL_ATTRIBUTE_LIFETIME_BOUND`, google/leveldb's `LOCKS_EXCLUDED(mu_)` and `GUARDED_BY(mu_)`), or alone on a line above a comment ({fmt}'s `FMT_BEGIN_EXPORT`). Before, protobuf's generated message classes were missing and their members, like `GetDescriptor`, `GetReflection` and `unknown_fields`, were indexed as functions outside any class, and leveldb's annotated methods were indexed under the annotation's name, so calls to them linked to nothing. Re-index C++ projects after upgrading. - In C++, a call on a range-based `for` loop's variable, like `suite->SetVerbose(verbose)` inside `for (ConformanceTestSuite *suite : suites)`, now links to the method of the type the loop declares. Before, the loop's declaration was never read, so the call was matched by the variable's name alone: in RocksDB, `log->file()` on a `log::Writer` went to `BlobLogWriter::file`, and a loop variable named like a member of its class was taken for that member. The variable counts only inside its loop. Re-index C++ projects after upgrading. -- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is matched as before. The same goes for `->` on a container's iterator, like `it->data()` on a `std::deque::const_iterator it` or `loc->path_size()` on protocolbuffers/protobuf's `RepeatedPtrField::iterator loc`: the call links to that method of the container's element type, and to no project method when the element is a standard-library type like `std::string`. That includes an iterator an `auto` variable gets from a standard-library container's `begin()`, `find()` and the like, or from a standard search like `std::lower_bound`, as in facebook/rocksdb's `auto it = logs_.begin(); it->IsSyncing()`. A call on a dereferenced iterator or pointer, like `(*it)->Visit()` over a `std::vector` or `(*it).Weight()` over a `std::vector`, and on a variable a dereference sets, like `auto first = *nodes.begin()`, links the same way, where before such a call went to whichever class had a method of that name. So does a call on the key or value a map's iterator points at, like `it->second.Flush()`, `it->second->Flush()` or a range-based `for` loop's `kv.second.Flush()`, and on the `first` or `second` of a `std::pair` the code declares. Re-index C++ projects after upgrading. +- In C++, a call made with `->` on a variable, parameter or member declared as a `std::unique_ptr`, `std::shared_ptr` or `std::optional`, like `iter->Valid()` on a `std::unique_ptr iter`, now links to that method of the type it holds, or of a class that type inherits from, with the type looked up the way C++ does it, so a class of the same name nested elsewhere is not mistaken for it. Before, the call went to whichever project method shared its name: in facebook/rocksdb, `iter->Valid()` reached `ArenaWrappedDBIter::Valid` and `fs->GetFileSize(…)` on a `std::shared_ptr` reached a test's stand-in file system, and calls like `it->Next()` were often not linked at all. When what it holds is a template parameter or a standard-library type, like `std::unique_ptr>`, or a class that has no method of that name and neither do the classes it derives from, the call no longer links to a project method; a class derived from something that can't be followed, like a `decltype(…)`, may still have it, so that call is matched as before. The same goes for `->` on a container's iterator, like `it->data()` on a `std::deque::const_iterator it` or `loc->path_size()` on protocolbuffers/protobuf's `RepeatedPtrField::iterator loc`: the call links to that method of the container's element type, and to no project method when the element is a standard-library type like `std::string`. That includes an iterator an `auto` variable gets from a standard-library container's `begin()`, `find()` and the like, or from a standard search like `std::lower_bound`, as in facebook/rocksdb's `auto it = logs_.begin(); it->IsSyncing()`. A call on a dereferenced iterator or pointer, like `(*it)->Visit()` over a `std::vector` or `(*it).Weight()` over a `std::vector`, and on a variable a dereference sets, like `auto first = *nodes.begin()`, links the same way, where before such a call went to whichever class had a method of that name. So does a call on the key or value a map's iterator points at, like `it->second.Flush()`, `it->second->Flush()` or a range-based `for` loop's `kv.second.Flush()`, and on the `first` or `second` of a `std::pair` the code declares. So does a call on an element read by subscript, like `statuses[i].IsNotFound()` over a `std::vector`, `buffers[key].Flush()` over a map, `iters[i]->Valid()` over owning pointers, or an element of an array or of a class's `operator[]`. Re-index C++ projects after upgrading. - In C++, a call a method makes with no receiver, or through `this->`, now links to the method C++ itself would call: one of the method's own class, else one the class inherits from a base class, else one of a class it is nested in. The same goes for a call in a lambda inside the method, and of a class's overloads the one the call's arguments fit is taken. Before, the call was matched by its name alone, so another class's method of that name could win because it was declared nearer or found first: in protocolbuffers/protobuf, every generated message's `Swap` and move assignment calling `InternalSwap(…)` reached `Any::InternalSwap`, and the `GetArena()` messages inherit from `MessageLite` reached an arena helper's `GetArena`, while googletest's `~linked_ptr() { depart(); }`, vendored in facebook/rocksdb, reached `linked_ptr_internal::depart`. Calls C++ doesn't look up in the class are matched as before: a free function's, a call to a parameter or local of that name, and a template's bare call into a base class that depends on its template parameters. Re-index C++ projects after upgrading. Thanks @danusha2345 for reporting the C++ case in #1952. - In C++, a call on the variable of a range-based `for` loop over `auto` elements, like `cfd->Ref()` inside facebook/rocksdb's `for (auto cfd : cfds)`, now links to the method of the type the loop goes over. That type is read from what the loop iterates: a `std::vector`, `std::set`, `absl::Span` or similar container, protocolbuffers/protobuf's `RepeatedPtrField` or RocksDB's `autovector`, whether a variable, parameter or member holds it, an `auto` variable is set to it or a function or method returns it, and a project class you loop over through its `begin()`, like RocksDB's `ColumnFamilySet`. Before, the call was matched by the variable's name, or by an earlier variable of that name: in RocksDB, `blob_file.GetBlobFileNumber()` over a list of `BlobFileGarbage` reached `BlobFileAddition::GetBlobFileNumber`, and in protobuf's Objective-C generator, calls over a list of `std::unique_ptr` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector`, no longer links to a project method, and `->` on an element held in a `std::unique_ptr` or `std::shared_ptr` reaches the type it holds. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. A type written from the global scope, like `::upb::Arena& arena`, is looked up there, so a namespace's own `upb::Arena` is not taken for it, nor is an alias of that name the calling function declares. A type written as a member of a class template, like `SkipList::Iterator iter` in leveldb's and RocksDB's skip-list tests, is now read whole, so its calls reach the skip list's own iterator rather than the general `Iterator`, and RocksDB's skip-list memtable iterator no longer links its calls on the skip list's iterator to its own methods; a standard container's `iterator` written this way is the library's, never a class of yours named `iterator`. Re-index C++ projects after upgrading. @@ -107,7 +107,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call written without a receiver in a gtest `TEST_F` or `TEST_P` body now links to the test fixture's method, its own or one it inherits, as C++ finds it: facebook/rocksdb's tests call `DBTestBase`'s `Put(…)`, `Flush()` and `NewIterator(…)` this way. A variable such a call initializes, like `auto iter = NewIterator(…)`, takes the type the method returns, so `iter->SeekToFirst()` reaches the iterator class. Before, those calls were guessed by name and often reached a namesake in another class, like RocksDB's `ColumnFamilyTestBase::Put` or protocolbuffers/protobuf's `EncodeDecodeTest::ExpectNoErrors`, and `iter->SeekToFirst()` reached the unrelated `MemTableRep::Iterator`. A `->` call on a value of a type from outside the project, like protobuf's `m->value()` on an `absl::StatusOr`, no longer links to a class of the project that shares the type's name, like protobuf's own `hpb::StatusOr`. Re-index C++ projects after upgrading. - In C++, a call made with `->` on an `absl::StatusOr`, an `absl::optional` or an `absl::NoDestructor`, like protocolbuffers/protobuf's `resolver->MergeFeatures(…)` on an `absl::StatusOr`, now links to the method of the class it holds, as one on a `std::unique_ptr` or `std::optional` does. A call written without a receiver in a class template now also finds a member of a base that depends on the template's parameters when the class brings it in with a using-declaration, like facebook/rocksdb's `using BasicTypedCacheHelperFns::DownCastValue;`, and so does a call on such a member. Re-index C++ projects after upgrading. - In C and C++, a call or a type name no longer links to something only another source file can see: a class, struct or alias a `.c` or `.cc` file declares for itself, its members, or anything inside that file's unnamed `namespace { … }`. In facebook/rocksdb, other test files' bare `Put(…)`, `Flush()` and `dbfull()` calls had linked to the methods of a test fixture declared in `column_family_test.cc`, and calls on an `Env*` had linked to a test-only environment class in `memenv.cc`. When a header and a source file each declare a class of the same name, like RocksDB's `SharedState`, a call now links to the header's. A source file's own declarations are still found from the file itself, from a file that `#include`s it, and from a header it includes below them. Re-index C and C++ projects after upgrading. -- In C++, a call to an overloaded method now links to the overload its arguments fit: by how many there are, and then by their types. That covers a call on a variable or member of a known class, like facebook/rocksdb's `db->Get(ro, key, &value)` and `db->Put(write_opts, key, value)`, as well as a call written without a receiver, like `Insert(ch)` on a `char ch`. Before, a call on a variable always linked to the first overload declared, so RocksDB's convenience overloads of `DB::Get`, `DB::Put` and `DB::NewIterator` and protocolbuffers/protobuf's `io::Printer::Print`, `Emit` and `Annotate` had almost no callers, and a call without a receiver picked by argument count alone. A literal, a variable the calling function declares, `&variable`, `container[i]` and the result of a call are read for their types; a parameter's default value is read from the method's declaration in its class, and comments in argument lists are skipped. Re-index C++ projects after upgrading. +- In C++, a call to an overloaded method now links to the overload its arguments fit: by how many there are, and then by their types. That covers a call on a variable or member of a known class, like facebook/rocksdb's `db->Get(ro, key, &value)` and `db->Put(write_opts, key, value)`, as well as a call written without a receiver, like `Insert(ch)` on a `char ch`. Before, a call on a variable always linked to the first overload declared, so RocksDB's convenience overloads of `DB::Get`, `DB::Put` and `DB::NewIterator` and protocolbuffers/protobuf's `io::Printer::Print`, `Emit` and `Annotate` had almost no callers, and a call without a receiver picked by argument count alone. A literal, a variable the calling function declares, `&variable`, an element read by subscript, like `container[i]` or a map's `values[key]`, and the result of a call are read for their types; a parameter's default value is read from the method's declaration in its class, and comments in argument lists are skipped. Re-index C++ projects after upgrading. - In C++, a nested class defined outside the class it belongs to, like google/leveldb's `struct SkipList::Node { … }` or facebook/rocksdb's `struct BlockBasedTable::Rep { … }`, is now indexed under its own name, `SkipList::Node`, with its methods and members. Before, it was indexed as an unnamed class, so it was missing from search and `codegraph_explore`, its methods belonged to no class you could name, and a call like `x->Next(level)` on a `Node*` was matched by the method's name alone. Creating an object, as in `new log::Writer(…)` or `Writer w(…)`, now links to the class C++ itself finds for the type as written there: leveldb's `new log::Writer(…)` in `DBImpl` methods no longer links to `DBImpl`'s own `Writer`, RocksDB's tests' `Random rnd(301)` links to RocksDB's `Random` instead of googletest's, an object created through an alias links to the class the alias names, and one links to its class rather than to the class's constructor. An object of a template parameter's type, or of a type the function names with its own `using`, like protocolbuffers/protobuf's `using Map = std::map<…>`, no longer links to a project class that only shares the name. Re-index C++ projects after upgrading. - In C++, a call whose receiver's type can't be read no longer links to a method of a class nested in the class the receiver's name suggests, or of a class whose name only contains that name. Before, such a guess took any method of the call's name whose qualified name contained the class's name: protocolbuffers/protobuf's `descriptor->file()` in a template reached `Descriptor::ExtensionRange::file`, and facebook/rocksdb's `CompositeEnv::IsInstanceOf(name)` reached `CompositeEnvWrapper::IsInstanceOf`. A call written through a class, like `PessimisticTransaction::PopSavePoint()` in an override, now links to the method that class inherits, here `TransactionBaseImpl::PopSavePoint`. Re-index C++ projects after upgrading. - In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading. diff --git a/__tests__/cpp-element-access-calls.test.ts b/__tests__/cpp-element-access-calls.test.ts new file mode 100644 index 0000000000..0689af627c --- /dev/null +++ b/__tests__/cpp-element-access-calls.test.ts @@ -0,0 +1,231 @@ +/** + * A C++ call on a subscripted element, like `statuses[i].IsNotFound()` over + * a `std::vector`, `m[k].Flush()` over a map, or `iters[i]->Valid()` + * over owning pointers, calls a member of the element's class. The + * extractor records such a call by its bare name, which went to whichever + * class had a method of that name, and a local an element initializes + * (`auto& s = statuses[i]`) had no type. An array's element is the type the + * array is declared with (`Row rows[2]`), however many dimensions it has. + */ +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-element-access-')); + 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, as `qualifiedName`. */ +function calls(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => r.node.qualifiedName) + .sort(); +} + +/** `calls` callees of a function or method, as `qualifiedName:startLine`, which tells overloads apart. */ +function callsAt(cg: CodeGraph, caller: string): string[] { + return cg + .getCallees(callable(cg, caller).id) + .filter((r) => r.edge.kind === 'calls') + .map((r) => `${r.node.qualifiedName}:${r.node.startLine}`) + .sort(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** `Other` comes first: a call matched by its bare name lands on it. */ +const TYPES = { + 'src/types.h': lines( + 'class Other {', + ' public:', + ' bool IsNotFound() const { return false; }', + ' bool ok() const { return false; }', + ' void Flush() {}', + ' bool Valid() const { return false; }', + ' int size() const { return 0; }', + ' void Print() const {}', + ' int Size() const { return 0; }', + '};', + 'class Status {', + ' public:', + ' bool IsNotFound() const { return true; }', + ' bool ok() const { return true; }', + '};', + 'class Buffer {', + ' public:', + ' void Flush() {}', + '};', + 'class Iterator {', + ' public:', + ' bool Valid() const { return true; }', + '};', + 'class Cell {', + ' public:', + ' void Print() const {}', + '};', + 'class Row {', + ' public:', + ' Cell& operator[](int i) { return cells_[i]; }', + ' int Size() const { return 4; }', + ' private:', + ' Cell cells_[4];', + '};', + ), +}; + +describe('a C++ call on a subscripted element', () => { + it('reproduction: a vector\'s, a map\'s, and owning pointers\' elements', async () => { + const cg = await indexed({ + ...TYPES, + 'src/access.cc': lines( + '#include ', + '#include ', + '#include ', + '#include "src/types.h"', + 'bool Missing(std::vector& statuses, int i) { return statuses[i].IsNotFound(); }', + 'void FlushOne(std::map& buffers, int k) { buffers[k].Flush(); }', + 'bool AnyValid(std::vector>& iters, int i) { return iters[i]->Valid(); }', + ), + }); + try { + expect(calls(cg, 'Missing')).toEqual(['Status::IsNotFound']); + expect(calls(cg, 'FlushOne')).toEqual(['Buffer::Flush']); + expect(calls(cg, 'AnyValid')).toEqual(['Iterator::Valid']); + } finally { + cg.close(); + } + }); + + it('a raw array, a pointer, two subscripts, a pair\'s member, and a local the element initializes', async () => { + const cg = await indexed({ + ...TYPES, + 'src/more.cc': lines( + '#include ', + '#include ', + '#include "src/types.h"', + 'bool Array() { Status statuses[3]; return statuses[0].ok(); }', + 'bool Pointer(Status* statuses, int i) { return statuses[i].ok(); }', + 'void Grid(std::vector>& table) { table[0][1].Flush(); }', + 'void Pairs(std::vector>& cfs, int i) { cfs[i].first.Flush(); }', + 'bool Bound(std::vector& statuses, int i) {', + ' auto& s = statuses[i];', + ' return s.ok();', + '}', + ), + }); + try { + expect(calls(cg, 'Array')).toEqual(['Status::ok']); + expect(calls(cg, 'Pointer')).toEqual(['Status::ok']); + expect(calls(cg, 'Grid')).toEqual(['Buffer::Flush']); + expect(calls(cg, 'Pairs')).toEqual(['Buffer::Flush']); + expect(calls(cg, 'Bound')).toEqual(['Status::ok']); + } finally { + cg.close(); + } + }); + + it('a class\'s `operator[]`, a key with a `]` in it, and an element of a library type', async () => { + const cg = await indexed({ + ...TYPES, + 'src/rows.cc': lines( + '#include ', + '#include ', + '#include ', + '#include "src/types.h"', + 'void Show(Row& row) { row[0].Print(); }', + 'void Named(std::map& buffers) { buffers["a]b"].Flush(); }', + 'int Length(std::vector& values, int i) { return values[i].size(); }', + ), + }); + try { + expect(calls(cg, 'Show')).toEqual(['Cell::Print']); + expect(calls(cg, 'Named')).toEqual(['Buffer::Flush']); + expect(calls(cg, 'Length')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('an array is of the type it is declared with, however many dimensions it has', async () => { + const cg = await indexed({ + ...TYPES, + 'src/arrays.cc': lines( + '#include ', + '#include "src/types.h"', + 'class Shards {', + ' public:', + ' bool Second() { return statuses_[1].ok(); }', + ' private:', + ' Status statuses_[2];', + '};', + 'int Rows() { Row rows[2]; return rows[1].Size(); }', + 'using Line = Row;', + 'int Lines() { Line lines[2]; return lines[1].Size(); }', + 'bool Grid() { Status grid[2][3]; return grid[1][2].ok(); }', + 'void Pools() { std::vector pools[2]; pools[1][0].Flush(); }', + 'bool First() { Status statuses[2]; return (*statuses).ok(); }', + 'bool Each() {', + ' Status statuses[2];', + ' for (auto& s : statuses) {', + ' if (!s.ok()) return false;', + ' }', + ' return true;', + '}', + ), + }); + try { + expect(calls(cg, 'Shards::Second')).toEqual(['Status::ok']); + expect(calls(cg, 'Rows')).toEqual(['Row::Size']); + expect(calls(cg, 'Lines')).toEqual(['Row::Size']); + expect(calls(cg, 'Grid')).toEqual(['Status::ok']); + expect(calls(cg, 'Pools')).toEqual(['Buffer::Flush']); + expect(calls(cg, 'First')).toEqual(['Status::ok']); + expect(calls(cg, 'Each')).toEqual(['Status::ok']); + } finally { + cg.close(); + } + }); + + it('an element read by subscript picks the overload it is passed to', async () => { + const cg = await indexed({ + ...TYPES, + 'src/sink.cc': lines( + '#include ', + '#include "src/types.h"', + 'class Sink {', + ' public:', + ' void Put(const Other& o) {}', + ' void Put(const Buffer& b) {}', + '};', + 'void Drain(Sink& sink, std::map& buffers) { sink.Put(buffers[1]); }', + ), + }); + try { + expect(callsAt(cg, 'Drain')).toEqual(['Sink::Put:6']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 04cd96f369..b943fe926f 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9323,6 +9323,11 @@ interface CppWrittenType { file: string; line: number; parameters: ReadonlySet; + /** + * The array dimensions a declarator gives a variable of `text`, which the + * type doesn't show: `Status statuses[3]` is a `Status` with one. + */ + array?: number; } /** @@ -9365,6 +9370,74 @@ function cppOpeningParen(text: string, close: number): number { return -1; } +/** Where the `[` that the `]` at `text[close]` closes opens, or -1. */ +function cppOpeningBracket(text: string, close: number): number { + let depth = 0; + for (let i = close; i >= 0; i--) { + if (text[i] === ']') depth++; + else if (text[i] === '[' && --depth === 0) return i; + } + return -1; +} + +/** Where the `]` that closes the `[` at `text[open]` is, or -1. */ +function cppClosingBracket(text: string, open: number): number { + let depth = 0; + for (let i = open; i < text.length; i++) { + if (text[i] === '[') depth++; + else if (text[i] === ']' && --depth === 0) return i; + } + return -1; +} + +/** + * How many array dimensions the C++ declarator that the code `rest` follows + * declares: one for the `[kNumShards];` after `LRUCache shard_`. (Not an + * attribute's `[[…]]`.) + */ +function cppArrayDimensions(rest: string): number { + let dimensions = 0; + for (let open = /^\s*\[(?!\[)/.exec(rest); open; open = /^\s*\[(?!\[)/.exec(rest)) { + dimensions++; + const close = cppClosingBracket(rest, open[0].length - 1); + if (close < 0) break; + rest = rest.slice(close + 1); + } + return dimensions; +} + +/** An element of the array `type`: the type it is declared with, one dimension less. */ +function cppArrayElement(type: CppWrittenType): CppWrittenType { + const { array, ...element } = type; + return array !== undefined && array > 1 ? { ...element, array: array - 1 } : element; +} + +/** + * The element a subscript of a value of the type `type` reads: an array's, + * what a pointer points at, a library map's mapped value, a sequence's + * element (a library container's, or one of CPP_PROJECT_SEQUENCES'), or what + * a class's `operator[]` returns. Null for anything else: a `std::string`'s + * `char`, or a class whose `operator[]` the index doesn't hold. + */ +function cppSubscriptElement(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null { + if (type.array) return cppArrayElement(type); + if (isCppPointerType(type.text)) return cppPointee(type); + const segments = cppTypeSegments(type.text); + const last = segments?.[segments.length - 1]; + if (!segments || !last) return null; + const library = segments[0] === 'std' || segments[0] === 'absl'; + if (library && CPP_LIBRARY_MAPS.has(last)) { + const value = cppTemplateArguments(type.text)?.[1]; + return value ? { ...type, text: cppWithoutNullability(value) } : null; + } + if ((library && CPP_LIBRARY_SEQUENCES.has(last)) || CPP_PROJECT_SEQUENCES.has(last)) return cppRangeElementOf(type, at, context); + if (library) return null; + const cls = cppWrittenClass(type, at, context); + if (!cls) return null; + const element = cppReturnTypeOf(cppMethodsOf(cls, 'operator[]', at, context), context); + return element ? { ...element, text: element.text.replace(/&+\s*$/, '').trim() } : null; +} + /** * The return type of a C or C++ function, as its declaration writes it: * `std::vector` for `std::vector 0 ? { array } : {}), + }; break; } } @@ -9727,13 +9810,15 @@ function cppPointee(type: CppWrittenType): CppWrittenType | null { } /** - * The element type of a range of the type `type`, as written: a - * container's first template argument (`autovector`), or, - * for one of the project's classes, what its `begin()` returns dereferenced - * — rocksdb's `ColumnFamilySet`, whose iterator's `operator*` returns a - * `ColumnFamilyData*`. Null when that can't be read. + * The element type of a range of the type `type`, as written: an array's + * (cppArrayElement), a container's first template argument + * (`autovector`), or, for one of the project's classes, + * what its `begin()` returns dereferenced — rocksdb's `ColumnFamilySet`, + * whose iterator's `operator*` returns a `ColumnFamilyData*`. Null when that + * can't be read. */ function cppRangeElementOf(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext, hops = 0): CppWrittenType | null { + if (type.array) return cppArrayElement(type); const range = cppFollowWrittenAliases(type, at, context); if (!range || isCppPointerType(range.text) || hops > 2) return null; const segments = cppTypeSegments(range.text); @@ -9814,7 +9899,7 @@ function cppMemberObject(object: string, op: string, at: UnresolvedRef, context: const followed = type && cppFollowWrittenAliases(type, at, context); if (!followed) return null; if (op === '->') return cppDereferenced(followed, at, context); - return isCppPointerType(followed.text) ? null : followed; + return isCppPointerType(followed.text) || followed.array ? null : followed; } /** @@ -9856,11 +9941,19 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC return cppExpressionType(decl.initializer.text, { ...at, line: decl.initializer.line, column: 0 }, context, depth + 1); } // (A data member of the caller's class has its type as `declared`.) - return decl.raw !== undefined && decl.callers && !decl.shadowed ? { ...callers, text: decl.raw } : null; + if (decl.raw === undefined || !decl.callers || decl.shadowed) return null; + return { ...callers, text: decl.raw, ...(decl.array ? { array: decl.array } : {}) }; } // `new Foo(…)`: a pointer to what it constructs. const created = /^new\s+((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}()]*>)?)\s*(?:[({[][\s\S]*)?$/.exec(text); if (created) return { ...callers, text: `${created[1]!.trim()}*` }; + // `statuses[i]`, `(*m)[key]`: the element a subscript reads. + if (text.endsWith(']')) { + const open = cppOpeningBracket(text, text.length - 1); + const container = open > 0 ? cppExpressionType(text.slice(0, open), at, context, depth + 1) : null; + const followed = container && cppFollowWrittenAliases(container, at, context); + return followed && cppSubscriptElement(followed, at, context); + } if (!text.endsWith(')')) { // `compact_->sub_compact_states`: a data member of the object's class; // `it->second` through a map's iterator: the member of the `std::pair`. @@ -10106,6 +10199,8 @@ interface CppReceiverDeclaration { classTemplate?: boolean; /** The declared type as written (`std::vector*`). */ raw?: string; + /** The array dimensions the declarator gives the receiver (`Status statuses[3]`), which `raw` doesn't show. */ + array?: number; /** The declaration is the calling function's own, or a member of its class (isCppCallersDeclaration). */ callers?: boolean; /** @@ -10338,11 +10433,13 @@ function cppIteratorTarget(iterator: CppWrittenType, at: UnresolvedRef, context: /** * What dereferencing a value of the type `type` gives, for `*x` and `x->`: - * what a pointer points at or a holder holds (cppPointee), or the element a - * container's iterator points at (cppIteratorTarget) — the `Node*` of - * `*nodes.begin()` over a `std::vector`. + * an array's first element, what a pointer points at or a holder holds + * (cppPointee), or the element a container's iterator points at + * (cppIteratorTarget) — the `Node*` of `*nodes.begin()` over a + * `std::vector`. */ function cppDereferenced(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null { + if (type.array) return cppArrayElement(type); return cppPointee(type) ?? cppIteratorTarget(type, at, context); } @@ -10794,33 +10891,52 @@ function cppUtf8Column(line: string, column: number): number | null { } /** - * A call's receiver the extractor drops, up to the member's name: a - * dereferenced variable (`(*it)->`), the member of a pair a variable holds - * or points at (`it->second.`, `entry.first->`), or both (`(*it).second.`). - * Groups: the receiver, the operator. + * A call's receiver the extractor drops, at the start of `text` (its literals + * blanked) up to the member's name: a variable or a dereferenced one + * (`(*it)`), then what the extractor can't spell — subscripts + * (`statuses[i]`, `(*m)[key]`) and the member of a pair (`it->second`, + * `cfs[i].first`) — and the operator. Null when the variable has none of it + * (the extractor spells `it.Flush()`), it is `(*this)` alone + * (cppImplicitReceiver's), or a subscript doesn't close. */ -const CPP_UNSPELLED_RECEIVER = /^((?:\(\s*\*\s*[A-Za-z_]\w*\s*\))(?:\s*(?:->|\.)\s*(?:first|second)\b)?|[A-Za-z_]\w*\s*(?:->|\.)\s*(?:first|second)\b)\s*(->|\.)\s*(?:template\s+)?/; +function cppUnspelledReceiverText(text: string): { expression: string; op: string; length: number } | null { + const head = /^(?:\(\s*\*\s*([A-Za-z_]\w*)\s*\)|[A-Za-z_]\w*)/.exec(text); + if (!head) return null; + let end = head[0].length; + let unspelled = head[1] !== undefined && head[1] !== 'this'; + for (let open = /^\s*\[/.exec(text.slice(end)); open; open = /^\s*\[/.exec(text.slice(end))) { + const close = cppClosingBracket(text, end + open[0].length - 1); + if (close < 0) return null; + end = close + 1; + unspelled = true; + } + const member = /^\s*(?:->|\.)\s*(?:first|second)\b/.exec(text.slice(end)); + if (member) { + end += member[0].length; + unspelled = true; + } + const op = unspelled ? /^\s*(->|\.)\s*(?:template\s+)?/.exec(text.slice(end)) : null; + return op ? { expression: text.slice(0, end).trim(), op: op[1]!, length: end + op[0].length } : null; +} /** * The receiver a C++ call the extractor recorded by its bare name is made - * on, when it is one CPP_UNSPELLED_RECEIVER reads, with the operator — `(*it)` + * on, when cppUnspelledReceiverText reads one, with the operator — `(*it)` * and `->` for `(*it)->Visit()`, `it->second` and `.` for - * `it->second.Flush()` — read at the call's column, where the receiver - * starts. Null for any other call: on `(*this)` (cppImplicitReceiver's), or - * on a receiver the extractor spells. + * `it->second.Flush()`, `statuses[i]` and `.` for `statuses[i].ok()` — read + * at the call's column, where the receiver starts. Null for any other call. */ function cppUnspelledReceiver(ref: UnresolvedRef, context: ResolutionContext): { expression: string; op: string } | null { - const line = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]; - if (line === undefined) return null; + const raw = (context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/))?.[ref.line - 1]; + const line = raw === undefined ? null : cppCodeOf(cppLiteralsBlanked(raw, ref.line, cppNonCodeLines(ref.filePath, context))); + if (!line) return null; const at = (column: number): { expression: string; op: string } | null => { const text = line.slice(column); - const receiver = CPP_UNSPELLED_RECEIVER.exec(text); - if (!receiver || receiver[1]!.replace(/\s+/g, '') === '(*this)') return null; - const rest = text.slice(receiver[0].length); + const receiver = cppUnspelledReceiverText(text); + if (!receiver) return null; + const rest = text.slice(receiver.length); if (!rest.startsWith(ref.referenceName) || /[\w$]/.test(rest.charAt(ref.referenceName.length))) return null; - return cppArgumentListFollows(rest, ref.referenceName.length) - ? { expression: receiver[1]!.replace(/\s+/g, ''), op: receiver[2]! } - : null; + return cppArgumentListFollows(rest, ref.referenceName.length) ? { expression: receiver.expression, op: receiver.op } : null; }; const found = at(Math.max(0, ref.column)); if (found || !/[^\x00-\x7f]/.test(line)) return found; @@ -11075,9 +11191,9 @@ type CppArgumentType = { kind: 'char' | 'number' | 'zero' | 'bool' | 'null' } | /** * What the argument `arg` of the call at `ref` is, where that can be read: a * character, number, `true`/`false` or `nullptr` literal, or the written - * type of a name the calling function declares, of `&name`, of `name[i]` - * (rocksdb's `TryLock(column_family[i], keys[i], …)` on a - * `std::vector`), or of an expression (cppExpressionType). Null + * type of a name the calling function declares, of `&name`, or of an + * expression (cppExpressionType) — the element `keys[i]` reads in rocksdb's + * `TryLock(column_family[i], keys[i], …)` on a `std::vector`. Null * otherwise. (String literals are cppOverloadFit's.) */ function cppArgumentType(arg: string, ref: UnresolvedRef, context: ResolutionContext): CppArgumentType | null { @@ -11096,14 +11212,6 @@ function cppArgumentType(arg: string, ref: UnresolvedRef, context: ResolutionCon if (!written || !decl.callers || decl.shadowed) return null; return { kind: 'type', text: address ? `${written}*` : written }; } - // `keys[i]`: an element of the container, or what the pointer points at, a name the calling function declares is. - const subscript = /^([A-Za-z_]\w*)\s*\[[^[\]]*\]$/.exec(text); - if (subscript) { - const container = cppExpressionType(subscript[1]!, ref, context, 0); - const followed = container && cppFollowWrittenAliases(container, ref, context); - const element = followed && (isCppPointerType(followed.text) ? cppPointee(followed) : cppRangeElementOf(followed, ref, context)); - return element ? { kind: 'type', text: element.text } : null; - } const typed = /^(?:u8|u|U|L)?R?["']/.test(text) ? null : cppExpressionType(text, ref, context, 0); return typed ? { kind: 'type', text: typed.text } : null; } @@ -11361,12 +11469,13 @@ function cppRebindsReceiver(line: string, escapedReceiver: string): boolean { new RegExp(String.raw`\bauto\s*&{0,2}\s*\[[^\]]*\b${escapedReceiver}\b[^\]]*\]`).test(line); } -/** Record the C++ declaration receiver inference settles on. */ -function noteCppDeclaration(found: CppReceiverDeclaration | undefined, raw: string, callers: boolean): void { +/** Record the C++ declaration receiver inference settles on, with the array dimensions its declarator gives it. */ +function noteCppDeclaration(found: CppReceiverDeclaration | undefined, raw: string, callers: boolean, array = 0): void { if (!found) return; found.raw = raw; found.callers = callers; found.pointer = isCppPointerType(raw); + found.array = array > 0 ? array : undefined; } /** @@ -11446,7 +11555,8 @@ function inferCppReceiverType( found.shadowed = true; } else if (normalized) { const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, ref, context); - noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); + const array = cppArrayDimensions(line.slice(declaratorMatch.index + declaratorMatch[0].length)); + noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope, array); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); } else if (cppTypeBeganAbove(declaratorMatch[1] ?? '')) { // The end of a declaration begun on an earlier line, maybe the @@ -11526,7 +11636,8 @@ function inferCppReceiverType( const normalized = normalizeCppTypeName(declaratorMatch[1] ?? ''); if (normalized && normalized !== 'auto') { const inCallerScope = isCppCallersDeclaration(headerPath, i + 1, ref, context); - noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); + const array = cppArrayDimensions(line.slice(declaratorMatch.index + declaratorMatch[0].length)); + noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope, array); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); } }