From 01ab45d0687c52a243926ca94188cf7d9a859b16 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Sun, 11 Oct 2026 15:19:01 +0000 Subject: [PATCH 1/4] fix(cpp): a range-based for loop over `auto` elements has the element type of its range (#2492) #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 a984c54d7..c7a5c3eb1 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 000000000..a03fa01b2 --- /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 040d6e979..f93aa4c11 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 a49057f60..3b80716eb 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); From fc0da38f0f1a18147bd8c395ff78ae915d8de6e9 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Sun, 11 Oct 2026 16:06:58 +0000 Subject: [PATCH 2/4] fix(cpp): a call on a declared pointer, value or reference reaches the class its type names, not a namesake (#2493) 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 c7a5c3eb1..7c288aa6e 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 000000000..0efc38674 --- /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 f7d895c4a..2ba4bfa00 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 f93aa4c11..85de85939 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 3b80716eb..6df55a82e 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); From 5171a6aa9c69a367ace852c46c128ef1440fce70 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Sun, 11 Oct 2026 19:22:41 +0000 Subject: [PATCH 3/4] fix(sync): a Python import alias links its module when the module appears later (#2501) `from .models import User as UserModel` and `from . import helpers as h` bind a name their module declares under another: `User`, `helpers`. Every reference through the alias (the import, a call, a base class, the class or function passed as a value) was parked under the alias, which no file a sync adds or changes carries. So a module added after its importers were indexed, restored after a delete, or given the name in a later edit never relinked them, while a fresh index did. #2450 fixed the same gap for ES modules. Such a reference is now parked under its module's key. A Python import names its module by a dotted path, counted from the importing file's own package when it starts with dots: `.models` in app/main.py is 'module:models', `.` is 'module:app', `..pkg` the package above's 'module:pkg'. #2450's lookup still finds it by its own name, which is how a fresh index links an alias its module never declares. A package's `__init__.py` also carries the key of the package above it, as a module file beside it does through its folder, so `from app.pkg import sub as s` relinks when the subpackage appears. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + .../sync-python-import-binding-retry.test.ts | 289 ++++++++++++++++++ src/db/reference-tail.ts | 9 +- src/resolution/import-resolver.ts | 41 +++ 4 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 __tests__/sync-python-import-binding-retry.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c288aa6e..0d6e83499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - A file named the way Google names its tests, like `wire_format_unittest.cc` in protocolbuffers/protobuf, `logging_unittest.cc` in google/glog or a Python `foo_unittest.py`, now counts as a test, as `wire_format_test.cc` already did. Before, it was taken for production code: search and `codegraph_explore` ranked it alongside the code it tests, `codegraph affected` never listed it, and calls in production code could link to a class or type a unittest declares for itself, as google/breakpad's `stack_frame_entries_.size()` did to a unittest's `StackHelper`. Re-index projects that have such files after upgrading. - A file in a `unittests/` folder, where llvm/llvm-project and google/breakpad keep their unit tests, or in a folder named like a unittest, such as `dcheck_unittest/` in google/glog, now counts as a test whatever its own name says, like LLVM's `CountCopyAndMove.h` helper. Before, such a file was taken for production code unless its name said test, and none of them were kept apart from the program the way a `tests/` folder is: search and `codegraph_explore` ranked them alongside production code, `codegraph affected` missed them, and production code linked to a unit test's own declarations, as LLVM's `Ctx` and `OpInfo` variables did to ones its unit tests declare. A folder named just `unittest/`, like CPython's `Lib/unittest/`, is still production code. Re-index projects that have such folders after upgrading. - `codegraph sync` and the file watcher now link a file to a module it imports under a name of its own when the module is added after the file was indexed, comes back after being deleted, or gets the export in a later edit: a default import like `import tagsController from './tag/tag.controller'`, a namespace import like `import * as api from './api'`, or an aliased one like `import { Component as GroupsPageWrapper } from './groups_page'`, in JavaScript, TypeScript, Vue, Svelte and Astro. Before, the import and every use of the name, such as a call, `new`, a base class, a JSX or template tag or an Express `app.use` mount, stayed unlinked until the importing file changed or the project was re-indexed, so the module looked unused and `codegraph affected` missed the files that use it. Re-index after upgrading to pick up links an earlier version missed. +- `codegraph sync` and the file watcher now do the same for a Python import under a name of its own, like `from .models import User as UserModel`, `from . import helpers as h` or `from app.pkg import sub as subpkg`: when the module or package is added after the importing file was indexed, comes back after being deleted, or gets the imported name in a later edit, the import and every use of the alias, such as a call, a base class or the class passed as a value, now link to it. Before, they stayed unlinked until the importing file changed or the project was re-indexed. Re-index Python projects after upgrading to pick up links an earlier version missed. - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a call on a variable, parameter or member declared with template arguments nested inside its template arguments, like `autovector>` or `Striped>`, now links to the method of the class it is declared as. Before, the nested arguments hid that class: in RocksDB, `files_marked_for_compaction_.clear()` on such an `autovector` linked to an unrelated `CompactionInputFiles::clear` that only shares words with the variable's name, and `mutex_.Get(key)` on a `Striped` linked to nothing. Re-index C++ projects after upgrading. diff --git a/__tests__/sync-python-import-binding-retry.test.ts b/__tests__/sync-python-import-binding-retry.test.ts new file mode 100644 index 000000000..870d022d4 --- /dev/null +++ b/__tests__/sync-python-import-binding-retry.test.ts @@ -0,0 +1,289 @@ +/** + * A sync links a reference through a Python import alias once the module appears. + * + * `from .models import User as UserModel` and `from . import helpers as h` + * bind a name their module never declares: it has `User` and `helpers`. Every + * reference through that name — the import itself, a call, a base class, a + * class or function passed as a value — resolves only through the module. Sync + * retries a parked failed ref by its tail (#1240), and these were parked under + * the alias, which neither the module's file nor its symbols carry. So a module + * added after its importers were indexed, one restored after a delete, or one + * that gained the name in a later edit stayed unlinked from them until each + * importer changed or the project was indexed again. #2450 parked the ES-module + * bindings under their module; a Python import names its module by a dotted + * path, relative to the importing file's package when it starts with dots. + */ +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/index'; +import { DatabaseConnection, getDatabasePath } from '../src/db'; +import { moduleReferenceKeys } from '../src/db/reference-tail'; + +type Files = Record; + +/** The files that reach a module through a name the module declares under another. */ +const IMPORTERS: Files = { + 'app/__init__.py': '', + 'app/sub/__init__.py': '', + 'app/pkg/__init__.py': '', + // A package that re-exports a name from a module beside it. + 'app/store/__init__.py': 'from .user import Account\n', + 'app/main.py': [ + 'from .models import User as UserModel', + 'from . import helpers as h', + 'from app.models import Group as GroupModel', + 'from .pkg import sub as subpkg', + 'from app.pkg import other as other_mod', + 'from .svc import make_thing as mk', + 'from .store import Account as Acct', + '', + 'class Admin(UserModel):', + ' pass', + '', + 'def run(u):', + ' x = mk()', + ' if isinstance(u, UserModel):', + ' return GroupModel(), Acct(), x', + ' return mk', + '', + ].join('\n'), + 'app/sub/deep.py': [ + 'from .. import helpers as hh', + 'from ..models import User as U2', + 'from ..pkg import sub as sp2', + '', + 'def use():', + ' return U2()', + '', + ].join('\n'), + // A module at the root, imported by its absolute name. + 'main.py': 'from settings import Config as Cfg\n\n\ndef load():\n return Cfg()\n', +}; + +const MODULES: Files = { + 'app/models.py': 'class User:\n pass\n\n\nclass Group:\n pass\n', + 'app/helpers.py': 'def x():\n return 1\n', + // A subpackage: what `from .pkg import sub` imports is its `__init__.py`. + 'app/pkg/sub/__init__.py': 'VALUE = 1\n', + 'app/pkg/other.py': 'def f():\n return 2\n', + 'app/svc.py': 'def make_thing():\n return 3\n', + 'app/store/user.py': 'class Account:\n pass\n', + 'settings.py': 'class Config:\n pass\n', +}; + +/** What a full index links through the aliases: `: -> ::`. */ +const LINKS = [ + 'Acct: imports file app/main.py -> class app/store/user.py::Account', + 'Acct: instantiates function app/main.py -> class app/store/user.py::Account', + 'Cfg: imports file main.py -> class settings.py::Config', + 'Cfg: instantiates function main.py -> class settings.py::Config', + 'GroupModel: imports file app/main.py -> class app/models.py::Group', + 'GroupModel: instantiates function app/main.py -> class app/models.py::Group', + 'U2: imports file app/sub/deep.py -> class app/models.py::User', + 'U2: instantiates function app/sub/deep.py -> class app/models.py::User', + 'UserModel: extends class app/main.py -> class app/models.py::User', + 'UserModel: imports file app/main.py -> class app/models.py::User', + 'UserModel: references function app/main.py -> class app/models.py::User', + 'h: imports file app/main.py -> file app/helpers.py::helpers.py', + 'hh: imports file app/sub/deep.py -> file app/helpers.py::helpers.py', + 'mk: calls function app/main.py -> function app/svc.py::make_thing', + 'mk: imports file app/main.py -> function app/svc.py::make_thing', + 'mk: references function app/main.py -> function app/svc.py::make_thing', + 'other_mod: imports file app/main.py -> file app/pkg/other.py::other.py', + 'sp2: imports file app/sub/deep.py -> file app/pkg/sub/__init__.py::__init__.py', + 'subpkg: imports file app/main.py -> file app/pkg/sub/__init__.py::__init__.py', +]; + +/** An alias of a name its module doesn't have, which a full index links to a namesake. */ +const BY_NAME: Files = { + 'app/__init__.py': '', + 'app/lib.py': 'unrelated = 1\n', + 'app/form.py': 'from .lib import FormSchemaBase as FormSchema\n\n\nclass Form(FormSchema):\n pass\n', +}; +const BY_NAME_ADDED: Files = { 'app/schemas.py': 'class FormSchema:\n pass\n' }; + +let roots: string[] = []; +let graphs: CodeGraph[] = []; + +afterEach(() => { + for (const graph of graphs) graph.close(); + graphs = []; + for (const root of roots) fs.rmSync(root, { recursive: true, force: true }); + roots = []; +}); + +function tempRoot(): string { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-sync-pybinding-')); + roots.push(root); + return root; +} + +function write(root: string, files: Files): void { + for (const [file, text] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); + fs.writeFileSync(path.join(root, file), text); + } +} + +async function index(root: string): Promise { + const graph = await CodeGraph.init(root, { index: true }); + graphs.push(graph); + return graph; +} + +/** Every edge, by its ends' natural keys, with where it is written and how it resolved. */ +function edgesOf(graph: CodeGraph): string[] { + const keys = new Map( + graph.getFiles() + .flatMap((file) => graph.getNodesInFile(file.path)) + .map((n) => [n.id, `${n.kind} ${n.filePath}:${n.qualifiedName}:${n.startLine}`] as const) + ); + return graph.getOutgoingEdgesFrom([...keys.keys()]) + .map((e) => `${e.kind} ${keys.get(e.source)} -> ${keys.get(e.target) ?? e.target} @${e.line}:${e.column} ${JSON.stringify(e.metadata ?? {})}`) + .sort(); +} + +/** The {@link LINKS} the graph has. */ +function linksOf(graph: CodeGraph): string[] { + const names = new Set(LINKS.map((link) => link.slice(0, link.indexOf(':')))); + const ids = graph.getFiles().flatMap((file) => graph.getNodesInFile(file.path)).map((n) => n.id); + const links: string[] = []; + for (const edge of graph.getOutgoingEdgesFrom(ids)) { + const ref = edge.metadata?.refName; + if (typeof ref !== 'string' || !names.has(ref)) continue; + const source = graph.getNode(edge.source)!; + const target = graph.getNode(edge.target)!; + links.push(`${ref}: ${edge.kind} ${source.kind} ${source.filePath} -> ${target.kind} ${target.filePath}::${target.name}`); + } + return [...new Set(links)].sort(); +} + +/** The final tree, indexed from scratch in a folder of its own: what the sync has to match. */ +async function fullIndexOf(files: Files): Promise { + const root = tempRoot(); + write(root, files); + return edgesOf(await index(root)); +} + +describe('sync links references through a Python import alias to their module', () => { + it('when the module is added after its importers', async () => { + const root = tempRoot(); + write(root, IMPORTERS); + const graph = await index(root); + expect(linksOf(graph)).toEqual([]); + + write(root, MODULES); + const result = await graph.sync(); + // The importers did not change: only the retry of their parked refs links them. + expect(result.filesAdded).toBe(Object.keys(MODULES).length); + expect(result.filesModified).toBe(0); + expect(linksOf(graph)).toEqual(LINKS); + expect(edgesOf(graph)).toEqual(await fullIndexOf({ ...IMPORTERS, ...MODULES })); + }, 120_000); + + it('when the module comes back after it was deleted', async () => { + const root = tempRoot(); + write(root, { ...IMPORTERS, ...MODULES }); + const graph = await index(root); + expect(linksOf(graph)).toEqual(LINKS); + + for (const file of Object.keys(MODULES)) fs.rmSync(path.join(root, file)); + expect((await graph.sync()).filesRemoved).toBe(Object.keys(MODULES).length); + expect(linksOf(graph)).toEqual([]); + + write(root, MODULES); + expect((await graph.sync()).filesAdded).toBe(Object.keys(MODULES).length); + expect(linksOf(graph)).toEqual(LINKS); + expect(edgesOf(graph)).toEqual(await fullIndexOf({ ...IMPORTERS, ...MODULES })); + }, 120_000); + + it('when an edit gives the module the name its importers import', async () => { + const root = tempRoot(); + write(root, IMPORTERS); + write(root, Object.fromEntries(Object.keys(MODULES).map((file) => [file, 'PLACEHOLDER = 0\n']))); + const graph = await index(root); + + write(root, MODULES); + const result = await graph.sync(); + expect(result.filesModified).toBe(Object.keys(MODULES).length); + expect(result.filesAdded).toBe(0); + expect(linksOf(graph)).toEqual(LINKS); + expect(edgesOf(graph)).toEqual(await fullIndexOf({ ...IMPORTERS, ...MODULES })); + }, 120_000); + + it('and by its own name when its module never declares it', async () => { + // As vben's case for an ES module: the module has no such name, so a full + // index links the alias by its own name to the class that carries it. + const root = tempRoot(); + write(root, BY_NAME); + const graph = await index(root); + + write(root, BY_NAME_ADDED); + expect((await graph.sync()).filesAdded).toBe(1); + const formSchema = edgesOf(graph).filter((edge) => edge.includes('"refName":"FormSchema"')); + expect(formSchema).toHaveLength(2); + expect(formSchema.every((edge) => edge.includes('-> class app/schemas.py:'))).toBe(true); + expect(edgesOf(graph)).toEqual(await fullIndexOf({ ...BY_NAME, ...BY_NAME_ADDED })); + }, 120_000); +}); + +describe('a failed reference through a Python import alias', () => { + it('waits for its module, and every other reference keeps its own tail', async () => { + const root = tempRoot(); + write(root, { + ...IMPORTERS, + ...BY_NAME, + // An unaliased import, a standard-library alias and a member read + // through an alias wait for the name they use. + 'app/other.py': [ + 'from .models import Group', + 'from collections import OrderedDict as OD', + 'from .models import User as UserModel', + '', + 'def build():', + ' return Group(), OD(), UserModel.create()', + '', + ].join('\n'), + }); + (await CodeGraph.init(root, { index: true })).close(); + + const db = DatabaseConnection.open(getDatabasePath(root)); + try { + const rows = db.getDb() + .prepare("SELECT reference_name AS name, reference_kind AS kind, file_path AS file, name_tail AS tail FROM unresolved_refs WHERE status = 'failed'") + .all() as Array<{ name: string; kind: string; file: string; tail: string }>; + const tailOf = (file: string, name: string, kind: string): string | undefined => + rows.find((r) => r.file === file && r.name === name && r.kind === kind)?.tail; + + // A module named relative to the importing file's package, or absolutely. + expect(tailOf('app/main.py', 'UserModel', 'imports')).toBe('module:models'); + expect(tailOf('app/main.py', 'UserModel', 'extends')).toBe('module:models'); + expect(tailOf('app/main.py', 'GroupModel', 'calls')).toBe('module:models'); + expect(tailOf('app/sub/deep.py', 'U2', 'calls')).toBe('module:models'); + expect(tailOf('app/main.py', 'mk', 'calls')).toBe('module:svc'); + // `from . import helpers` imports from the file's own package, `..` from the one above. + expect(tailOf('app/main.py', 'h', 'imports')).toBe('module:app'); + expect(tailOf('app/sub/deep.py', 'hh', 'imports')).toBe('module:app'); + expect(tailOf('app/main.py', 'subpkg', 'imports')).toBe('module:pkg'); + expect(tailOf('app/main.py', 'other_mod', 'imports')).toBe('module:pkg'); + expect(tailOf('app/main.py', 'Acct', 'calls')).toBe('module:store'); + expect(tailOf('main.py', 'Cfg', 'calls')).toBe('module:settings'); + // Still found by its own name too: sync's lookup by whole name. + expect(tailOf('app/form.py', 'FormSchema', 'extends')).toBe('module:lib'); + + expect(tailOf('app/other.py', 'Group', 'calls')).toBe('Group'); + expect(tailOf('app/other.py', 'OD', 'calls')).toBe('OD'); + expect(tailOf('app/other.py', 'UserModel.create', 'calls')).toBe('create'); + + // A submodule's keys hold its package's: a module file's folder, and + // the folder above a subpackage's `__init__.py`. + expect(moduleReferenceKeys('app/helpers.py')).toContain('module:app'); + expect(moduleReferenceKeys('app/pkg/sub/__init__.py')).toEqual(expect.arrayContaining(['module:sub', 'module:pkg'])); + expect(moduleReferenceKeys('pkg/__init__.py')).toEqual(['module:pkg', 'module:__init__.py', 'module:__init__']); + } finally { + db.close(); + } + }, 60_000); +}); diff --git a/src/db/reference-tail.ts b/src/db/reference-tail.ts index a0b954c59..2bdd6646a 100644 --- a/src/db/reference-tail.ts +++ b/src/db/reference-tail.ts @@ -127,5 +127,12 @@ export function importPathKeys(filePath: string): string[] { * name (`importBindingTail`), which is parked under the same keys. */ export function moduleReferenceKeys(filePath: string): string[] { - return importPathKeys(filePath).map((key) => MODULE_KEY + key); + const keys = importPathKeys(filePath); + // A Python package's `__init__.py` is a submodule of the package above it, + // as a module file beside it is through its folder: `app/pkg/sub/__init__.py` + // is what `from app.pkg import sub as s`, parked as 'module:pkg', imports. + const parts = filePath.split('/'); + const above = parts.length >= 3 && parts[parts.length - 1] === '__init__.py' ? parts[parts.length - 3]! : ''; + if (above && !keys.includes(above)) keys.push(above); + return keys.map((key) => MODULE_KEY + key); } diff --git a/src/resolution/import-resolver.ts b/src/resolution/import-resolver.ts index 01cd934cd..ba9face41 100644 --- a/src/resolution/import-resolver.ts +++ b/src/resolution/import-resolver.ts @@ -3124,6 +3124,7 @@ export function isBoundToOutOfRepoImport( * keeps its tail. Undefined for every other reference. */ export function importBindingTail(ref: UnresolvedRef, context: ResolutionContext): string | undefined { + if (ref.language === 'python') return pythonImportBindingTail(ref, context); if (!ESM_IMPORT_LANGUAGES.has(ref.language) || !IDENTIFIER.test(ref.referenceName)) return undefined; const binding = context.getImportMappings(ref.filePath, ref.language).find((m) => m.localName === ref.referenceName); if (!binding || (!binding.isDefault && !binding.isNamespace && binding.exportedName === binding.localName)) return undefined; @@ -3139,4 +3140,44 @@ export function importBindingTail(ref: UnresolvedRef, context: ResolutionContext return moduleTail(modulePath) || undefined; } +/** + * {@link importBindingTail} for Python: `from .models import User as + * UserModel` and `from . import helpers as h` bind a name their module has as + * `User` and `helpers`, `import app.tools as tl` one it has as `tools`. The + * module is the one the import names by its dotted path, counted from the + * importing file's own package when it starts with dots (`.` is that package, + * each further dot the one above): `.models` in `app/main.py` is `app/models`, + * parked as 'module:models', and `.` is `app`, 'module:app'. The name may + * import a submodule of it (`helpers` is `app/helpers.py`), whose keys hold + * the package's name too (moduleReferenceKeys). + * + * An unaliased import keeps its tail: the module, or the one it re-exports the + * name from, declares that name. So does an import of the standard library. An + * installed package's import can't be told from a module the project doesn't + * have yet (`from settings import Config as Cfg`), so it waits for its module + * too, and a sync still finds it by its own name. + */ +function pythonImportBindingTail(ref: UnresolvedRef, context: ResolutionContext): string | undefined { + if (!IDENTIFIER.test(ref.referenceName)) return undefined; + const binding = context.getImportMappings(ref.filePath, 'python').find((m) => m.localName === ref.referenceName); + if (!binding) return undefined; + const declared = binding.isNamespace ? binding.source.slice(binding.source.lastIndexOf('.') + 1) : binding.exportedName; + if (declared === binding.localName || isExternalImport(binding.source, 'python', context)) return undefined; + const modulePath = pythonModulePath(binding.source, ref.filePath); + return modulePath ? moduleTail(modulePath) || undefined : undefined; +} + +/** + * A Python import's module as a project path: `app.models` → 'app/models'; + * from `app/sub/views.py`, `.models` → 'app/sub/models', `..` → 'app' and + * `..forms` → 'app/forms'. Null when its dots climb above the project. + */ +function pythonModulePath(source: string, fromFile: string): string | null { + const name = source.replace(/^\.+/, ''); + const dots = source.length - name.length; + if (dots === 0) return name.replace(/\./g, '/'); + const modulePath = path.posix.join(path.posix.dirname(fromFile), '../'.repeat(dots - 1), name.replace(/\./g, '/')); + return modulePath === '..' || modulePath.startsWith('../') ? null : modulePath; +} + const IDENTIFIER = /^[A-Za-z_$][\w$]*$/; From 7dc9fdfcddbb1ffe587b8ccf450175ba99745e08 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 11 Oct 2026 18:28:15 -0600 Subject: [PATCH 4/4] fix(resolve): tighten C++ auto range-for element lookups A type written `::Registry` is looked up at global scope, not in the classes around the code. Overloads that take different parameters and return different types give no element type, since which one a call picks is not read; a const and non-const pair with the same parameters still gives one. In the line scan, a nearer declaration of the receiver inside the loop body hides the loop's variable. --- __tests__/cpp-auto-range-for-receiver.test.ts | 98 +++++++++++++++++++ codegraph-kernel/src/resolve/cpp_range_for.rs | 47 +++++---- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/__tests__/cpp-auto-range-for-receiver.test.ts b/__tests__/cpp-auto-range-for-receiver.test.ts index f7539b8ce..973b75743 100644 --- a/__tests__/cpp-auto-range-for-receiver.test.ts +++ b/__tests__/cpp-auto-range-for-receiver.test.ts @@ -558,3 +558,101 @@ describe('an `auto` element that points at a class template', () => { } }); }); + +describe('an `auto` element read through a lookup that has to pick one declaration', () => { + it('a type written `::Registry` is the global class, not one nested in the caller\'s class', async () => { + const cg = await indexed({ + 'ui/screen.h': lines( + '#include ', + 'struct Widget { void Draw() {} };', + 'struct Registry { std::vector items() { return {}; } };', + 'namespace app {', + 'struct Gadget { void Draw() {} };', + 'class Screen {', + ' public:', + ' struct Registry { std::vector items() { return {}; } };', + ' void DrawAll();', + ' ::Registry registry_;', + '};', + '} // namespace app', + ), + 'ui/screen.cc': lines( + '#include "ui/screen.h"', + 'namespace app {', + 'void Screen::DrawAll() {', + ' for (auto w : registry_.items()) {', + ' w->Draw();', + ' }', + '}', + '} // namespace app', + ), + }); + try { + const drawn = calls(cg, 'app::Screen::DrawAll').filter((c) => c.includes('Draw @')); + expect(drawn).toEqual(['Widget::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it('overloads that return different types give no element type', async () => { + const cg = await indexed({ + 'ui/registry.h': lines( + '#include ', + 'namespace app {', + 'struct Widget { void Draw() {} };', + 'struct Gadget { void Draw() {} };', + 'class Registry {', + ' public:', + ' const std::vector& items(int kind) const { return gadgets_; }', + ' const std::vector& items() const { return widgets_; }', + ' std::vector gadgets_;', + ' std::vector widgets_;', + '};', + '} // namespace app', + ), + 'ui/draw.cc': lines( + '#include "ui/registry.h"', + 'namespace app {', + 'void DrawAll(const Registry& registry) {', + ' for (auto w : registry.items()) {', + ' w->Draw();', + ' }', + '}', + '} // namespace app', + ), + }); + try { + // Which overload the call picks is not read, so neither return type is taken for it. + expect(calls(cg, 'app::DrawAll')).toEqual(['app::Registry::items @0.9']); + } finally { + cg.close(); + } + }); + + it('a name the loop body declares again hides the loop\'s variable', async () => { + const cg = await indexed({ + 'ui/draw.h': lines( + '#include ', + 'namespace app {', + 'struct Gadget { Gadget* Self() { return this; } void Draw() {} };', + 'struct Widget { Widget* Self() { return this; } void Draw() {} };', + '} // namespace app', + ), + 'ui/draw.cc': lines( + '#include "ui/draw.h"', + 'namespace app {', + 'void DrawAll(const std::vector& widgets, Gadget* g) {', + ' for (auto w : widgets) { { Gadget* w = g; w->Self()->Draw(); } }', + '}', + '} // namespace app', + ), + }); + try { + // The inner `w` is a `Gadget*`: the chained call is no `Widget`'s. + expect(calls(cg, 'app::DrawAll').filter((c) => c.startsWith('app::Widget::'))).toEqual([]); + } finally { + cg.close(); + } + }); +}); diff --git a/codegraph-kernel/src/resolve/cpp_range_for.rs b/codegraph-kernel/src/resolve/cpp_range_for.rs index 8ada0d12c..1a7dc5767 100644 --- a/codegraph-kernel/src/resolve/cpp_range_for.rs +++ b/codegraph-kernel/src/resolve/cpp_range_for.rs @@ -109,20 +109,22 @@ pub(super) fn cpp_pointee(text: &str) -> Option { cpp_held_type(&held[1]) } -/// Whether a declarator declares a function (`f()`, `*f()`, `&f()`), not a variable. -fn declares_function(mut declarator: TsNode) -> bool { +/// The function declarator of a declarator that declares a function (`f()`, `*f()`, `&f()`), not +/// a variable. +fn declared_function(mut declarator: TsNode) -> Option { loop { match declarator.kind() { - "function_declarator" => return true, + "function_declarator" => return Some(declarator), "pointer_declarator" | "reference_declarator" | "parenthesized_declarator" => { - let Some(inner) = declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0)) else { return false }; - declarator = inner; + declarator = declarator.child_by_field_name("declarator").or_else(|| declarator.named_child(0))?; } - _ => return false, + _ => return None, } } } +fn without_spaces(text: &str) -> String { text.chars().filter(|c| !c.is_whitespace()).collect() } + /// The initializer of `name`'s declaration in the block `parent`, before `at`: `vstorage-> /// GetBlobFiles()` in `const auto& blob_files = vstorage->GetBlobFiles();`. The same declarations /// `cpp_binding_type` reads. @@ -174,6 +176,8 @@ impl KernelResolver { let Some(tree) = self.parsed_tree(&source, r) else { return Ok(None) }; let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize + 1)); while let Some(parent) = at.parent() { + // A nearer declaration of the name, `{ Gadget* w = g; w->Self(); }` in the body, hides the loop's. + if cpp_binding_type(parent, at, receiver, &source).is_some() { return Ok(None); } if parent.kind() == "for_range_loop" && parent.start_position().row == line && parent.child_by_field_name("body").is_some_and(|body| body.id() == at.id()) && parent.child_by_field_name("declarator").is_some_and(|d| cpp_declarator_name(d, &source) == receiver) { @@ -222,10 +226,11 @@ impl KernelResolver { let Some(written) = self.cpp_follow_written_aliases(written.clone(), r)? else { return Ok(None) }; if top_level_pointer(&written.text) || self.cpp_written_template_parameter(&written)? { return Ok(None); } let Some(parts) = type_segments(&written.text) else { return Ok(None) }; - let mut owner = if parts.len() == 1 { self.cpp_class_scope_type(&parts[0], &written.site)? } else { None }; + let plain = cpp_plain_type(&written.text); + let primary = plain.split('<').next().unwrap_or(&plain).trim_end_matches('&').trim(); + // `::Registry` is the global one, never one nested in the classes around it. + let mut owner = if parts.len() == 1 && !primary.starts_with("::") { self.cpp_class_scope_type(&parts[0], &written.site)? } else { None }; if owner.is_none() { - let plain = cpp_plain_type(&written.text); - let primary = plain.split('<').next().unwrap_or(&plain).trim_end_matches('&').trim(); let included = self.namespace_visible_files(&r.file_path, "cpp")?; owner = self.cpp_type_owner_visible(primary, &written.site, 0, false, &included)?; } @@ -243,20 +248,25 @@ impl KernelResolver { } /// What one of `functions` (overloads, a declaration and its definition) returns, as written - /// where it is declared. + /// where it is declared. None when overloads taking different parameters return different + /// types, since which one a call picks is not read; a `const` and a non-`const` overload with + /// the same parameters (`items()`, `items() const`) give the first one's. fn cpp_return_type_of(&mut self, functions: &[Arc], r: &ResolveRefIn) -> Res> { + let mut first: Option<(CppWritten, String)> = None; for function in functions { - if let Some(text) = self.cpp_written_return_type(function, r)? { - return Ok(Some(CppWritten { text, site: site_at(r, function) })); + let Some((text, parameters)) = self.cpp_written_return_type(function, r)? else { continue }; + match &first { + None => first = Some((CppWritten { text, site: site_at(r, function) }, parameters)), + Some((seen, seen_parameters)) => if without_spaces(&seen.text) != without_spaces(&text) && *seen_parameters != parameters { return Ok(None); }, } } - Ok(None) + Ok(first.map(|(written, _)| written)) } /// The return type of a C or C++ function as its declaration writes it: `std::vector` (the index keeps only `vector`). None for a constructor, `void`, `auto` - /// or `decltype(…)`. - fn cpp_written_return_type(&mut self, function: &Arc, r: &ResolveRefIn) -> Res> { + /// FieldDescriptor*>` (the index keeps only `vector`), and its parameter list without spaces. + /// None for a constructor, `void`, `auto` or `decltype(…)`. + fn cpp_written_return_type(&mut self, function: &Arc, r: &ResolveRefIn) -> Res> { let Some(source) = self.read_file(&function.file_path) else { return Ok(None) }; let site = site_at(r, function); let Some(tree) = self.parsed_tree(&source, &site) else { return Ok(None) }; @@ -274,10 +284,11 @@ impl KernelResolver { node = parent; }; let (Some(ty), Some(declarator)) = (declaration.child_by_field_name("type"), declaration.child_by_field_name("declarator")) else { return Ok(None) }; - if !declares_function(declarator) { return Ok(None); } + let Some(function_declarator) = declared_function(declarator) else { return Ok(None) }; let raw = cpp_plain_type(&cpp_ast_declared_type(ty, declarator, &source)); if raw.is_empty() || raw == "void" || is_cpp_auto(&raw) || raw.contains("decltype") { return Ok(None); } - Ok(Some(raw)) + let parameters = function_declarator.child_by_field_name("parameters").map(|p| without_spaces(&source.text()[p.byte_range()])).unwrap_or_default(); + Ok(Some((raw, parameters))) } /// The type the C++ class `cls` declares its data member `field` with, as written in the class.