From 6da4f7693adea20d96e3035d47a0be084ed0acd2 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Sun, 11 Oct 2026 10:18:10 -0500 Subject: [PATCH] fix(cpp): a range-based for loop over `auto` elements has the element type of its range #2439 typed a range-for variable when the loop names its type. A loop over `auto` elements names none, so receiver inference kept scanning as for an `auto` local with an unreadable initializer: it reached an earlier declaration of the same name, often another variable's, or fell through to the guess by the receiver's name. facebook/rocksdb's db_impl_write.cc loops `for (auto cfd : cfds)` over a `const autovector&`. The loop's range is now read from its header and typed as written, template arguments and all, since the index keeps only a return type's last name: - a name the calling function declares, or a data member of its class read from the class's own lines (`versions_` from db_impl_write.cc); - an `auto` local's initializer; - `*x`, `x.m`, `x->m`, `this`, and calls whose declaration writes their return type, free or on the object's class (own or inherited); - aliases followed with their template arguments (cppAliasedTypeWritten); - the element: a standard or abseil sequence container's first template argument, protobuf's RepeatedPtrField and rocksdb's autovector's, or what a project class's begin() returns, dereferenced (ColumnFamilySet's iterator's operator* gives ColumnFamilyData*). Nullability annotations are dropped. The element goes through the same declaration record as any declared receiver, so the typed path, #2413's gate for a standard-library element and #2440's held-type lookup for `->` on a smart-pointer element apply unchanged. Classes are looked up with #2440's cppClassWritten, methods through its cppMethodOf, and matchCppHeldTypeCall now reads its held type through the shared cppStdHeldType. A/B on main 8a5ba3d0, site-keyed: leveldb identical; fmt +1 / 3 retargeted; rocksdb -68 / +173 / 104 retargeted / 231 confidence only; protobuf -13 / +25 / 7 / 59. Every removal and retarget was a wrong guess; 4 of rocksdb's additions reach MemTableRep::Iterator through the typed path's last-name lookup, as a declared `Iterator* it` does on main. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- __tests__/cpp-auto-range-for-receiver.test.ts | 533 ++++++++++++++++++ src/resolution/cpp-type-aliases.ts | 30 + src/resolution/name-matcher.ts | 432 +++++++++++++- 4 files changed, 978 insertions(+), 20 deletions(-) create mode 100644 __tests__/cpp-auto-range-for-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a984c54d79..c7a5c3eb1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,9 +86,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a class, struct or method now keeps the namespaces and the class it is declared in when the parser misreads something earlier in the file, such as an unknown macro in front of a member or in a class header. Before, everything after the misread code could lose them, or land inside a class it isn't in: in protocolbuffers/protobuf, `FieldDescriptor` was indexed without `google::protobuf::`, and in RocksDB, the `Opts` struct declared inside a cache table class was indexed outside it, so finding a class's base classes and the methods called on its objects fell back to guessing by name. Methods that had come loose from their class are its members again, and classes the misread code had hidden, like leveldb's POSIX file and environment classes, are now indexed. Re-index C++ projects after upgrading. - 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. 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, and loops over `auto` elements resolve as before. 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>`, 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. - 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-auto-range-for-receiver.test.ts b/__tests__/cpp-auto-range-for-receiver.test.ts new file mode 100644 index 0000000000..a03fa01b22 --- /dev/null +++ b/__tests__/cpp-auto-range-for-receiver.test.ts @@ -0,0 +1,533 @@ +/** + * A C++ range-based for over `auto` elements has the element type of the + * range it iterates. + * + * #2439 read the type a loop declares, `for (ConformanceTestSuite *suite : + * suites)`. A loop over `auto` elements declares none, so the scan went on + * as for an `auto` local whose initializer it can't read: it reached an + * earlier declaration of the same name, often another variable's, or the + * call fell through to a guess by the receiver's name. In facebook/rocksdb, + * `for (auto cfd : cfds)` iterates a `const autovector&` + * parameter. The element type is read from the range where that can be done + * reliably: + * - a name the calling function declares, or a data member of its class: + * a container's first template argument, aliases followed with their + * template arguments; + * - a call whose declaration writes its return type, `*x`, `x.m`, `x->m`; + * - one of the project's classes, through what its `begin()` returns: + * rocksdb's `ColumnFamilySet`, whose iterator's `operator*` returns a + * `ColumnFamilyData*`. + * A guess can still land on the right method by a shared word, so these tests + * check the typed path's confidence, not only the target. + */ +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-auto-range-for-')); + 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 @confidence`, one per call site. */ +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} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`) + .sort(); +} + +const lines = (...text: string[]): string => [...text, ''].join('\n'); + +/** + * facebook/rocksdb's column families, trimmed. `ColumnFamilyHandle` has + * methods of the same names; `DBImpl` is declared in db_impl.h and its + * methods are defined in db_impl_write.cc, so the scan's same-stem header + * never reads `versions_`. + */ +const ROCKSDB = { + 'util/autovector.h': lines( + '#include ', + 'namespace rocksdb {', + 'template ', + 'class autovector {', + ' public:', + ' size_t size() const { return 0; }', + ' bool empty() const { return true; }', + '};', + '} // namespace rocksdb', + ), + 'db/column_family.h': lines( + '#include ', + 'namespace rocksdb {', + 'class ColumnFamilyData {', + ' public:', + ' bool IsDropped() const;', + ' void Ref();', + ' uint32_t GetID() const;', + '};', + 'class ColumnFamilySet {', + ' public:', + ' class iterator {', + ' public:', + ' explicit iterator(ColumnFamilyData* cfd) : current_(cfd) {}', + ' iterator& operator++() { current_ = nullptr; return *this; }', + ' bool operator!=(const iterator& other) const { return current_ != other.current_; }', + ' ColumnFamilyData* operator*() { return current_; }', + ' private:', + ' ColumnFamilyData* current_;', + ' };', + ' iterator begin() { return iterator(head_); }', + ' iterator end() { return iterator(nullptr); }', + ' private:', + ' ColumnFamilyData* head_ = nullptr;', + '};', + 'class ColumnFamilyHandle {', + ' public:', + ' bool IsDropped() const;', + ' void Ref();', + ' uint32_t GetID() const;', + '};', + '} // namespace rocksdb', + ), + 'db/column_family.cc': lines( + '#include "db/column_family.h"', + 'namespace rocksdb {', + 'bool ColumnFamilyData::IsDropped() const { return false; }', + 'void ColumnFamilyData::Ref() {}', + 'uint32_t ColumnFamilyData::GetID() const { return 0; }', + 'bool ColumnFamilyHandle::IsDropped() const { return false; }', + 'void ColumnFamilyHandle::Ref() {}', + 'uint32_t ColumnFamilyHandle::GetID() const { return 0; }', + '} // namespace rocksdb', + ), + 'db/version_set.h': lines( + '#include ', + '#include "db/column_family.h"', + 'namespace rocksdb {', + 'class VersionSet {', + ' public:', + ' ColumnFamilySet* GetColumnFamilySet() { return column_family_set_.get(); }', + ' private:', + ' std::unique_ptr column_family_set_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl.h': lines( + '#include ', + '#include "util/autovector.h"', + '#include "db/version_set.h"', + 'namespace rocksdb {', + 'class DBImpl {', + ' public:', + ' void AssignAtomicFlushSeq(const autovector& cfds);', + ' void ScheduleFlushes();', + ' private:', + ' std::unique_ptr versions_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl_write.cc': lines( + '#include "db/db_impl.h"', + 'namespace rocksdb {', + 'void UseHandle(ColumnFamilyHandle* cfd) {', + ' cfd->Ref();', + '}', + 'void DBImpl::AssignAtomicFlushSeq(const autovector& cfds) {', + ' for (auto cfd : cfds) {', + ' cfd->GetID();', + ' }', + '}', + 'void DBImpl::ScheduleFlushes() {', + ' for (auto cfd : *versions_->GetColumnFamilySet()) {', + ' if (!cfd->IsDropped()) cfd->Ref();', + ' }', + '}', + '} // namespace rocksdb', + ), +}; + +/** protocolbuffers/protobuf's descriptors, trimmed; `upb` has a `FieldDefPtr` with the same accessors. */ +const PROTOBUF = { + 'src/google/protobuf/descriptor.h': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'class FieldDescriptor {', + ' public:', + ' int number() const;', + ' const std::string& full_name() const;', + '};', + 'class Descriptor {', + ' public:', + ' int field_count() const;', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/descriptor.cc': lines( + '#include "google/protobuf/descriptor.h"', + 'namespace google {', + 'namespace protobuf {', + 'int FieldDescriptor::number() const { return 0; }', + 'const std::string& FieldDescriptor::full_name() const { static std::string s; return s; }', + 'int Descriptor::field_count() const { return 0; }', + '} // namespace protobuf', + '} // namespace google', + ), + 'upb/reflection/def.hpp': lines( + 'namespace upb {', + 'class FieldDefPtr {', + ' public:', + ' int number() const { return 0; }', + ' const char* full_name() const { return ""; }', + '};', + '} // namespace upb', + ), + 'src/google/protobuf/compiler/cpp/field_layout.h': lines( + '#include ', + '#include "absl/types/span.h"', + '#include "google/protobuf/descriptor.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace cpp {', + 'std::vector FieldNumberOrder(const Descriptor* descriptor);', + 'class FieldLayout {', + ' public:', + ' absl::Span optimized_order() const { return {}; }', + '};', + '} // namespace cpp', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/cpp/message.cc': lines( + '#include "google/protobuf/compiler/cpp/field_layout.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace cpp {', + 'std::vector FieldNumberOrder(const Descriptor* descriptor) {', + ' return {};', + '}', + 'int Numbers(const Descriptor* descriptor) {', + ' int sum = 0;', + ' for (const auto* field : FieldNumberOrder(descriptor)) {', + ' sum += field->number();', + ' }', + ' return sum;', + '}', + 'void Names(const FieldLayout& layout) {', + ' for (const auto* field : layout.optimized_order()) {', + ' field->full_name();', + ' }', + '}', + '} // namespace cpp', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), +}; + +describe('an `auto` range-for variable has the element type of its range', () => { + it('reproduction: rocksdb\'s `for (auto cfd : cfds)` over an autovector parameter', async () => { + const cg = await indexed(ROCKSDB); + try { + // The scan went past the loop to UseHandle's `ColumnFamilyHandle* cfd`. + expect(calls(cg, 'rocksdb::DBImpl::AssignAtomicFlushSeq')).toEqual(['rocksdb::ColumnFamilyData::GetID @0.9']); + expect(calls(cg, 'rocksdb::UseHandle')).toEqual(['rocksdb::ColumnFamilyHandle::Ref @0.9']); + } finally { + cg.close(); + } + }); + + it('a project class iterated through its begin(), reached from a member another header declares', async () => { + const cg = await indexed(ROCKSDB); + try { + // `versions_` is DBImpl's `std::unique_ptr`; GetColumnFamilySet() + // returns a `ColumnFamilySet*`, whose iterator's `operator*` returns a + // `ColumnFamilyData*`. (The range's own call is left out here: no + // receiver scan reads db_impl.h from db_impl_write.cc.) + expect(calls(cg, 'rocksdb::DBImpl::ScheduleFlushes').filter((c) => !c.startsWith('rocksdb::VersionSet::'))).toEqual([ + 'rocksdb::ColumnFamilyData::IsDropped @0.9', + 'rocksdb::ColumnFamilyData::Ref @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('a call whose declaration writes its return type, and a nullability annotation in an element', async () => { + const cg = await indexed(PROTOBUF); + try { + expect(calls(cg, 'google::protobuf::compiler::cpp::Numbers')).toEqual([ + 'google::protobuf::FieldDescriptor::number @0.9', + 'google::protobuf::compiler::cpp::FieldNumberOrder @0.9', + ]); + expect(calls(cg, 'google::protobuf::compiler::cpp::Names')).toEqual([ + 'google::protobuf::FieldDescriptor::full_name @0.9', + 'google::protobuf::compiler::cpp::FieldLayout::optimized_order @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('an `auto` local\'s initializer, an alias with template arguments, and a smart pointer element', async () => { + const cg = await indexed({ + 'db/blob_file_meta.h': lines( + '#include ', + 'namespace rocksdb {', + 'class BlobFileMetaData {', + ' public:', + ' uint64_t GetBlobFileNumber() const { return 0; }', + '};', + 'class BlobFileAddition {', + ' public:', + ' uint64_t GetBlobFileNumber() const { return 0; }', + '};', + '} // namespace rocksdb', + ), + 'db/version_storage.h': lines( + '#include ', + '#include ', + '#include "db/blob_file_meta.h"', + 'namespace rocksdb {', + 'class VersionStorageInfo {', + ' public:', + ' using BlobFiles = std::vector>;', + ' const BlobFiles& GetBlobFiles() const { return blob_files_; }', + ' private:', + ' BlobFiles blob_files_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_filesnapshot.cc': lines( + '#include "db/version_storage.h"', + 'namespace rocksdb {', + 'uint64_t Largest(const VersionStorageInfo* vstorage) {', + ' uint64_t largest = 0;', + ' const auto& blob_files = vstorage->GetBlobFiles();', + ' for (const auto& meta : blob_files) {', + ' largest = meta->GetBlobFileNumber();', + ' }', + ' return largest;', + '}', + '} // namespace rocksdb', + ), + }); + try { + // `->` on the `std::shared_ptr` element reaches the type it holds. + expect(calls(cg, 'rocksdb::Largest')).toEqual([ + 'rocksdb::BlobFileMetaData::GetBlobFileNumber @0.9', + 'rocksdb::VersionStorageInfo::GetBlobFiles @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('a data member of another object, and loops inside loops', async () => { + const cg = await indexed({ + 'db/compaction/subcompaction_state.h': lines( + 'namespace rocksdb {', + 'class Output {', + ' public:', + ' void Finish() {}', + '};', + 'class SubcompactionState {', + ' public:', + ' void RemoveLastEmptyOutput() {}', + ' Output outputs[2];', + '};', + 'class FlushJob {', + ' public:', + ' void RemoveLastEmptyOutput() {}', + '};', + '} // namespace rocksdb', + ), + 'db/compaction/compaction_state.h': lines( + '#include ', + '#include "db/compaction/subcompaction_state.h"', + 'namespace rocksdb {', + 'class CompactionState {', + ' public:', + ' std::vector sub_compact_states;', + '};', + '} // namespace rocksdb', + ), + 'db/compaction/compaction_job.h': lines( + '#include "db/compaction/compaction_state.h"', + 'namespace rocksdb {', + 'class CompactionJob {', + ' public:', + ' void RemoveEmptyOutputs();', + ' private:', + ' CompactionState* compact_;', + '};', + '} // namespace rocksdb', + ), + 'db/compaction/compaction_job.cc': lines( + '#include "db/compaction/compaction_job.h"', + 'namespace rocksdb {', + 'void CompactionJob::RemoveEmptyOutputs() {', + ' for (auto& state : compact_->sub_compact_states) {', + ' state.RemoveLastEmptyOutput();', + ' }', + '}', + 'void Finish(const std::vector& compactions) {', + ' for (const auto* compaction : compactions) {', + ' for (auto& state : compaction->sub_compact_states) {', + ' state.RemoveLastEmptyOutput();', + ' }', + ' }', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::CompactionJob::RemoveEmptyOutputs')).toEqual(['rocksdb::SubcompactionState::RemoveLastEmptyOutput @0.9']); + expect(calls(cg, 'rocksdb::Finish')).toEqual(['rocksdb::SubcompactionState::RemoveLastEmptyOutput @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('what an `auto` element calls when it is not the project\'s', () => { + const SLICE = { + 'util/slice.h': lines( + '#include ', + 'namespace rocksdb {', + 'class Slice {', + ' public:', + ' size_t size() const { return 0; }', + ' const char* c_str() const { return ""; }', + '};', + 'class TableReader {', + ' public:', + ' bool Valid() const { return true; }', + ' void reset() {}', + '};', + 'class TruncatedRangeDelIterator {', + ' public:', + ' bool Valid() const { return true; }', + '};', + '} // namespace rocksdb', + ), + }; + + it('a standard-library element\'s own member links to nothing', async () => { + const cg = await indexed({ + ...SLICE, + 'db/names.cc': lines( + '#include ', + '#include ', + '#include ', + '#include "util/slice.h"', + 'namespace rocksdb {', + 'size_t Total(const std::vector& files) {', + ' size_t total = 0;', + ' for (const auto& file : files) {', + ' total += file.size();', + ' }', + ' return total;', + '}', + '} // namespace rocksdb', + ), + }); + try { + // The guess took `Slice::size` by the method's name. + expect(calls(cg, 'rocksdb::Total')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('a smart pointer element: `->` reaches the type it holds, `.` the holder\'s own member', async () => { + const cg = await indexed({ + ...SLICE, + 'db/range_del_aggregator.h': lines( + '#include ', + '#include ', + '#include "util/slice.h"', + 'namespace rocksdb {', + 'class RangeDelAggregator {', + ' public:', + ' bool IsRangeOverlapped();', + ' void Reset();', + ' private:', + ' std::vector> iters_;', + '};', + '} // namespace rocksdb', + ), + 'db/range_del_aggregator.cc': lines( + '#include "db/range_del_aggregator.h"', + 'namespace rocksdb {', + 'bool RangeDelAggregator::IsRangeOverlapped() {', + ' for (auto& iter : iters_) {', + ' if (iter->Valid()) return true;', + ' }', + ' return false;', + '}', + 'void RangeDelAggregator::Reset() {', + ' for (auto& iter : iters_) iter.reset();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::RangeDelAggregator::IsRangeOverlapped')).toEqual(['rocksdb::TruncatedRangeDelIterator::Valid @0.9']); + // `std::unique_ptr::reset`, not TableReader's. + expect(calls(cg, 'rocksdb::RangeDelAggregator::Reset')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('an element a template argument decides is not read', async () => { + const cg = await indexed({ + ...SLICE, + 'db/each.h': lines( + '#include ', + '#include "util/slice.h"', + 'namespace rocksdb {', + 'template ', + 'bool AllValid(const std::vector& items) {', + ' for (auto* item : items) {', + ' if (!item->Valid()) return false;', + ' }', + ' return true;', + '}', + '} // namespace rocksdb', + ), + }); + try { + // T is whatever the caller iterates: no typed edge, as before. + expect(calls(cg, 'rocksdb::AllValid').filter((c) => c.endsWith('@0.9'))).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 040d6e9794..f93aa4c113 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -556,6 +556,36 @@ export function cppClassNamed( return next === name.names.length ? { alias } : null; } +/** + * The type the C++ alias written as `written` names, as the alias's + * declaration writes it, template arguments and all — `std::vector< + * std::shared_ptr>` for rocksdb's `BlobFiles` under `using + * BlobFiles = std::vector<…>;` — with the alias. The alias is looked up from + * `scopes` (innermost first), as cppClassNamed looks a name up, and only its + * own text is read: what that names is the caller's to look up in turn, + * from where the alias is declared. + * - `undefined`: `written` names no alias the lookup finds. + * - `null`: an alias whose type can't be read: one each `#if` arm declares + * differently, a member of the aliased type (`Table::Iterator`), an alias + * template, or a `decltype(…)` or `typename …` type. + */ +export function cppAliasedTypeWritten( + written: string, + scopes: readonly string[], + ref: UnresolvedRef, + context: ResolutionContext, +): { text: string; alias: Node } | null | undefined { + const name = cppTypeName(written); + if (!name) return undefined; + const { found, next } = lookupSegments(name, scopes, ref, context); + if (!found || typeof found !== 'object' || 'cls' in found) return undefined; + if ('ambiguous' in found || next !== name.names.length || isAliasTemplate(found.alias, context)) return null; + const text = declarationText(found.alias, context); + const aliased = text ? aliasedTypeIn(text, found.alias.name) : null; + if (!aliased || /\b(?:typename|decltype)\b/.test(aliased)) return null; + return { text: aliased, alias: found.alias }; +} + /** The scopes visible from inside the scope `qualified`, innermost first, ending with the global scope. */ export function cppScopesWithin(qualified: string): string[] { return scopesWithin(qualified); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index a49057f603..3b80716eb6 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -14,7 +14,7 @@ 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, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; +import { cppAliasedTypeName, cppAliasedTypeWritten, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments } from './cpp-type-aliases'; import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppNamespaceAliases } from './cpp-namespaces'; import { cppClassWritten } from './cpp-supertypes'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; @@ -8938,16 +8938,380 @@ const CPP_DECLARED_TYPE_TAIL = /([A-Za-z_][\w:]*(?:\s*<[^;=(){}]+>)?(?:\s*[*&]+) * loop's name is followed by its `:`. (Neither is a bit-field, `unsigned car * : 1;`, which no method is called on.) */ -function cppRangeForDeclaration(line: string, receiverName: string): { type: string; paren: number } | null { +function cppRangeForDeclaration(line: string, receiverName: string): { type: string; paren: number; colon: number } | null { if (!line.includes('for')) return null; for (const m of line.matchAll(CPP_RANGE_FOR_DECLARATION)) { if (m[2] !== receiverName) continue; const tail = m[1]!.match(CPP_DECLARED_TYPE_TAIL); - return tail ? { type: tail[1]!, paren: m.index! + m[0].indexOf('(') } : null; + return tail ? { type: tail[1]!, paren: m.index! + m[0].indexOf('('), colon: m.index! + m[0].length } : null; } return null; } +/** + * The range a range-based for iterates, as written — `*versions_-> + * GetColumnFamilySet()` — from the declaration's `:` at `lines[line][column]` + * to the header's closing `)`, comments dropped. Null when the header runs + * on past a few lines or holds a literal. + */ +function cppRangeExpression(lines: readonly string[], line: number, column: number): string | null { + let depth = 1; + let text = ''; + for (let l = line; l < Math.min(lines.length, line + 4); l++) { + const code = lines[l]!.replace(/\/\*.*?\*\//g, ' ').replace(/\/\/.*$/, ''); + for (let c = l === line ? column : 0; c < code.length; c++) { + const ch = code[c]!; + if (ch === '"' || ch === '\'') return null; + if (ch === '(') depth++; + else if (ch === ')' && --depth === 0) return text.trim() || null; + text += ch; + } + text += ' '; + } + return null; +} + +/** + * A C++ type as written (`const autovector&`), with where + * it is written, which is where the names in it are looked up from (see + * cppClassWritten): the scope, the file and line, and the template + * parameters in scope there. + */ +interface CppWrittenType { + text: string; + scope: string; + file: string; + line: number; + parameters: ReadonlySet; +} + +/** + * The standard library's and abseil's containers whose elements are their + * first template argument. A map's elements are key and value pairs, whose + * `first` and `second` are no receiver of a call this resolves. + */ +const CPP_LIBRARY_SEQUENCES: ReadonlySet = new Set([ + 'vector', 'list', 'forward_list', 'deque', 'set', 'multiset', 'unordered_set', 'unordered_multiset', 'array', 'span', + 'initializer_list', 'Span', 'InlinedVector', 'FixedArray', 'flat_hash_set', 'node_hash_set', 'btree_set', + 'btree_multiset', +]); +/** Widely used project containers of the same kind: protobuf's repeated fields, rocksdb's `autovector`. */ +const CPP_PROJECT_SEQUENCES: ReadonlySet = new Set(['RepeatedField', 'RepeatedPtrField', 'autovector']); + +/** Clang's and abseil's nullability annotations: `const FieldDescriptor* absl_nonnull`. */ +const CPP_NULLABILITY = /\b(?:absl_nonnull|absl_nullable|absl_nullability_unknown|_Nonnull|_Nullable|_Null_unspecified)\b/g; + +/** A written type without its nullability annotations, which are no part of its name. */ +function cppWithoutNullability(text: string): string { + return text.replace(CPP_NULLABILITY, ' ').replace(/\s+/g, ' ').trim(); +} + +/** Is `n` a C or C++ function or method? */ +function isCppFunctionNode(n: Node): boolean { + return (n.kind === 'method' || n.kind === 'function') && (n.language === 'cpp' || n.language === 'c'); +} + +/** Where the `(` that the `)` at `text[close]` closes opens, or -1. */ +function cppOpeningParen(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; +} + +/** + * The return type of a C or C++ function, as its declaration writes it: + * `std::vector` for `std::vector FieldNumberOrder(…)`. (The index keeps only `vector`.) + * Null for none that can be read: a constructor, `void`, `auto` or + * `decltype(…)`. + */ +function cppWrittenReturnType(fn: Node, context: ResolutionContext): string | null { + const lines = context.getFileLines?.(fn.filePath) ?? context.readFile(fn.filePath)?.split(/\r?\n/); + if (!lines) return null; + const head = lines.slice(fn.startLine - 1, fn.startLine + 2).join(' ').slice(fn.startColumn); + const name = fn.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/^operator/, 'operator\\s*'); + const declared = new RegExp(String.raw`^(.*?)(?:\b[A-Za-z_]\w*\s*(?:<[^<>;{}]*>)?\s*::\s*)*\b${name}\s*\(`).exec(head); + let written = declared?.[1]; + if (written === undefined) return null; + if (/^\s*template\b/.test(written)) { + const open = written.indexOf('<'); + let depth = 0; + let end = -1; + for (let i = open; open >= 0 && i < written.length; i++) { + if (written[i] === '<') depth++; + else if (written[i] === '>' && --depth === 0) { + end = i; + break; + } + } + if (end < 0) return null; + written = written.slice(end + 1); + } + written = cppWithoutNullability(written + .replace(/\[\[[^\]]*\]\]/g, ' ') + .replace(/\b(?:inline|static|virtual|constexpr|consteval|explicit|friend|extern)\b/g, ' ') + // A leading macro: `PROTOBUF_EXPORT`, `ABSL_MUST_USE_RESULT`. + .replace(/^\s*(?:[A-Z][A-Z0-9]*_[A-Z0-9_]*(?:\([^()]*\))?\s+)+/, '')); + if (!written || /^(?:void|auto)$/.test(written) || /\bdecltype\b/.test(written)) return null; + return written; +} + +/** What one of `fns` (overloads, a declaration and its definition) returns, as written where it is declared. */ +function cppReturnTypeOf(fns: readonly Node[], context: ResolutionContext): CppWrittenType | null { + for (const fn of fns) { + const text = cppWrittenReturnType(fn, context); + if (text) { + return { text, scope: cppParentScope(fn.qualifiedName), file: fn.filePath, line: fn.startLine, parameters: cppTemplateParameters(fn, context) }; + } + } + return null; +} + +/** + * The C++ class `cls`'s methods named `name`: its own declarations and + * definitions, else the nearest one a class it derives from has (cppMethodOf). + */ +function cppMethodsOf(cls: Node, name: string, at: UnresolvedRef, context: ResolutionContext): Node[] { + const own = context.getNodesByQualifiedName(`${cls.qualifiedName}::${name}`).filter(isCppFunctionNode); + if (own.length > 0) return own; + const inherited = cppMethodOf(cls, name, at, context); + return inherited ? [inherited] : []; +} + +/** + * `type` once the aliases it is written with are followed, each in its own + * scope, with what they name as written: rocksdb's `const BlobFiles&` is + * `std::vector>`. Null when an alias on + * the way can't be read. + */ +function cppFollowWrittenAliases(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null { + let current = type; + for (let hop = 0; hop < 4; hop++) { + const aliased = cppAliasedTypeWritten(current.text, cppScopesWithin(current.scope), at, context); + if (aliased === undefined) return current; + if (aliased === null) return null; + const { alias } = aliased; + current = { + text: isCppPointerType(current.text) ? `${aliased.text}*` : aliased.text, + scope: cppParentScope(alias.qualifiedName), + file: alias.filePath, + line: alias.startLine, + parameters: cppTemplateParameters(alias, context), + }; + } + return null; +} + +/** Is the type `type` names a template parameter's, which a template argument decides? */ +function isCppTemplateParameterType(type: CppWrittenType): boolean { + const head = cppTypeSegments(type.text)?.[0]; + return !!head && type.parameters.has(head); +} + +/** The project class `type` names, looked up as C++ looks it up where it is written. */ +function cppWrittenClass(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): Node | null { + const found = cppClassWritten(type.text, type, at, context); + return found && CPP_CLASS_KINDS.has(found.node.kind) ? found.node : null; +} + +/** + * The type the C++ class `cls` declares its data member `name` with, as + * written, in the class's scope: `std::vector` for + * rocksdb's `CompactionState::sub_compact_states`. Data members are not + * indexed, so the class's own lines are read — one inside none of the + * functions or classes nested in it. Null when none of them declares it + * (a base class may). + */ +function cppDataMemberType(cls: string, name: string, context: ResolutionContext): CppWrittenType | null { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const word = new RegExp(`\\b${escaped}\\b`); + const declarator = buildDeclaratorRegex(escaped); + for (const decl of context.getNodesByQualifiedName(cls)) { + if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) continue; + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/); + if (!lines) continue; + const end = Math.min(decl.endLine ?? decl.startLine, lines.length); + const inner = context.getNodesInFile(decl.filePath).filter((n) => n.id !== decl.id && n.startLine >= decl.startLine && + (n.endLine ?? n.startLine) <= end && (isCppFunctionNode(n) || CPP_CLASS_KINDS.has(n.kind))); + for (let l = decl.startLine; l <= end; l++) { + if (!word.test(lines[l - 1]!) || inner.some((n) => l >= n.startLine && l <= (n.endLine ?? n.startLine))) continue; + const declared = cppCodeOf(lines[l - 1]!)?.match(declarator)?.[1]; + const type = declared && normalizeCppTypeName(declared); + if (type && type !== 'auto') { + return { text: declared!, scope: cls, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) }; + } + } + } + return null; +} + +/** + * The type `*x` has when `x` has the type `type`: what a pointer points at, + * or what a standard smart pointer or optional holds. Null for any other type. + */ +function cppPointee(type: CppWrittenType): CppWrittenType | null { + const text = type.text.replace(/&+\s*$/, '').trim(); + let depth = 0; + for (let i = text.length - 1; i >= 0; i--) { + const ch = text[i]!; + if (ch === '>') depth++; + else if (ch === '<') depth--; + else if (ch === '*' && depth === 0) return { ...type, text: text.slice(0, i).trim() }; + } + const held = cppStdHeldType(text); + return held ? { ...type, text: held } : 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. + */ +function cppRangeElementOf(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext, hops = 0): CppWrittenType | null { + const range = cppFollowWrittenAliases(type, at, context); + if (!range || isCppPointerType(range.text) || hops > 2) return null; + const segments = cppTypeSegments(range.text); + const last = segments?.[segments.length - 1]; + if (!segments || !last) return null; + if (CPP_LIBRARY_SEQUENCES.has(last) || CPP_PROJECT_SEQUENCES.has(last)) { + // A project type of a standard container's name, written without + // `std::` or `absl::`, is that type, not the container. + if (CPP_LIBRARY_SEQUENCES.has(last) && segments[0] !== 'std' && segments[0] !== 'absl' && cppTypesNamed(last, context).length > 0) return null; + const element = cppTemplateArguments(range.text)?.[0]; + return element ? { ...range, text: cppWithoutNullability(element) } : null; + } + const cls = cppWrittenClass(range, at, context); + const begin = cls ? cppReturnTypeOf(cppMethodsOf(cls, 'begin', at, context), context) : null; + return begin ? cppIteratorElement(begin, at, context, hops) : null; +} + +/** What dereferencing an iterator of the type `type` gives, as written. */ +function cppIteratorElement(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext, hops: number): CppWrittenType | null { + const iterator = cppFollowWrittenAliases(type, at, context); + if (!iterator) return null; + if (isCppPointerType(iterator.text)) return cppPointee(iterator); + // `std::vector::const_iterator`: the container's element. + const nested = /^(.*>)\s*::\s*(?:const_)?(?:reverse_)?iterator\s*$/.exec(iterator.text.replace(/\b(?:typename|const)\b|&/g, ' ').trim()); + if (nested) return cppRangeElementOf({ ...iterator, text: nested[1]! }, at, context, hops + 1); + const cls = cppWrittenClass(iterator, at, context); + const star = cls ? cppReturnTypeOf(cppMethodsOf(cls, 'operator*', at, context), context) : null; + return star ? { ...star, text: star.text.replace(/&+\s*$/, '').trim() } : null; +} + +/** + * The type of the object `x.m` or `x->m` (`op`) names a member of, given + * `x`'s text: `x`'s own type for `.`, what it points at or holds for `->`. + */ +function cppMemberObject(object: string, op: string, at: UnresolvedRef, context: ResolutionContext, depth: number): CppWrittenType | null { + const type = cppExpressionType(object, at, context, depth + 1); + const followed = type && cppFollowWrittenAliases(type, at, context); + if (!followed) return null; + if (op === '->') return cppPointee(followed); + return isCppPointerType(followed.text) ? null : followed; +} + +/** + * The type of a C++ expression as written where it is declared, for the + * ranges of `auto` loops: a variable, parameter or member the calling + * function or its class declares; `*x`; `this`; and a call to a function + * or method whose declaration writes its return type — `FieldNumberOrder( + * desc)`, `vstorage->GetBlobFiles()`. Null for anything else, or a name + * declared only outside the caller. + */ +function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionContext, depth: number): CppWrittenType | null { + if (depth > 4) return null; + let text = expr.trim(); + while (text.startsWith('(') && text.endsWith(')') && cppOpeningParen(text, text.length - 1) === 0) text = text.slice(1, -1).trim(); + if (text.startsWith('*')) { + const pointer = cppExpressionType(text.slice(1), at, context, depth + 1); + const followed = pointer && cppFollowWrittenAliases(pointer, at, context); + return followed && cppPointee(followed); + } + const caller = context.getNodeById?.(at.fromNodeId); + if (!caller) return null; + // What the calling function writes is looked up from inside it. + const callers = { scope: caller.qualifiedName, file: at.filePath, line: at.line, parameters: cppTemplateParameters(caller, context) }; + const owner = cppMemberOwner(caller, context); + if (text === 'this') return owner ? { ...callers, text: `${owner}*` } : null; + const name = /^(?:this\s*->\s*)?([A-Za-z_]\w*)$/.exec(text); + if (name) { + const decl: CppReceiverDeclaration = {}; + inferCppReceiverType(name[1]!, at, context, depth + 1, decl); + if (decl.element) return decl.element; + // `const auto& blob_files = vstorage->GetBlobFiles();`: what its initializer is. + if (decl.initializer && isCppCallersDeclaration(at.filePath, decl.initializer.line, at, context)) { + return cppExpressionType(decl.initializer.text, { ...at, line: decl.initializer.line, column: 0 }, context, depth + 1); + } + if (decl.raw !== undefined && decl.callers) return decl.shadowed ? null : { ...callers, text: decl.raw }; + // Declared nowhere in the calling method (`versions_` in db_impl_write.cc, + // whose class is declared in db_impl.h): a data member of its class. + return decl.shadowed || !owner ? null : cppDataMemberType(owner, name[1]!, context); + } + if (!text.endsWith(')')) { + // `compact_->sub_compact_states`: a data member of the object's class. + const field = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(text); + const object = field && cppMemberObject(field[1]!, field[2]!, at, context, depth); + const cls = object && cppWrittenClass(object, at, context); + return cls ? cppDataMemberType(cls.qualifiedName, field![3]!, context) : null; + } + const open = cppOpeningParen(text, text.length - 1); + const callee = open > 0 ? text.slice(0, open).trim() : ''; + // `recv.method(…)`, `recv->method(…)`: the method of the receiver's class. + const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(callee); + if (member) { + const object = cppMemberObject(member[1]!, member[2]!, at, context, depth); + const cls = object && cppWrittenClass(object, at, context); + return cls ? cppReturnTypeOf(cppMethodsOf(cls, member[3]!, at, context), context) : null; + } + // `f(…)`, `ns::f(…)`, `Class::f(…)`: looked up from the caller outwards. + if (!/^(?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*$/.test(callee)) return null; + const spelled = callee.replace(/\s+/g, ''); + for (const scope of cppScopesWithin(cppParentScope(caller.qualifiedName))) { + const fns = context.getNodesByQualifiedName(scope ? `${scope}::${spelled}` : spelled).filter(isCppFunctionNode); + if (fns.length > 0) return cppReturnTypeOf(fns, context); + } + return null; +} + +const CPP_LOOP_ELEMENTS = new WeakMap>(); + +/** + * The element type of the range the `auto` range-for on line `line` + * iterates, as written where the range's type is declared — what the loop's + * variable is. `for (auto cfd : cfds)` over a parameter `const + * autovector& cfds` gives `ColumnFamilyData*`. Null when + * it can't be read reliably: then the loop's variable is as unknown as an + * `auto` local whose initializer can't be read. + */ +function cppAutoLoopElement( + lines: readonly string[], + line: number, + loop: { colon: number }, + ref: UnresolvedRef, + context: ResolutionContext, + depth: number, +): CppWrittenType | null { + let memo = CPP_LOOP_ELEMENTS.get(context); + if (!memo) CPP_LOOP_ELEMENTS.set(context, (memo = new Map())); + const key = `${ref.fromNodeId}\u0000${ref.filePath}\u0000${line}:${loop.colon}`; + if (memo.has(key)) return memo.get(key)!; + // (Unknown while it is worked out, so a range naming its own loop's variable ends.) + memo.set(key, null); + const range = cppRangeExpression(lines, line, loop.colon); + const at = { ...ref, line: line + 1, column: loop.colon }; + const type = range ? cppExpressionType(range, at, context, depth) : null; + const element = type && cppRangeElementOf(type, at, context); + const followed = element && cppFollowWrittenAliases(element, at, context); + const result = followed && !isCppTemplateParameterType(followed) ? followed : null; + memo.set(key, result); + return result; +} + /** Where the C or C++ string or character literal opening at `text[start]` closes (the line's end if it doesn't). */ function cppLiteralEnd(text: string, start: number): number { for (let i = start + 1; i < text.length; i++) { @@ -9044,6 +9408,18 @@ interface CppReceiverDeclaration { * `auto [x, y] = …` — so the declaration it found is an earlier variable's. */ shadowed?: boolean; + /** + * The receiver is an `auto` range-for's variable, and this is the element + * type of its range as written where the range's type is declared (`raw` + * is its text). + */ + element?: CppWrittenType; + /** + * The first declaration the scan met is an `auto` local's, on this line, + * with this initializer: `vstorage->GetBlobFiles()` in `const auto& + * blob_files = vstorage->GetBlobFiles();`. + */ + initializer?: { line: number; text: string }; } /** @@ -9178,6 +9554,13 @@ function isCppLibraryType(raw: string, context: ResolutionContext): boolean { /** Standard-library types whose `->` reaches the value they hold. */ const CPP_STD_HOLDERS: ReadonlySet = new Set(['unique_ptr', 'shared_ptr', 'optional']); +/** The type a standard smart pointer or optional written as `raw` holds (`Iterator` for `std::unique_ptr`), or null. */ +function cppStdHeldType(raw: string): string | null { + const holder = cppTypeSegments(raw); + if (holder?.length !== 2 || holder[0] !== 'std' || !CPP_STD_HOLDERS.has(holder[1]!)) return null; + return cppTemplateArguments(raw)?.[0] ?? null; +} + /** * A `->` call on a C++ receiver declared as a standard smart pointer or * optional (`std::unique_ptr iter; iter->Valid()`) calls a member of @@ -9197,11 +9580,10 @@ const CPP_STD_HOLDERS: ReadonlySet = new Set(['unique_ptr', 'shared_ptr' * doesn't name): the call goes on as before. */ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { - const holder = cppTypeSegments(raw); - if (holder?.length !== 2 || holder[0] !== 'std' || !CPP_STD_HOLDERS.has(holder[1]!) || !isCppLibraryType(raw, context)) return undefined; - const held = cppTemplateArguments(raw)?.[0]; + const held = cppStdHeldType(raw); + if (!held || !isCppLibraryType(raw, context)) return undefined; const caller = context.getNodeById?.(ref.fromNodeId); - if (!held || !caller || isCppPointerType(held)) return undefined; + 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, { @@ -9673,7 +10055,9 @@ function inferCppReceiverType( if (normalized === 'auto') { // `auto x = Foo::instance();` — the declared type is deduced; recover it // from the initializer (call return type / construction) (#645). - const initType = inferCppAutoInitializerType(line, receiverName, ref, context, depth); + const init = cppAutoInitializer(line, receiverName); + if (init && found && !found.shadowed && !found.initializer) found.initializer = { line: i + 1, text: init }; + const initType = init ? inferCppAutoInitializerType(init, ref, context, depth) : null; if (initType) return initType; // No usable initializer on this line — keep scanning earlier ones, // where what is declared is an earlier variable. @@ -9689,8 +10073,9 @@ function inferCppReceiverType( } } else { // `for (ConformanceTestSuite *suite : suites)`, when the call is in the - // loop's body. A loop over `auto` elements has no initializer to read, - // so the scan goes on as for an `auto` local whose initializer is unreadable. + // loop's body. A loop over `auto` elements has the element type of its + // range, when that can be read; when it can't, the scan goes on as for + // an `auto` local whose initializer is unreadable. const loopVar = cppRangeForDeclaration(line, receiverName); if (loopVar && cppForBodyEncloses(lines, i, loopVar.paren, callLineIndex, ref.column)) { const normalized = normalizeCppTypeName(loopVar.type); @@ -9699,6 +10084,13 @@ function inferCppReceiverType( noteCppDeclaration(found, loopVar.type, inCallerScope); return cppDeclaredType(loopVar.type, normalized, inCallerScope, ref, context, found); } + const element = normalized === 'auto' ? cppAutoLoopElement(lines, i, loopVar, ref, context, depth) : null; + const elementType = element && normalizeCppTypeName(element.text); + if (element && elementType && elementType !== 'auto') { + noteCppDeclaration(found, element.text, true); + if (found) found.element = element; + return cppDeclaredType(element.text, elementType, true, ref, context, found); + } } if (found && cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; } @@ -9837,23 +10229,24 @@ function resolveCppCallResultType( return null; } +/** The initializer of `auto receiverName = …;` on `line`, when the line holds it. */ +function cppAutoInitializer(line: string, receiverName: string): string | null { + const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const m = line.match(new RegExp(`\\b${escaped}\\b\\s*=\\s*([^;]+)`)); + return m?.[1]?.trim() || null; +} + /** - * Recover the type of an `auto`-declared local from its initializer on the - * declaration line — `auto x = Foo::instance();`, `auto w = make_unique();`, + * Recover the type of an `auto`-declared local from its initializer `init` on + * the declaration line — `auto x = Foo::instance();`, `auto w = make_unique();`, * `auto p = new W();`, `auto w = Widget();` (#645). */ function inferCppAutoInitializerType( - line: string, - receiverName: string, + init: string, ref: UnresolvedRef, context: ResolutionContext, depth: number, ): string | null { - const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const m = line.match(new RegExp(`\\b${escaped}\\b\\s*=\\s*([^;]+)`)); - if (!m || !m[1]) return null; - const init = m[1].trim(); - const neu = init.match(/^new\s+([A-Za-z_][\w:]*)/); if (neu && neu[1]) return cppLastSegment(neu[1]); @@ -10243,6 +10636,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_CLASSES_IN_FILE.delete(context); CPP_BASE_SPECS.delete(context); CPP_MEMBER_MACRO_ARGS.delete(context); + CPP_LOOP_ELEMENTS.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context);