From 76592cc1c580b5951c44d73df87e6ad0eae6e177 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sun, 11 Oct 2026 10:55:08 -0500 Subject: [PATCH] fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Receiver typing resolved a C++ call on a variable, parameter or member by the declared type's last name: `Iterator* iter; iter->Valid()` became resolveMethodOnType('Iterator', 'Valid'), which takes any class named `Iterator` that declares `Valid`. facebook/rocksdb has 22 such classes, and the `Iterator` the code means inherits `Valid`, `Next` and `key` from `IteratorBase`, so nested namesakes like `MemTableRep::Iterator` took the calls (google/leveldb's went to `SkipList::Iterator`). A declaration the calling function or its class writes, called with the operator it takes (`->` on a pointer, `.` on a value or reference), now goes through the same C++ lookup #2440 uses for smart pointers' held types (cppClassWritten from the caller's scopes, then the file's `using`s, aliases followed) and cppMethodOf (the class's own method, else one through its own base edges): - An alias #2399 followed from the caller (function-local typedefs included) is the class it names, by qualified name. An `auto` loop's element type (#2492) is looked up where its range's type is written. - A class found only as the one class of that name the file can see, a template parameter's type, a library type, or a type nothing visible declares: the call goes on as before. - A class with no such method gets no edge, unless it is a class template or a base on the way is one the index doesn't link (a `decltype(…)` base, an alias each `#if` arm declares differently), which may declare it: then as before. - Declarations read from elsewhere, or past one the scan couldn't read, are unchanged. cppClassWritten now tries the file's `using`s before the global scope: protobuf's upb generator writes `using google::protobuf::Descriptor;` inside its namespace, and the PHP extension declares a global C struct `Descriptor` in a header. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-declared-type-receiver.test.ts | 410 +++++++++++++++++++ src/resolution/cpp-supertypes.ts | 22 +- src/resolution/cpp-type-aliases.ts | 5 + src/resolution/name-matcher.ts | 106 ++++- 5 files changed, 528 insertions(+), 16 deletions(-) create mode 100644 __tests__/cpp-declared-type-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a5c3eb1f..7c288aa6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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>`, the call no longer links to a project method. 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. Re-index C++ projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. - In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged. - In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading. diff --git a/__tests__/cpp-declared-type-receiver.test.ts b/__tests__/cpp-declared-type-receiver.test.ts new file mode 100644 index 0000000000..0efc38674f --- /dev/null +++ b/__tests__/cpp-declared-type-receiver.test.ts @@ -0,0 +1,410 @@ +/** + * A C++ call on a variable, parameter or member the calling function or its + * class declares — `Iterator* iter; iter->Valid()`, `const Iterator& it; + * it.Valid()` — calls a member of the class C++ name lookup finds for the + * declared type where the call is written. + * + * Receiver typing used to look the method up by the type's last name, and + * took any class of that name that declares it. facebook/rocksdb has 22 + * classes named `Iterator`; the `Iterator` the code means + * (`ROCKSDB_NAMESPACE::Iterator`) inherits `Valid`, `Next` and `key` from + * `IteratorBase`, so a nested namesake that declares them — + * `MemTableRep::Iterator` — took about 1,100 calls. google/leveldb's + * `Iterator*` calls went to `SkipList::Iterator` the same way. + * + * The type is now looked up as for a smart pointer's held type (#2440): from + * the calling function's scopes outwards, then the file's `using`s, then the + * global scope, with aliases followed. The method is the class's own, or + * one a class it derives from declares. A class with no such method gets no + * edge: protocolbuffers/protobuf's descriptor accessors aren't indexed, and + * `field->full_name()` was guessed onto unrelated classes. When the lookup + * can't place the type — a template parameter, a class only another source + * file defines — or the declaration may be another variable's, the call + * resolves as before. + */ +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-declared-')); + 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(); +} + +const lines = (...l: string[]): string => [...l, ''].join('\n'); + +/** rocksdb's iterator family, trimmed: the public Iterator, which inherits its cursor methods, and a nested namesake that declares them. */ +const ROCKSDB = { + 'include/rocksdb/iterator_base.h': lines( + 'namespace rocksdb {', + 'class IteratorBase {', + ' public:', + ' virtual ~IteratorBase() {}', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/iterator.h': lines( + '#include "include/rocksdb/iterator_base.h"', + 'namespace rocksdb {', + 'class Iterator : public IteratorBase {', + ' public:', + ' virtual int value() const = 0;', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/memtablerep.h': lines( + 'namespace rocksdb {', + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' virtual bool Valid() const = 0;', + ' virtual void Next() = 0;', + ' };', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/db.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class DB {', + ' public:', + ' virtual Iterator* NewIterator() = 0;', + '};', + '} // namespace rocksdb', + ), +}; + +describe('a call on a receiver the caller declares reaches the class its type names', () => { + it('reproduction: `Iterator* iter; iter->Valid()` is the Iterator the scope sees, not a nested namesake', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_impl.cc': lines( + '#include "include/rocksdb/db.h"', + '#include "include/rocksdb/memtablerep.h"', + 'namespace rocksdb {', + 'int CountKeys(DB* db) {', + ' Iterator* iter = db->NewIterator();', + ' int n = 0;', + ' for (; iter->Valid(); iter->Next()) n += iter->value();', + ' delete iter;', + ' return n;', + '}', + '} // namespace rocksdb', + ), + }); + try { + // `Valid` and `Next` are IteratorBase's, which Iterator derives from; not MemTableRep::Iterator's. + expect(calls(cg, 'rocksdb::CountKeys')).toEqual([ + 'rocksdb::DB::NewIterator', + 'rocksdb::Iterator::value', + 'rocksdb::IteratorBase::Next', + 'rocksdb::IteratorBase::Valid', + ]); + } finally { + cg.close(); + } + }); + + it('a reference parameter, a value, and a member the class declares in its header', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'util/bit_fields.h': lines( + 'namespace rocksdb {', + 'class BitFields {', + ' public:', + ' int Get() const { return 0; }', + '};', + 'class ConcurrentTest {', + ' public:', + ' struct State {', + ' int Get() const { return 1; }', + ' };', + '};', + '} // namespace rocksdb', + ), + 'db/compaction_job.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class CompactionJob {', + ' public:', + ' bool Advance();', + ' private:', + ' Iterator* input_;', + '};', + '} // namespace rocksdb', + ), + 'db/compaction_job.cc': lines( + '#include "db/compaction_job.h"', + '#include "include/rocksdb/memtablerep.h"', + '#include "util/bit_fields.h"', + 'namespace rocksdb {', + 'struct State : public BitFields {};', + 'bool IsValid(const Iterator& it) {', + ' return it.Valid();', + '}', + 'int Slot() {', + ' State state;', + ' return state.Get();', + '}', + 'bool CompactionJob::Advance() {', + ' input_->Next();', + ' return input_->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::IsValid')).toEqual(['rocksdb::IteratorBase::Valid']); + // Not ConcurrentTest::State::Get, the one class named State that declares Get. + expect(calls(cg, 'rocksdb::Slot')).toEqual(['rocksdb::BitFields::Get']); + expect(calls(cg, 'rocksdb::CompactionJob::Advance')).toEqual(['rocksdb::IteratorBase::Next', 'rocksdb::IteratorBase::Valid']); + } finally { + cg.close(); + } + }); + + it('a nested class is found first from inside a class that inherits it', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'memtable/skiplistrep.cc': lines( + '#include "include/rocksdb/iterator.h"', + '#include "include/rocksdb/memtablerep.h"', + 'namespace rocksdb {', + 'class SkipListRep : public MemTableRep {', + ' public:', + ' bool Check();', + ' private:', + ' Iterator* iter_;', + '};', + 'bool SkipListRep::Check() { return iter_->Valid(); }', + '} // namespace rocksdb', + ), + }); + try { + // MemTableRep's own `Iterator`, which SkipListRep inherits, hides rocksdb::Iterator. + expect(calls(cg, 'rocksdb::SkipListRep::Check')).toEqual(['rocksdb::MemTableRep::Iterator::Valid']); + } finally { + cg.close(); + } + }); + + it('a `using` in the namespace around the call comes before a global class of that name', async () => { + // protocolbuffers/protobuf's upb generator writes `using + // google::protobuf::Descriptor;` inside `namespace upb::generator`, and its + // PHP extension declares a C struct `Descriptor` in a header. + const cg = await indexed({ + 'php/ext/google/protobuf/def.h': lines( + 'typedef struct Descriptor {', + ' int index;', + '} Descriptor;', + ), + 'src/google/protobuf/descriptor.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Descriptor {', + ' public:', + ' int FindFieldByNumber(int number) const { return number; }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'upb_generator/common/cpp_to_upb_def.cc': lines( + '#include "src/google/protobuf/descriptor.h"', + 'namespace upb {', + 'namespace generator {', + 'using google::protobuf::Descriptor;', + 'int FindField(const Descriptor* message, int number) {', + ' return message->FindFieldByNumber(number);', + '}', + '} // namespace generator', + '} // namespace upb', + ), + }); + try { + expect(calls(cg, 'upb::generator::FindField')).toEqual(['google::protobuf::Descriptor::FindFieldByNumber']); + } finally { + cg.close(); + } + }); +}); + +describe('a class with no such method gets no guess', () => { + it('a method the class only inherits from a library type', async () => { + // rocksdb's `MultiScanException& ex; ex.what()` reached the Java bindings' `KVException::what`. + const cg = await indexed({ + 'java/rocksjni/kv_helper.h': lines( + 'namespace rocksdb {', + 'class KVException {', + ' public:', + ' const char* what() const { return ""; }', + '};', + '} // namespace rocksdb', + ), + 'include/rocksdb/multi_scan.h': lines( + '#include ', + 'namespace rocksdb {', + 'class MultiScanException : public std::runtime_error {', + ' public:', + ' explicit MultiScanException(const char* message) : std::runtime_error(message) {}', + '};', + '} // namespace rocksdb', + ), + 'tools/db_bench_tool.cc': lines( + '#include "include/rocksdb/multi_scan.h"', + 'namespace rocksdb {', + 'const char* Describe(const MultiScanException& ex) {', + ' return ex.what();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Describe')).toEqual([]); + } finally { + cg.close(); + } + }); +}); + +describe('what keeps the call as it was', () => { + it('a template parameter, and a class another source file defines', async () => { + // rocksdb's multi-CF iterator takes its heap type as a template parameter + // named like the class it is used with; version_set.cc defines its file + // pickers, then includes the header whose code uses them. + const cg = await indexed({ + 'util/heap.h': lines( + 'namespace rocksdb {', + 'class BinaryHeap {', + ' public:', + ' void push(int v) {}', + '};', + '} // namespace rocksdb', + ), + 'db/multi_cf_iterator_impl.h': lines( + '#include "util/heap.h"', + 'namespace rocksdb {', + 'template ', + 'class MultiCfIteratorImpl {', + ' public:', + ' void Reset(BinaryHeap& heap) { heap.push(1); }', + '};', + '} // namespace rocksdb', + ), + 'db/version_set.cc': lines( + 'namespace rocksdb {', + 'class FilePicker {', + ' public:', + ' int GetNextFile() { return 0; }', + '};', + '} // namespace rocksdb', + '#include "db/version_set_sync_and_async.h"', + ), + 'db/version_set_sync_and_async.h': lines( + 'namespace rocksdb {', + 'int NextFile() {', + ' FilePicker fp;', + ' return fp.GetNextFile();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiCfIteratorImpl::Reset')).toEqual(['rocksdb::BinaryHeap::push']); + expect(calls(cg, 'rocksdb::NextFile')).toEqual(['rocksdb::FilePicker::GetNextFile']); + } finally { + cg.close(); + } + }); + + it('a class with a base the index does not link, which may declare the method', async () => { + // protobuf's FlatAllocator derives from a `decltype(…)` that names FlatAllocatorImpl. + const cg = await indexed({ + 'src/google/protobuf/descriptor.cc': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'template ', + 'class FlatAllocatorImpl {', + ' public:', + ' void PlanArray(int array_size) {}', + '};', + 'template ', + 'FlatAllocatorImpl ApplyTypeList(int);', + 'class FlatAllocator : public decltype(ApplyTypeList(0)) {};', + '} // namespace internal', + 'void PlanAllocationSize(internal::FlatAllocator& alloc) {', + ' alloc.PlanArray(1);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::PlanAllocationSize')).toEqual(['google::protobuf::internal::FlatAllocatorImpl::PlanArray']); + } finally { + cg.close(); + } + }); + + it('a declaration read from outside the calling function and its class', async () => { + // Reading back from the call reaches an earlier function's local of the + // same name, not the global the call uses, so its type is no evidence. + const cg = await indexed({ + ...ROCKSDB, + 'db/table_cursor.h': lines( + '#include "include/rocksdb/memtablerep.h"', + 'namespace rocksdb {', + 'extern MemTableRep::Iterator* iter;', + '} // namespace rocksdb', + ), + 'db/db_iter.cc': lines( + '#include "include/rocksdb/iterator.h"', + '#include "db/table_cursor.h"', + 'namespace rocksdb {', + 'void Prepare() {', + ' Iterator* iter = nullptr;', + '}', + 'bool Step() {', + ' return iter->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Step')).toEqual(['rocksdb::MemTableRep::Iterator::Valid']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-supertypes.ts b/src/resolution/cpp-supertypes.ts index f7d895c4a9..2ba4bfa000 100644 --- a/src/resolution/cpp-supertypes.ts +++ b/src/resolution/cpp-supertypes.ts @@ -13,14 +13,19 @@ * * The name is looked up as C++ looks it up: * - from the scope the class is declared in (its own members are not in - * scope in its base clause), outwards to the global scope; a class scope - * includes its bases, a qualified name is resolved a segment at a time and - * an alias is followed to the class it names (cpp-type-aliases.ts); - * `::Base` is the global one; + * scope in its base clause), outwards to the outermost namespace; a class + * scope includes its bases, a qualified name is resolved a segment at a + * time and an alias is followed to the class it names + * (cpp-type-aliases.ts); `::Base` is the global one; * - then through what the file writes before the class: a namespace alias * (`namespace _pbi = ::google::protobuf::internal;`), a using-declaration * (`using leveldb::FilterPolicy;`), a using-directive (`using namespace - * ROCKSDB_NAMESPACE;`); and through a namespace a macro opens + * ROCKSDB_NAMESPACE;`). These come before the global scope: they put the + * name in a namespace around the code (`namespace upb::generator { using + * google::protobuf::Descriptor; }`), and a global declaration of it is one + * the file can't see at the same time (protobuf's PHP extension declares a + * C struct `Descriptor`); + * - then the global scope, and a namespace a macro opens * (`fmt::detail::buffer` under fmt's FMT_BEGIN_NAMESPACE); * - a class a source file (`.cc`, `.cpp`, `.c`) defines belongs to that * translation unit, so only that file sees it (leveldb's skiplist test @@ -132,8 +137,8 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol const scopes = cppScopesWithin(site.scope); const resolved = (node: Node): CppWrittenClass => ({ node, exact: true }); - // The scopes around the name, or only the global one for `::Base`. - const lexical = classNode(cppClassNamed(written, global ? [''] : scopes, ref, context, visible), site, context); + // The scopes around the name short of the global one, or only the global one for `::Base`. + const lexical = classNode(cppClassNamed(written, global ? [''] : scopes.slice(0, -1), ref, context, visible), site, context); if (lexical !== undefined) return lexical && resolved(lexical); // What the file writes before the name, the nearest first: each stands for @@ -166,6 +171,9 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol const hit = lookUp(`${u.path}::${written}`); if (hit) return resolved(hit); } + // The global scope. + const outermost = classNode(cppClassNamed(written, [''], ref, context, visible), site, context); + if (outermost !== undefined) return outermost && resolved(outermost); } // `fmt::detail::buffer`, where a macro opens `namespace fmt` around `detail::buffer`. if (segments.length > 1) { diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index f93aa4c113..85de85939b 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -503,6 +503,11 @@ export function cppAliasedTypeName(resolved: CppAliasedType): string { return [resolved.target[resolved.target.length - 1]!, ...resolved.rest].join('::'); } +/** The qualified name of the project class a type resolved through aliases is, if it is one: `leveldb::SkipList::Iterator`. */ +export function cppAliasedClass(resolved: CppAliasedType, context: ResolutionContext): string | undefined { + return classNamed([...resolved.target, ...resolved.rest], scopesWithin(resolved.scope), context); +} + /** What a C++ type name names when it is looked up for a class (see cppClassNamed). */ export type CppNamedClass = /** A class, by its qualified name. */ diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 3b80716eb6..6df55a82ef 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,9 +14,9 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries'; import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; -import { cppAliasedTypeName, cppAliasedTypeWritten, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; +import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases'; import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; -import { cppClassWritten } from './cpp-supertypes'; +import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; @@ -9392,6 +9392,8 @@ interface CppReceiverDeclaration { * particular class. */ aliased?: 'followed' | 'unreadable'; + /** The type a `followed` alias names, looked up from the calling function (its own typedefs included). */ + alias?: CppAliasedType; /** The type as written in the declaration, without following an alias (`Table`). */ written?: string; /** The type is reached through a pointer (`Table* t`, `using Field = const FieldDescriptor*;`). */ @@ -9446,6 +9448,7 @@ function cppDeclaredType( if (aliased === undefined) return normalized; if (found) { found.aliased = aliased ? 'followed' : 'unreadable'; + if (aliased) found.alias = aliased; found.written = normalized; found.pointer = aliased ? aliased.pointer : isCppPointerType(raw); found.classTemplate = aliased?.classTemplate ?? false; @@ -9586,13 +9589,7 @@ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRe if (!caller || isCppPointerType(held)) return undefined; if (isCppLibraryType(held, context)) return null; // A template parameter's type (`std::unique_ptr`, `typename T::Iter`) names no particular class: null. - const found = cppClassWritten(held, { - // A class local to the calling function is found first. - scope: caller.kind === 'file' ? '' : caller.qualifiedName, - file: ref.filePath, - line: ref.line, - parameters: cppTemplateParameters(caller, context), - }, ref, context); + const found = cppClassWritten(held, cppCallSite(caller, ref, context), ref, context); if (found === undefined) return undefined; if (found === null) return null; if (!CPP_CLASS_KINDS.has(found.node.kind)) { @@ -9612,6 +9609,89 @@ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRe const CPP_CLASS_KINDS: ReadonlySet = new Set(['class', 'struct', 'union']); +/** Where a type the calling function writes is looked up from: the function itself, so a class local to it is found first. */ +function cppCallSite(caller: Node, ref: UnresolvedRef, context: ResolutionContext): CppNameSite { + return { + scope: caller.kind === 'file' ? '' : caller.qualifiedName, + file: ref.filePath, + line: ref.line, + parameters: cppTemplateParameters(caller, context), + }; +} + +/** + * A C++ member call on a receiver the calling function or its class declares + * — `Iterator* iter; iter->Valid()`, `const Slice& key; key.size()` — with the + * operator that declaration takes (`->` on a pointer, `.` on a value or + * reference) calls a member of the class the declared type names, looked up as + * for a smart pointer's held type (matchCppHeldTypeCall); a type named through + * an alias is the class the alias names. The type's last name alone finds + * namesakes: rocksdb's `Iterator* iter; iter->Valid()` reached + * `MemTableRep::Iterator::Valid`, one of 22 classes named `Iterator`, while the + * `Iterator` the code means inherits `Valid` from `IteratorBase`. + * - The member of that class, or of a class it derives from. + * - null when that class has no such method: protobuf's descriptor accessors + * aren't indexed, and `field->full_name()` reached unrelated classes' by + * the method's name. (A class template's specialization may declare it, so + * not there.) + * - undefined when the declaration may be another variable's (read from + * elsewhere, past one of the receiver the scan couldn't read, or not taking + * that operator), or the lookup doesn't place the type: a library type, a + * template parameter's, an alias of a type outside the project, a name + * nothing the file sees declares, or one only the class of that name the + * file can see shares (rocksdb's out-of-line `InlineSkipList::Node` is + * indexed unnamed, so its `Node` matched `FlushScheduler::Node`). The call + * goes on as before. + */ +function matchCppDeclaredTypeCall( + receiver: string, + methodName: string, + decl: CppReceiverDeclaration, + ref: UnresolvedRef, + context: ResolutionContext, +): ResolvedRef | null | undefined { + if (!decl.callers || decl.shadowed || !decl.raw || isCppLibraryType(decl.raw, context)) return undefined; + if (cppMemberOperator(receiver, ref, context) !== (decl.pointer ? '->' : '.')) return undefined; + const caller = context.getNodeById?.(ref.fromNodeId); + if (!caller) return undefined; + // An alias was followed from the calling function, through typedefs of its + // own the index doesn't hold: the class it names, by its qualified name. + const aliasedClass = decl.alias && cppAliasedClass(decl.alias, context); + if (decl.aliased && !aliasedClass) return undefined; + // An `auto` loop's element type is looked up where its range's type is written. + const site = decl.element ?? cppCallSite(caller, ref, context); + const found = cppClassWritten(aliasedClass ? `::${aliasedClass}` : decl.raw, site, ref, context); + if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return undefined; + const method = cppMethodOf(found.node, methodName, ref, context); + if (method) return { original: ref, targetNodeId: method.id, confidence: 0.9, resolvedBy: 'instance-method' }; + return cppTemplateParameters(found.node, context).size === 0 && cppBasesKnown(found.node, context) ? null : undefined; +} + +/** + * Whether every base C++ class `cls` writes, and every base of those, is one + * cppMethodOf walks — a base edge to a class — or a library type such as + * `std::runtime_error`, which declares no project method. A base the index + * doesn't link may declare any method: protobuf's `FlatAllocator` derives + * from a `decltype(…)` that names `FlatAllocatorImpl`, and rocksdb's + * `WritePreparedTxnDB` from an alias each `#if` arm declares differently. + */ +function cppBasesKnown(cls: Node, context: ResolutionContext, depth = 0, seen = new Set()): boolean { + if (seen.has(cls.qualifiedName)) return true; + seen.add(cls.qualifiedName); + if (!context.getSupertypeNodes) return false; + for (const decl of context.getNodesByQualifiedName(cls.qualifiedName)) { + if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) continue; + const bases = context.getSupertypeNodes(decl.id); + const written = cppBaseSpecifiers(decl, context).filter((s) => !isCppLibraryType(s, context)); + // (cppMethodOf looks no deeper than this.) + if (depth >= 4 ? bases.length + written.length > 0 : written.length > bases.length) return false; + for (const base of bases) { + if (!CPP_CLASS_KINDS.has(base.kind) || !cppBasesKnown(base, context, depth + 1, seen)) return false; + } + } + return true; +} + /** * C++ class `cls`'s method `name`: its own, else the nearest one of a class * it derives from, through the base edges every declaration of `cls` has. @@ -11812,6 +11892,14 @@ export function matchMethodCall( ref.language === 'cpp' ? inferCppReceiverType(objectOrClass!, ref, context, 0, cppDecl) : inferLocalReceiverType(objectOrClass!, ref, context, decl)); + // A C++ receiver the calling function or its class declares calls a + // member of the class C++ name lookup finds for its type, not of any + // class sharing the type's last name. + if (ref.language === 'cpp') { + const declared = nmTimedT('mc-cppdecl', ref, () => + matchCppDeclaredTypeCall(objectOrClass!, methodName!, cppDecl, ref, context)); + if (declared !== undefined) return declared; + } // A pytest test's parameter is what its fixture returns: flaskbb's // `cli_runner.invoke(…)` is click's `CliRunner`, not the project's one `invoke`. if (!inferredType && ref.language === 'python' && dotMatch) inferredType = pythonFixtureReturnType(objectOrClass!, ref, context);