From 79cd737becf77da18fd8c1f1adb4325e4f749fe5 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 13 Oct 2026 00:42:38 +0000 Subject: [PATCH 1/3] fix(cpp): a call on an `auto` variable or a chained call reaches the class C++ finds, not a namesake (#2532) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2493 resolved calls on declared pointers, values and references by C++ lookup. Calls whose receiver type came from an initializer or a return type still went by the type's last name: - An `auto` local now has the type its initializer has, as written where that is declared: a cast or smart pointer written out (`std::unique_ptr(…)`, `static_cast(arg)`), `new` / `std::make_unique` / a construction of a class, or what a function or method is declared to return (`txn->GetIterator(…)`), with the initializer read to its `;` across lines. Its calls then go through matchCppDeclaredTypeCall from that site. A type that depends on a template parameter (`T*`, `std::shared_ptr`) is left to the old path. - A chained call `a->b()->c()` looks `c` up on the class C++ finds for the written return type of `b`, through a smart pointer too (matchCppChainOnWrittenType), before the last-name path. - A type from outside the project gets no project method: a std/abseil library type (#2413's gate, now applied before the last-name match, so a namesake class like fmt's own `ostream` no longer wins), and a capitalized type under a namespace the project declares no type in (`absl::Status`, which reached `upb::Status::ok`). - A bare call in a method types an `auto` local from the class's own or inherited method; a `template <…>` header followed by macros or specifiers still names its parameters; a trailing annotation macro (`T* PROTOBUF_NONNULL`) is no part of a return type; and memoized `auto` types are worked out with a fresh depth budget, so a deep chain that reads a local first no longer leaves it untyped for its other calls. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-auto-local-receiver.test.ts | 710 ++++++++++++++++++++++ src/resolution/cpp-namespaces.ts | 5 + src/resolution/cpp-type-aliases.ts | 5 +- src/resolution/name-matcher.ts | 315 ++++++++-- 5 files changed, 988 insertions(+), 48 deletions(-) create mode 100644 __tests__/cpp-auto-local-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 729069f0ba..2cd7b71df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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. - In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. +- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr(…)` or `auto* mutex = static_cast(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. 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-local-receiver.test.ts b/__tests__/cpp-auto-local-receiver.test.ts new file mode 100644 index 0000000000..c179d179b8 --- /dev/null +++ b/__tests__/cpp-auto-local-receiver.test.ts @@ -0,0 +1,710 @@ +/** + * A C++ call on a receiver whose type C++ lookup finds reaches that class, + * not a namesake; and a receiver of a type from outside the project reaches + * none of the project's classes. + * + * #2493 resolved calls on declared pointers, values and references by C++ + * lookup. An `auto` local was still typed by the last name of its + * initializer's type, when that could be read at all: in facebook/rocksdb, + * `auto iter = std::unique_ptr(…)` couldn't be, so the scan read an + * earlier function's `Iterator* iter`, and its last name took + * `iter->SeekToFirst()` to the nested `MemTableRep::Iterator`. The local now + * has the type its initializer has as written where it is declared: a + * construction or cast names it; a call's declaration writes it. Its calls + * then resolve as for a declared receiver. + * + * A type from outside the project (`absl::Status`, `std::ostream`) took the + * project's class of the same last name: protocolbuffers/protobuf's + * `upb::Status`, fmtlib/fmt's own `ostream`. + * + * A guess can still land on the right method by a shared word, so these + * tests check the 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-local-')); + 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'); + +/** rocksdb's iterators, trimmed: the public Iterator inherits its cursor methods; a nested namesake declares them. */ +const ITERATORS = { + 'include/rocksdb/iterator_base.h': lines( + 'namespace rocksdb {', + 'class IteratorBase {', + ' public:', + ' virtual ~IteratorBase() {}', + ' virtual bool Valid() const = 0;', + ' virtual void SeekToFirst() = 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 SeekToFirst() = 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;', + '};', + 'class Transaction {', + ' public:', + ' virtual Iterator* GetIterator() = 0;', + '};', + '} // namespace rocksdb', + ), +}; + +describe('an `auto` local has the type its initializer has where that is written', () => { + it('reproduction: a smart pointer cast to, and what a method is declared to return', async () => { + const cg = await indexed({ + ...ITERATORS, + 'db/db_iterator_test.cc': lines( + '#include ', + '#include "include/rocksdb/db.h"', + '#include "include/rocksdb/memtablerep.h"', + 'namespace rocksdb {', + 'int CountKeys(DB* db) {', + ' int n = 0;', + ' auto iter = std::unique_ptr(db->NewIterator());', + ' for (iter->SeekToFirst(); iter->Valid(); iter->Next()) n += iter->value();', + ' return n;', + '}', + 'bool FirstValid(Transaction* txn) {', + ' auto it = txn->GetIterator();', + ' it->SeekToFirst();', + ' return it->Valid();', + '}', + '} // namespace rocksdb', + ), + }); + try { + // IteratorBase's, which Iterator derives from; not MemTableRep::Iterator's. + expect(calls(cg, 'rocksdb::CountKeys')).toEqual([ + 'rocksdb::DB::NewIterator @0.9', + 'rocksdb::Iterator::value @0.9', + 'rocksdb::IteratorBase::Next @0.9', + 'rocksdb::IteratorBase::SeekToFirst @0.9', + 'rocksdb::IteratorBase::Valid @0.9', + ]); + expect(calls(cg, 'rocksdb::FirstValid')).toEqual([ + 'rocksdb::IteratorBase::SeekToFirst @0.9', + 'rocksdb::IteratorBase::Valid @0.9', + 'rocksdb::Transaction::GetIterator @0.9', + ]); + } finally { + cg.close(); + } + }); + + it("a smart pointer to a class template of the project's, whatever its arguments", async () => { + // rocksdb's ExternalTableReaderAdapter holds an ExternalTableReaderBase. + const cg = await indexed({ + 'table/external_table.cc': lines( + '#include ', + 'namespace rocksdb {', + 'template ', + 'class ExternalTableReaderBase {', + ' public:', + ' virtual ~ExternalTableReaderBase() {}', + ' int NewIterator() { return Mode; }', + '};', + 'class IndexFactoryReader {', + ' public:', + ' int NewIterator() { return 0; }', + '};', + 'template ', + 'class ExternalTableReaderAdapter {', + ' public:', + ' int Open() {', + ' auto reader = std::unique_ptr>(nullptr);', + ' return reader->NewIterator();', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::ExternalTableReaderAdapter::Open')).toEqual(['rocksdb::ExternalTableReaderBase::NewIterator @0.9']); + } finally { + cg.close(); + } + }); + + it('a cast, and a construction through a typedef', async () => { + // rocksdb's sync-point callbacks cast their argument; HashSkipListRep's + // `Bucket` is a typedef of a SkipList. + const cg = await indexed({ + 'monitoring/instrumented_mutex.h': lines( + 'namespace rocksdb {', + 'class InstrumentedMutex {', + ' public:', + ' void Lock() {}', + ' void Unlock() {}', + '};', + 'namespace port {', + 'class Mutex {', + ' public:', + ' void Lock() {}', + ' void Unlock() {}', + '};', + '} // namespace port', + '} // namespace rocksdb', + ), + 'memtable/skiplist.h': lines( + 'namespace rocksdb {', + 'template ', + 'class SkipList {', + ' public:', + ' void Insert(const Key& key) {}', + '};', + 'class InlineSkipList {', + ' public:', + ' void Insert(const char* key) {}', + '};', + '} // namespace rocksdb', + ), + 'db/merge_test.cc': lines( + '#include "monitoring/instrumented_mutex.h"', + '#include "memtable/skiplist.h"', + 'namespace rocksdb {', + 'void Callback(void* arg) {', + ' auto* mutex = static_cast(arg);', + ' mutex->Unlock();', + ' mutex->Lock();', + '}', + 'class HashSkipListRep {', + ' public:', + ' typedef SkipList Bucket;', + ' void Add(const char* key) {', + ' auto list = new Bucket();', + ' list->Insert(key);', + ' }', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Callback')).toEqual(['rocksdb::InstrumentedMutex::Lock @0.9', 'rocksdb::InstrumentedMutex::Unlock @0.9']); + expect(calls(cg, 'rocksdb::HashSkipListRep::Add')).toEqual(['rocksdb::SkipList::Insert @0.9']); + } finally { + cg.close(); + } + }); + + it("a bare call to a method the caller's class inherits, and a smart pointer made for a class", async () => { + // protobuf's `auto* h = micro_rep();` read an earlier function's + // `LargeRep* h`; a test helper's `Widget` shares a name with the UI's. + const cg = await indexed({ + 'src/google/protobuf/micro_string.h': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'struct LargeRep {', + ' void ChangeSize(int n) {}', + '};', + 'struct MicroRep {', + ' void ChangeSize(int n) {}', + '};', + 'class MicroStringBase {', + ' protected:', + ' MicroRep* micro_rep() { return nullptr; }', + ' LargeRep* large_rep() { return nullptr; }', + '};', + 'class MicroString : public MicroStringBase {', + ' public:', + ' void Grow();', + ' void Clear();', + '};', + 'namespace ui {', + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + '} // namespace ui', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/micro_string.cc': lines( + '#include "src/google/protobuf/micro_string.h"', + 'namespace google {', + 'namespace protobuf {', + 'void MicroString::Grow() {', + ' LargeRep* h = large_rep();', + ' h->ChangeSize(2);', + '}', + 'namespace test {', + 'class Widget {', + ' public:', + ' void Draw() {}', + '};', + '} // namespace test', + 'void MicroString::Clear() {', + ' auto* h = micro_rep();', + ' h->ChangeSize(0);', + '}', + 'namespace ui {', + 'void Paint() {', + ' auto w = std::make_unique();', + ' w->Draw();', + '}', + 'void Repaint() {', + ' auto w = Widget();', + ' w.Draw();', + '}', + '} // namespace ui', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::MicroString::Clear')).toEqual([ + 'google::protobuf::MicroRep::ChangeSize @0.9', + 'google::protobuf::MicroStringBase::micro_rep @0.9', + ]); + expect(calls(cg, 'google::protobuf::ui::Paint').filter((c) => c.includes('Draw'))).toEqual(['google::protobuf::ui::Widget::Draw @0.9']); + expect(calls(cg, 'google::protobuf::ui::Repaint').filter((c) => c.includes('Draw'))).toEqual(['google::protobuf::ui::Widget::Draw @0.9']); + } finally { + cg.close(); + } + }); + + it('a chained call through a smart pointer reaches a method of the class it holds', async () => { + const cg = await indexed({ + 'table/table_test.cc': lines( + '#include ', + 'namespace rocksdb {', + 'class PinnedReader {', + ' public:', + ' void SetPinnedData(int data) {}', + '};', + 'class OtherReader {', + ' public:', + ' void SetPinnedData(int data) {}', + '};', + 'class PinnedFactory {', + ' public:', + ' PinnedReader* last_reader() const { return nullptr; }', + '};', + 'void Pin() {', + ' auto factory = std::make_shared();', + ' factory->last_reader()->SetPinnedData(1);', + '}', + 'void PinAgain(const std::unique_ptr& factory) {', + ' factory->last_reader()->SetPinnedData(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::Pin')).toEqual(['rocksdb::PinnedFactory::last_reader @0.9', 'rocksdb::PinnedReader::SetPinnedData @0.85']); + expect(calls(cg, 'rocksdb::PinAgain')).toEqual(['rocksdb::PinnedFactory::last_reader @0.9', 'rocksdb::PinnedReader::SetPinnedData @0.85']); + } finally { + cg.close(); + } + }); + + it('the type of a local a chained call reads first is the same for its other calls', async () => { + // rocksdb's compaction test reads `cfd` while it works out a three-call + // chain on it; the local's type is kept for its other calls too. + const cg = await indexed({ + 'db/version_set.h': lines( + '#include ', + 'namespace rocksdb {', + 'class VersionStorageInfo {', + ' public:', + ' int LevelFiles(int level) const { return level; }', + '};', + 'class Version {', + ' public:', + ' VersionStorageInfo* storage_info() { return nullptr; }', + '};', + 'class ColumnFamilyData {', + ' public:', + ' Version* current() const { return nullptr; }', + '};', + 'class ColumnFamilySet {', + ' public:', + ' ColumnFamilyData* GetDefault() const { return nullptr; }', + '};', + 'class VersionSet {', + ' public:', + ' ColumnFamilySet* GetColumnFamilySet() { return nullptr; }', + '};', + '} // namespace rocksdb', + ), + 'db/compaction/compaction_job_test.cc': lines( + '#include "db/version_set.h"', + 'namespace rocksdb {', + 'class CompactionJobTestBase {', + ' public:', + ' void VerifyTables(int output_level);', + ' protected:', + ' std::unique_ptr versions_;', + '};', + 'void CompactionJobTestBase::VerifyTables(int output_level) {', + ' auto cfd = versions_->GetColumnFamilySet()->GetDefault();', + ' cfd->current()->storage_info()->LevelFiles(output_level);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::CompactionJobTestBase::VerifyTables')).toEqual([ + 'rocksdb::ColumnFamilyData::current @0.9', + 'rocksdb::ColumnFamilySet::GetDefault @0.85', + 'rocksdb::Version::storage_info @0.85', + 'rocksdb::VersionSet::GetColumnFamilySet @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('a chained call reaches the class the inner call is declared to return, not a namesake', async () => { + // protobuf's Java and C# generators each declare an EnumGenerator. + const cg = await indexed({ + 'src/google/protobuf/compiler/csharp/csharp_enum.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace csharp {', + 'class EnumGenerator {', + ' public:', + ' void Generate(int printer) {}', + '};', + '} // namespace csharp', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/java/generator_factory.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace java {', + 'class EnumGenerator {', + ' public:', + ' void Generate(int printer) {}', + '};', + 'class GeneratorFactory {', + ' public:', + ' EnumGenerator* NewEnumGenerator(int descriptor) const { return nullptr; }', + '};', + '} // namespace java', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/java/file.cc': lines( + '#include "src/google/protobuf/compiler/java/generator_factory.h"', + 'namespace google {', + 'namespace protobuf {', + 'namespace compiler {', + 'namespace java {', + 'void GenerateEnums(const GeneratorFactory* generator_factory) {', + ' generator_factory->NewEnumGenerator(1)->Generate(2);', + '}', + '} // namespace java', + '} // namespace compiler', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::compiler::java::GenerateEnums')).toEqual([ + 'google::protobuf::compiler::java::EnumGenerator::Generate @0.85', + 'google::protobuf::compiler::java::GeneratorFactory::NewEnumGenerator @0.9', + ]); + } finally { + cg.close(); + } + }); +}); + +describe('what keeps the call as it was', () => { + it('an initializer that goes on past its line is read to its end', async () => { + // rocksdb's options test casts a member it reads on the line above the cast. + const cg = await indexed({ + 'include/rocksdb/merge_operator.h': lines( + '#include ', + 'namespace rocksdb {', + 'class Customizable {', + ' public:', + ' template ', + ' T* CheckedCast() { return nullptr; }', + '};', + 'class MergeOperator : public Customizable {', + ' public:', + ' virtual const char* Name() const { return ""; }', + '};', + 'struct ColumnFamilyOptions {', + ' std::shared_ptr merge_operator;', + '};', + 'namespace test {', + 'class ChanglingMergeOperator : public MergeOperator {', + ' public:', + ' void SetName(const char* name) {}', + '};', + '} // namespace test', + '} // namespace rocksdb', + ), + 'options/options_test.cc': lines( + '#include "include/rocksdb/merge_operator.h"', + 'namespace rocksdb {', + 'void Rename(ColumnFamilyOptions* base_cf_opt) {', + ' auto* merge_operator = base_cf_opt->merge_operator', + ' ->CheckedCast();', + ' merge_operator->SetName("other");', + '}', + '} // namespace rocksdb', + ), + }); + try { + // Not the shared_ptr member's type, which has no SetName. + expect(calls(cg, 'rocksdb::Rename').filter((c) => c.includes('SetName'))).toEqual(['rocksdb::test::ChanglingMergeOperator::SetName @0.7']); + } finally { + cg.close(); + } + }); + + it('what a function template returns is a template parameter, after the macros that follow its header', async () => { + // protobuf's `Arena::Create` is declared `PROTOBUF_NDEBUG_INLINE static T* PROTOBUF_NONNULL Create(…)`. + const cg = await indexed({ + 'src/google/protobuf/arena.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class Arena {', + ' public:', + ' void Reset() {}', + ' template ', + ' PROTOBUF_FUTURE_ADD_EARLY_NODISCARD PROTOBUF_NDEBUG_INLINE static T*', + ' PROTOBUF_NONNULL', + ' Create(Arena* arena, Args&&... args) {', + ' return new T();', + ' }', + '};', + 'template ', + 'class RepeatedPtrField {', + ' public:', + ' Element* Add() { return nullptr; }', + '};', + 'class MessageLite {', + ' public:', + ' int AccessCachedSize() const { return 0; }', + '};', + 'class Message : public MessageLite {};', + 'class Pool {', + ' public:', + ' void Reset() {}', + '};', + 'PROTOBUF_EXPORT Arena* PROTOBUF_NONNULL DefaultArena() { return nullptr; }', + '// Overloads by argument: what each returns depends on T.', + 'template ', + 'PROTOBUF_FUTURE_ADD_EARLY_NODISCARD std::shared_ptr DownCastMessage(const std::shared_ptr& from) {', + ' return nullptr;', + '}', + 'template ', + 'PROTOBUF_FUTURE_ADD_EARLY_NODISCARD const T& DownCastMessage(const MessageLite& from) {', + ' return *static_cast(&from);', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/repeated_ptr_field_unittest.cc': lines( + '#include "src/google/protobuf/arena.h"', + 'namespace google {', + 'namespace protobuf {', + 'void Fill(Arena* arena) {', + ' auto* field = Arena::Create>(arena);', + ' field->Add();', + '}', + 'int ByteSizeLong(const MessageLite& msg) {', + ' auto& _this = DownCastMessage(msg);', + ' return _this.AccessCachedSize();', + '}', + 'void Clear() {', + ' auto* arena = DefaultArena();', + ' arena->Reset();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Fill').filter((c) => c.includes('::Add'))).toEqual(['google::protobuf::RepeatedPtrField::Add @0.7']); + // Not read as the first overload's `std::shared_ptr`, which would call a library method. + expect(calls(cg, 'google::protobuf::ByteSizeLong').filter((c) => c.includes('AccessCachedSize'))).toEqual([ + 'google::protobuf::MessageLite::AccessCachedSize @0.7', + ]); + // A macro after the return type is no part of it. + expect(calls(cg, 'google::protobuf::Clear').filter((c) => c.includes('::Reset'))).toEqual(['google::protobuf::Arena::Reset @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('a type from outside the project reaches none of its classes', () => { + it('one under a namespace the project declares nothing in', async () => { + const cg = await indexed({ + 'upb/status.h': lines( + 'namespace upb {', + 'class Status {', + ' public:', + ' bool ok() const { return true; }', + '};', + '} // namespace upb', + ), + 'conformance/conformance_test.cc': lines( + '#include "absl/status/status.h"', + '#include "upb/status.h"', + 'namespace google {', + 'namespace protobuf {', + 'bool Passed(const absl::Status& status) {', + ' return status.ok();', + '}', + 'absl::Status Parse(int payload) { return absl::Status(); }', + 'bool Parsed(int payload) {', + ' return Parse(payload).ok();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + // A specialization the index reads as a method named `google::protobuf::absl::Cord`. + 'src/google/protobuf/lite_unittest.cc': lines( + '#include "absl/strings/cord.h"', + 'namespace google {', + 'namespace protobuf {', + 'template ', + 'T SerializeAs(const int& msg);', + 'template <>', + 'absl::Cord SerializeAs(const int& msg) {', + ' return absl::Cord();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Passed')).toEqual([]); + // What a function returns, for a chained call. + expect(calls(cg, 'google::protobuf::Parsed').filter((c) => c.includes('::ok'))).toEqual([]); + } finally { + cg.close(); + } + }); + + it("a standard one whose name a class of the project shares", async () => { + // fmt opens its namespace with a macro and declares an `ostream` of its own. + const cg = await indexed({ + 'include/fmt/os.h': lines( + '#define FMT_BEGIN_NAMESPACE namespace fmt { inline namespace v11 {', + '#define FMT_END_NAMESPACE } }', + 'FMT_BEGIN_NAMESPACE', + 'class ostream {', + ' public:', + ' void flush() {}', + '};', + 'FMT_END_NAMESPACE', + ), + 'src/ostream.cc': lines( + '#include ', + '#include "include/fmt/os.h"', + 'FMT_BEGIN_NAMESPACE', + 'void flush_stream(std::ostream& os) {', + ' os.flush();', + '}', + 'FMT_END_NAMESPACE', + ), + }); + try { + expect(calls(cg, 'flush_stream')).toEqual([]); + } finally { + cg.close(); + } + }); + + it('not one of a namespace the project declares, which the file does not see', async () => { + const cg = await indexed({ + 'src/google/protobuf/descriptor.pb.h': lines( + 'namespace google {', + 'namespace protobuf {', + 'class FileDescriptorProto {', + ' public:', + ' void Clear() {}', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/compiler/java/shared_code_generator.cc': lines( + 'namespace google {', + 'namespace protobuf {', + 'void Reset(google::protobuf::FileDescriptorProto& file_proto) {', + ' file_proto.Clear();', + '}', + '} // namespace protobuf', + '} // namespace google', + ), + }); + try { + expect(calls(cg, 'google::protobuf::Reset')).toEqual(['google::protobuf::FileDescriptorProto::Clear @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/cpp-namespaces.ts b/src/resolution/cpp-namespaces.ts index 79c4d9ca68..e7221d43ed 100644 --- a/src/resolution/cpp-namespaces.ts +++ b/src/resolution/cpp-namespaces.ts @@ -76,6 +76,11 @@ function cppNamespaceMacros(context: ResolutionContext): { openers: Map path[0]!); +} + /** The line ranges of a C / C++ file each namespace macro opens, with the namespace path it opens. */ export function cppMacroNamespaceFrames(file: string, context: ResolutionContext): Array<{ start: number; end: number; path: string[] }> { let memo = CPP_NS_FRAMES.get(context); diff --git a/src/resolution/cpp-type-aliases.ts b/src/resolution/cpp-type-aliases.ts index 85de85939b..3fbd1cc5ca 100644 --- a/src/resolution/cpp-type-aliases.ts +++ b/src/resolution/cpp-type-aliases.ts @@ -349,7 +349,10 @@ export function clearCppTypeAliasMemos(context: ResolutionContext): void { function collectTemplateHeader(lines: readonly string[], startLine: number, startColumn: number, names: Set): void { const before = [...lines.slice(Math.max(0, startLine - 4), startLine - 1), (lines[startLine - 1] ?? '').slice(0, startColumn)].join('\n'); const at = before.lastIndexOf('template'); - const header = at < 0 ? null : /^template\s*<([\s\S]*)>\s*$/.exec(before.slice(at)); + // Specifiers, attributes and macros may come between the header and where + // the declaration starts: protobuf's `template ` / `PROTOBUF_NDEBUG_INLINE static T* Create(…)`. + const header = at < 0 ? null : /^template\s*<([\s\S]*)>(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*$/.exec(before.slice(at)); if (!header) return; for (const item of splitTopLevel(header[1]!)) { // `typename T = int`, `template class Policy`, `typename... Ts`, `int N`. diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 83cae1579c..70c16bd830 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -15,7 +15,7 @@ import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; 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 { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNamespaceAliases } from './cpp-namespaces'; import { clearCppRawStringMemos, cppLiteralsBlanked, cppRawStringLines } from './cpp-raw-strings'; import { cppClassWritten, type CppNameSite } from './cpp-supertypes'; import { cppIncludedFile, cppIncluders } from './cpp-includers'; @@ -9055,7 +9055,9 @@ function cppWrittenReturnType(fn: Node, context: ResolutionContext): string | nu .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+)+/, '')); + .replace(/^\s*(?:[A-Z][A-Z0-9]*_[A-Z0-9_]*(?:\([^()]*\))?\s+)+/, '') + // A trailing one: `T* PROTOBUF_NONNULL`. + .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; } @@ -9218,11 +9220,14 @@ function cppMemberObject(object: string, op: string, at: UnresolvedRef, context: /** * 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. + * ranges of `auto` loops and the initializers of `auto` locals: a variable, + * parameter or member the calling function or its class declares; `*x`; + * `this`; a call to a function or method whose declaration writes its return + * type — `FieldNumberOrder(desc)`, `vstorage->GetBlobFiles()`, a bare + * `NewIterator()` of the caller's class; and what a construction (`new + * Foo(…)`, `Foo(…)`, `std::make_unique(…)`) or a cast + * (`static_cast(…)`, `std::unique_ptr(…)`) makes. 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; @@ -9243,7 +9248,7 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC if (name) { const decl: CppReceiverDeclaration = {}; inferCppReceiverType(name[1]!, at, context, depth + 1, decl); - if (decl.element) return decl.element; + if (decl.declared) return decl.declared; // `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); @@ -9253,6 +9258,9 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC // whose class is declared in db_impl.h): a data member of its class. return decl.shadowed || !owner ? null : cppDataMemberType(owner, name[1]!, context); } + // `new Foo(…)`: a pointer to what it constructs. + const created = /^new\s+((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}()]*>)?)\s*(?:[({[][\s\S]*)?$/.exec(text); + if (created) return { ...callers, text: `${created[1]!.trim()}*` }; 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); @@ -9262,6 +9270,14 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC } const open = cppOpeningParen(text, text.length - 1); const callee = open > 0 ? text.slice(0, open).trim() : ''; + // `static_cast(…)`: the type it casts to. + const cast = /^(?:static|dynamic|const|reinterpret)_cast\s*<([\s\S]+)>$/.exec(callee); + if (cast) return { ...callers, text: cast[1]!.trim() }; + // `std::make_unique(…)`: the smart pointer it makes. + const made = /^(?:::\s*)?(?:std|absl)\s*::\s*make_(unique|shared)\s*<([\s\S]+)>$/.exec(callee); + if (made) return { ...callers, text: `std::${made[1]}_ptr<${made[2]!.trim()}>` }; + // `std::unique_ptr(…)`: the smart pointer or optional it makes. + if (cppStdHeldType(callee)) return { ...callers, text: callee }; // `recv.method(…)`, `recv->method(…)`: the method of the receiver's class. const member = /^(.+?)\s*(\.|->)\s*([A-Za-z_]\w*)$/.exec(callee); if (member) { @@ -9269,14 +9285,20 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC const cls = object && cppWrittenClass(object, at, context); return cls ? cppReturnTypeOf(cppMethodsOf(cls, member[3]!, at, context), context) : null; } + if (!/^(?:[A-Za-z_]\w*\s*::\s*)*[A-Za-z_]\w*(?:\s*<[\s\S]*>)?$/.test(callee)) return null; + const spelled = stripCppTemplateArguments(callee).replace(/\s+/g, ''); + // `NewIterator(…)` in a method: its class's own or inherited method first. + const ownClass = owner && !spelled.includes('::') ? cppClassDecls(owner, context)[0] : undefined; + const own = ownClass ? cppMethodsOf(ownClass, spelled, at, context) : []; + if (own.length > 0) return cppReturnTypeOf(own, context); // `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; + // `Foo(…)`, `Foo(…)`: what it constructs, when C++ lookup finds the class. + const constructed = cppClassWritten(callee, callers, at, context); + return constructed?.exact && CPP_CLASS_KINDS.has(constructed.node.kind) ? { ...callers, text: callee } : null; } const CPP_LOOP_ELEMENTS = new WeakMap>(); @@ -9287,7 +9309,8 @@ const CPP_LOOP_ELEMENTS = new WeakMap& 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. + * `auto` local whose initializer can't be read. (It is worked out once per + * loop, so with its own depth budget, whatever lookup first asks for it.) */ function cppAutoLoopElement( lines: readonly string[], @@ -9295,7 +9318,6 @@ function cppAutoLoopElement( 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())); @@ -9305,7 +9327,7 @@ function cppAutoLoopElement( 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 type = range ? cppExpressionType(range, at, context, 0) : null; const element = type && cppRangeElementOf(type, at, context); const followed = element && cppFollowWrittenAliases(element, at, context); const result = followed && !isCppTemplateParameterType(followed) ? followed : null; @@ -9313,6 +9335,59 @@ function cppAutoLoopElement( return result; } +const CPP_AUTO_LOCALS = new WeakMap>(); + +/** + * The type of the initializer `init` of the `auto` local `name` declared on + * line `line`, as written where it is declared — what the local is: `auto + * iter = std::unique_ptr(…)` is a `std::unique_ptr`, and + * `auto it = txn->GetIterator(…)` the `Iterator*` that + * `Transaction::GetIterator` is declared to return. The index keeps only a + * return type's last name, which a namesake shares: by it, rocksdb's `auto + * iter = …; iter->SeekToFirst()` reached `MemTableRep::Iterator`. Null when + * the type can't be read reliably. (As for a loop, it is worked out once, + * with its own depth budget.) + */ +function cppAutoLocalType( + init: string, + line: number, + name: string, + ref: UnresolvedRef, + context: ResolutionContext, +): CppWrittenType | null { + let memo = CPP_AUTO_LOCALS.get(context); + if (!memo) CPP_AUTO_LOCALS.set(context, (memo = new Map())); + const key = `${ref.fromNodeId}\u0000${ref.filePath}\u0000${line}\u0000${name}`; + if (memo.has(key)) return memo.get(key)!; + // (Unknown while it is worked out, so an initializer naming its own local ends.) + memo.set(key, null); + const at = { ...ref, line, column: 0 }; + const type = cppExpressionType(init, at, context, 0); + const followed = type && cppFollowWrittenAliases(type, at, context); + // What a template returns may be what its arguments make it, which are not + // substituted, and its overloads may return different types: protobuf's + // `DownCastMessage` returns a `const T&`, a `T*` or a + // `std::shared_ptr`, by its argument. + const result = followed && !isCppTemplateDependentType(followed, context) ? followed : null; + memo.set(key, result); + return result; +} + +/** + * Is the C++ type `type` a template parameter's where it is written (`T*`, + * `const T&`), or a library template of one (`std::shared_ptr`, + * `std::vector`), whose members are the parameter's? A class template of + * the project's is that class whatever its arguments: `ProtectionInfoKVOS`, + * or the one a smart pointer holds (`std::unique_ptr>`). + */ +function isCppTemplateDependentType(type: CppWrittenType, context: ResolutionContext): boolean { + if (type.parameters.size === 0) return false; + if (isCppTemplateParameterType(type)) return true; + const held = cppStdHeldType(type.text); + if (held) return isCppTemplateDependentType({ ...type, text: held }, context); + return isCppLibraryType(type.text, context) && (type.text.match(/[A-Za-z_]\w*/g) ?? []).some((word) => type.parameters.has(word)); +} + /** 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++) { @@ -9412,11 +9487,12 @@ interface CppReceiverDeclaration { */ 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). + * The receiver's type as written where it is declared, when its own + * declaration says `auto` (`raw` is its text): the element type of the + * range an `auto` range-for iterates, or the type of an `auto` local's + * initializer — the `Iterator*` that `txn->GetIterator(…)` returns. */ - element?: CppWrittenType; + declared?: CppWrittenType; /** * The first declaration the scan met is an `auto` local's, on this line, * with this initializer: `vstorage->GetBlobFiles()` in `const auto& @@ -9635,14 +9711,20 @@ function cppCallSite(caller: Node, ref: UnresolvedRef, context: ResolutionContex * - null when that class surely has no such method (cppLacksMethod): * protobuf's descriptor accessors aren't indexed, and `field->full_name()` * reached unrelated classes' by the method's name. + * - null when the type is from outside the project, whatever class of the + * project shares its name: a standard or abseil one with a lowercase name + * (isCppLibraryType) — leveldb's `std::string data_; data_.data()` reached + * `Slice::data`, and fmt declares an `ostream` of its own with a `flush` — + * or one under a namespace the project declares nothing in: protobuf's + * `absl::Status status; status.ok()` reached `upb::Status::ok`. * - 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. + * that operator), or the lookup doesn't place the 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, @@ -9651,23 +9733,71 @@ function matchCppDeclaredTypeCall( ref: UnresolvedRef, context: ResolutionContext, ): ResolvedRef | null | undefined { - if (!decl.callers || decl.shadowed || !decl.raw || isCppLibraryType(decl.raw, context)) return undefined; + if (!decl.callers || decl.shadowed || !decl.raw) return undefined; if (cppMemberOperator(receiver, ref, context) !== (decl.pointer ? '->' : '.')) return undefined; + // (Not so for `->` on a value — a smart pointer, optional or iterator hands + // the call to its element type — which the operator check above leaves be.) + if (!decl.aliased && isCppLibraryType(decl.raw, context)) return null; 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); + // An `auto` variable's type is looked up where it is written: where its + // range's type or its initializer's function is declared. + const site = decl.declared ?? cppCallSite(caller, ref, context); const found = cppClassWritten(aliasedClass ? `::${aliasedClass}` : decl.raw, site, ref, context); + if (found === undefined && !aliasedClass && isCppOutsideQualifiedType(decl.raw, site, context)) return null; 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 cppLacksMethod(found.node, context) ? null : undefined; } +/** + * Is the C++ type `raw`, which nothing the file sees declares, written under + * a namespace the project declares no type in — `absl::Status`, + * `grpc::ServerContext` — so that it is from outside the project? Not when + * that first name is a template parameter or a namespace alias. A lowercase + * name, and anything of `std`, is isCppLibraryType's to tell: a trait or a + * member type may name one of the project's (`std::conditional_t`, `std::vector::value_type`). + */ +function isCppOutsideQualifiedType(raw: string, site: CppNameSite, context: ResolutionContext): boolean { + const segments = cppTypeSegments(raw); + if (!segments || segments.length < 2) return false; + const head = segments[0]!; + if (head === 'std' || !/^[A-Z]/.test(segments[segments.length - 1]!)) return false; + if (site.parameters.has(head) || cppNamespaceAliases(context).has(head)) return false; + if (cppMacroNamespaceFrames(site.file, context).some((frame) => frame.path.includes(head))) return false; + return !cppProjectScopeNames(context).has(head); +} + +const CPP_SCOPE_NAMES = new WeakMap>(); + +/** + * Every name in the qualified names of the types the project's C and C++ + * code declares — a namespace (`google`, `ROCKSDB_NAMESPACE`), a class, + * whatever a qualifier may start with from some scope — and in the + * namespaces its macros open (`fmt` for `FMT_BEGIN_NAMESPACE`), which no + * qualified name shows. (Not those of functions: protobuf's conversion + * operators read as methods named `google::protobuf::absl::Cord`.) + */ +function cppProjectScopeNames(context: ResolutionContext): Set { + const hit = CPP_SCOPE_NAMES.get(context); + if (hit) return hit; + const names = new Set(cppMacroNamespaceHeads(context)); + for (const kind of ['class', 'struct', 'union', 'enum', 'type_alias'] as const) { + for (const n of context.iterateNodesByKind?.(kind) ?? context.getNodesByKind(kind)) { + if (n.language !== 'cpp' && n.language !== 'c') continue; + for (const segment of n.qualifiedName.split('::')) if (segment) names.add(segment); + } + } + CPP_SCOPE_NAMES.set(context, names); + return names; +} + /** * Whether a C++ class with no method of a name, its own or one it inherits * through its base edges, surely has none: it is no class template, whose @@ -10119,7 +10249,7 @@ function inferCppReceiverType( ref: UnresolvedRef, context: ResolutionContext, depth = 0, - found?: CppReceiverDeclaration, + found: CppReceiverDeclaration = {}, ): string | null { // Per-file lines cache when available — this runs per `receiver->method()` // ref and re-splitting the file each time is the same quadratic as the @@ -10151,17 +10281,30 @@ function inferCppReceiverType( // `auto x = Foo::instance();` — the declared type is deduced; recover it // from the initializer (call return type / construction) (#645). const init = cppAutoInitializer(line, receiverName); - if (init && found && !found.shadowed && !found.initializer) found.initializer = { line: i + 1, text: init }; + const statement = cppAutoInitializerStatement(line, lines, i, receiverName, rawLines); + if (init && !found.shadowed && !found.initializer) found.initializer = { line: i + 1, text: statement ?? init }; + // The calling function's own local has the type its initializer has + // where that is written: `auto iter = std::unique_ptr(…)`, + // or the `Iterator*` that `txn->GetIterator(…)` is declared to return. + const local = statement && !found.shadowed && isCppCallersDeclaration(ref.filePath, i + 1, ref, context) + ? cppAutoLocalType(statement, i + 1, receiverName, ref, context) + : null; + const localType = local && normalizeCppTypeName(local.text); + if (local && localType && localType !== 'auto') { + noteCppDeclaration(found, local.text, true); + found.declared = local; + return cppDeclaredType(local.text, localType, true, ref, context, found); + } 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. - if (found) found.shadowed = true; + found.shadowed = true; } else if (normalized) { const inCallerScope = isCppCallersDeclaration(ref.filePath, i + 1, ref, context); noteCppDeclaration(found, declaratorMatch[1]!, inCallerScope); return cppDeclaredType(declaratorMatch[1]!, normalized, inCallerScope, ref, context, found); - } else if (found && cppTypeBeganAbove(declaratorMatch[1] ?? '')) { + } else if (cppTypeBeganAbove(declaratorMatch[1] ?? '')) { // The end of a declaration begun on an earlier line, maybe the // receiver's: one found further up may be another variable. found.shadowed = true; @@ -10179,15 +10322,15 @@ 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 element = normalized === 'auto' ? cppAutoLoopElement(lines, i, loopVar, ref, context) : null; const elementType = element && normalizeCppTypeName(element.text); if (element && elementType && elementType !== 'auto') { noteCppDeclaration(found, element.text, true); - if (found) found.element = element; + found.declared = element; return cppDeclaredType(element.text, elementType, true, ref, context, found); } } - if (found && cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; + if (cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; } } @@ -10332,6 +10475,44 @@ function cppAutoInitializer(line: string, receiverName: string): string | null { return m?.[1]?.trim() || null; } +/** + * The whole initializer of `auto receiverName = …;`, which the code `line` + * (`lines[index]`, comments and literals blanked) begins: read on to the `;` + * that ends it, since `auto* op = opts->merge_operator` may go on with + * `->CheckedCast();` on the next line. Null when no `;` ends it + * within a few lines. + */ +function cppAutoInitializerStatement( + line: string, + lines: readonly string[], + index: number, + receiverName: string, + rawLines: ReturnType, +): string | null { + const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const start = new RegExp(`\\b${escaped}\\b\\s*=(?!=)`).exec(line); + if (!start) return null; + let text = line.slice(start.index + start[0].length); + for (let next = index + 1; ; next++) { + const end = cppStatementEnd(text); + if (end >= 0) return text.slice(0, end).trim() || null; + if (next >= lines.length || next > index + 4) return null; + text += ` ${cppCodeOf(cppLiteralsBlanked(lines[next]!, next + 1, rawLines)) ?? ''}`; + } +} + +/** Where the statement that `text` begins ends: its first `;` outside brackets, or -1. */ +function cppStatementEnd(text: string): number { + let depth = 0; + for (let i = 0; i < text.length; i++) { + const c = text[i]!; + if (c === '(' || c === '[' || c === '{') depth++; + else if (c === ')' || c === ']' || c === '}') depth--; + else if (c === ';' && depth <= 0) return i; + } + return -1; +} + /** * Recover the type of an `auto`-declared local from its initializer `init` on * the declaration line — `auto x = Foo::instance();`, `auto w = make_unique();`, @@ -10366,11 +10547,62 @@ export function matchCppCallChain( ): ResolvedRef | null { const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/); if (!m || !m[1] || !m[2]) return null; + const looked = matchCppChainOnWrittenType(m[1], m[2], ref, context); + if (looked !== undefined) return looked; const cls = resolveCppCallResultType(m[1], ref, context); if (!cls) return null; return resolveMethodOnType(cls, m[2], ref, context, 0.85, 'instance-method'); } +/** + * A C++ chained call `inner().method` whose inner call's return type is read + * as its declaration writes it: `method` is that class's, looked up as C++ + * looks the type up there, or no project method's. By the type's last name, + * protobuf's `generator_factory_->NewEnumGenerator(…)->Generate(…)` in the + * Java generator reached `csharp::EnumGenerator`, and a call on an + * `absl::Status` reached `upb::Status`. + * - The method of that class or of a class it derives from, 0.85 as before. + * - null when the type is from outside the project, or a class that surely + * lacks the method (cppLacksMethod). + * - undefined when the type can't be read or placed: the last-name path + * decides, as before. + */ +function matchCppChainOnWrittenType(inner: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined { + const type = cppChainInnerType(inner, ref, context); + const followed = type && cppFollowWrittenAliases(type, ref, context); + if (!followed || isCppTemplateDependentType(followed, context)) return undefined; + // (The chain is spelled with `.` for `->` too; `->` on a pointer or a + // smart pointer reaches what it points at, and `.` on one isn't a call of + // the project's.) + const object = cppPointee(followed) ?? followed; + if (isCppLibraryType(object.text, context)) return null; + const found = cppClassWritten(object.text, object, ref, context); + if (found === undefined && isCppOutsideQualifiedType(object.text, object, context)) return null; + if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return undefined; + const target = cppMethodOf(found.node, method, ref, context); + if (target) return { original: ref, targetNodeId: target.id, confidence: 0.85, resolvedBy: 'instance-method' }; + return cppLacksMethod(found.node, context) ? null : undefined; +} + +/** + * What the inner call of a C++ chain returns, as written where it is + * declared: `recv.method` (the extractor spells `->` as `.`), `Class::method`, + * `func`. Null when that can't be read. + */ +function cppChainInnerType(inner: string, ref: UnresolvedRef, context: ResolutionContext): CppWrittenType | null { + const dot = inner.lastIndexOf('.'); + if (dot < 0) return cppExpressionType(`${inner}()`, ref, context, 1); + const recv = inner.slice(0, dot); + if (!/^[A-Za-z_]\w*$/.test(recv)) return null; + const op = cppMemberOperator(recv, ref, context); + const recvType = op ? cppExpressionType(recv, ref, context, 1) : null; + const followed = recvType && cppFollowWrittenAliases(recvType, ref, context); + if (!followed) return null; + const object = op === '->' ? cppPointee(followed) : isCppPointerType(followed.text) ? null : followed; + const cls = object && cppWrittenClass(object, ref, context); + return cls ? cppReturnTypeOf(cppMethodsOf(cls, inner.slice(dot + 1), ref, context), context) : null; +} + /** * Resolve a `::`-scoped factory chain whose receiver is a scoped/static call — * PHP `Cls::for($x)->method()` (#608, the per-credential Laravel client idiom) or @@ -10734,6 +10966,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_BASE_SPECS.delete(context); CPP_MEMBER_MACRO_ARGS.delete(context); CPP_LOOP_ELEMENTS.delete(context); + CPP_AUTO_LOCALS.delete(context); + CPP_SCOPE_NAMES.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context); @@ -12027,19 +12261,6 @@ export function matchMethodCall( // followed. if (isUndeclaredTypeName(cppDecl.written!, ref, context)) return null; } - // A C++ receiver the calling function or its class declares as a type - // from outside the project with a lowercase name calls that type's own - // member, through `.` or a raw pointer's `->`: leveldb's `std::string - // data_; data_.data()` went to `Slice::data`, protobuf's `std::string - // proto; proto.append(…)` to `LeftoverBuffer::append`. Not so for `->` - // on such a value — an iterator, smart pointer or optional hands the call - // to its element type — nor for a call that doesn't fit the declaration - // (`.` on a pointer), which then is another variable's. - if (ref.language === 'cpp' && cppDecl.callers && !cppDecl.aliased && !cppDecl.shadowed && - cppMemberOperator(objectOrClass!, ref, context) === (cppDecl.pointer ? '->' : '.') && - isCppLibraryType(cppDecl.raw!, context)) { - return null; - } // A `->` call on a standard smart pointer or optional the calling // function or its class declares is a call on the type it holds: // rocksdb's `std::unique_ptr iter; iter->Valid()` reached From df3ce972f7d5bbb82d75bcc0e24f2047616fba36 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 13 Oct 2026 00:46:13 +0000 Subject: [PATCH 2/3] fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared (#2533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Receiver inference read back through the call's file and the header of the same name only, so a member of a class declared elsewhere — rocksdb's DBImpl is declared in db_impl.h, its methods in db_impl_write.cc, db_impl_compaction_flush.cc, db_impl_open.cc and more — was never read, and calls on it (`versions_->GetColumnFamilySet()`, `mutex_.Lock()`) were guessed by the receiver's name. - Where the scan leaves the calling function, the name is looked up as a data member of the function's class before anything outside it (cppCallersMember): the class's own declaration lines wherever it is declared (the declaration in the caller's own translation unit, of classes sharing a name), then the classes it derives from through their base edges, skipping a base that depends on the template's parameters. A gtest `TEST_F` / `TEST_P` body is a method of a class derived from its fixture, so `db_` there is the fixture's. - The member goes into the declaration record as the caller's own, with its class as the lookup site, so #2493's lookup, the library-type gate and #2440's held-type path apply. A member of a template parameter's type, or of an alias of one, and a name the scan saw an unreadable declaration of, stay as before. (isCppTemplateDependentType now also counts a std trait of a parameter: protobuf's `using MapFieldBase = std::conditional_t`.) - cppDataMemberIn reads only declarations outside parentheses and not followed by an argument list (method declarations have no nodes, so their parameters are in the class's lines), skips nested functions and classes, follows block comments across lines, and blanks string literals. - A member declared below an inline method that uses it, and a member of the class itself rather than of a class nested in it, are found the same way; cppExpressionType's own member fallback became redundant. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-member-receiver.test.ts | 510 ++++++++++++++++++++++++++ src/resolution/name-matcher.ts | 256 +++++++++++-- 3 files changed, 744 insertions(+), 23 deletions(-) create mode 100644 __tests__/cpp-member-receiver.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cd7b71df5..01aa134aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, a call on a variable, parameter or member declared as 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. - In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. - In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr(…)` or `auto* mutex = static_cast(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. Re-index C++ projects after upgrading. +- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic shutting_down_`, no longer links to a project method that only shares the method's name. 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-member-receiver.test.ts b/__tests__/cpp-member-receiver.test.ts new file mode 100644 index 0000000000..5fe0408bc0 --- /dev/null +++ b/__tests__/cpp-member-receiver.test.ts @@ -0,0 +1,510 @@ +/** + * A C++ call on a data member of the calling method's class reaches the + * method of the member's type, wherever the class is declared. + * + * Receiver inference reads back through the call's file, then the header of + * the same name (`foo.cc` → `foo.h`). In facebook/rocksdb, `DBImpl` is + * declared in db_impl.h and its methods are defined in db_impl_write.cc, + * db_impl_compaction_flush.cc, db_impl_open.cc and more, so `versions_`, + * `mutex_` and the rest were never read there: their calls were guessed by + * the receiver's name. Neither was a member declared below an inline method + * that uses it, or one a base class declares. A gtest `TEST_F(Fixture, …)` + * body is a method of a class derived from `Fixture`, so `db_` there is the + * fixture's member. + * + * C++ looks a name up in the function first, then in its class and the + * classes it derives from: a parameter or local of the name, or one the scan + * can't read, still decides. A guess can land on the right method by a shared + * word, so these tests check the 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-member-')); + 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'); + +/** rocksdb's DBImpl, trimmed: declared in db_impl.h, its methods defined in other files. */ +const ROCKSDB = { + 'db/version_set.h': lines( + 'namespace rocksdb {', + 'class ColumnFamilySet {', + ' public:', + ' int NumberOfColumnFamilies() const { return 0; }', + '};', + 'class VersionSet {', + ' public:', + ' ColumnFamilySet* GetColumnFamilySet() { return nullptr; }', + ' void SetLastSequence(int s) {}', + '};', + 'class ColumnFamilyHandleImpl {', + ' public:', + ' ColumnFamilySet* GetColumnFamilySet() { return nullptr; }', + ' void SetLastSequence(int s) {}', + '};', + 'class Statistics {', + ' public:', + ' bool load() const { return false; }', + '};', + '} // namespace rocksdb', + ), + 'monitoring/instrumented_mutex.h': lines( + 'namespace rocksdb {', + 'class InstrumentedMutex {', + ' public:', + ' void Lock() {}', + ' void Unlock() {}', + '};', + 'namespace port {', + 'class Mutex {', + ' public:', + ' void Lock() {}', + ' void Unlock() {}', + '};', + '} // namespace port', + '} // namespace rocksdb', + ), + 'db/db_impl/db_impl.h': lines( + '#include ', + '#include ', + '#include "db/version_set.h"', + '#include "monitoring/instrumented_mutex.h"', + 'namespace rocksdb {', + 'class DBImpl {', + ' public:', + ' void WriteImpl();', + ' void FlushMemTable();', + ' protected:', + ' std::unique_ptr versions_;', + ' InstrumentedMutex mutex_;', + ' std::atomic shutting_down_;', + '};', + '} // namespace rocksdb', + ), +}; + +describe('a data member of the calling method’s class', () => { + it('reproduction: one the class declares in another header', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_impl/db_impl_write.cc': lines( + '#include "db/db_impl/db_impl.h"', + 'namespace rocksdb {', + '// Another function\'s local of the name, which the method does not see.', + 'void UnlockStale(port::Mutex* mutex_) { mutex_->Unlock(); }', + 'void DBImpl::WriteImpl() {', + ' mutex_.Lock();', + ' versions_->SetLastSequence(1);', + ' versions_->GetColumnFamilySet()->NumberOfColumnFamilies();', + ' if (shutting_down_.load()) return;', + ' mutex_.Unlock();', + '}', + '} // namespace rocksdb', + ), + }); + try { + // A `std::atomic` member calls no method of the project's. + expect(calls(cg, 'rocksdb::DBImpl::WriteImpl')).toEqual([ + 'rocksdb::ColumnFamilySet::NumberOfColumnFamilies @0.85', + 'rocksdb::InstrumentedMutex::Lock @0.9', + 'rocksdb::InstrumentedMutex::Unlock @0.9', + 'rocksdb::VersionSet::GetColumnFamilySet @0.9', + 'rocksdb::VersionSet::SetLastSequence @0.9', + ]); + } finally { + cg.close(); + } + }); + + it('one declared below the inline method that uses it, and one a base class declares', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/memtable_list.h': lines( + 'namespace rocksdb {', + 'class MemTableListVersion {', + ' public:', + ' int NumNotFlushed() const { return 0; }', + '};', + 'class MemTable {', + ' public:', + ' int NumNotFlushed() const { return 1; }', + '};', + 'class MemTableList {', + ' public:', + ' bool IsFlushPending() const { return current_->NumNotFlushed() > 0; }', + ' // A line that starts with a dereference is code, not a comment.', + ' int Pick(const int* first) const {', + ' return Choose(0,', + ' *first);', + ' }', + ' private:', + ' int Choose(int a, int b) const { return a + b; }', + ' /*', + ' MemTable* current_;', + ' */', + ' MemTableListVersion* current_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl/db_impl_secondary.h': lines( + '#include "db/db_impl/db_impl.h"', + 'namespace rocksdb {', + 'class DBImplSecondary : public DBImpl {', + ' public:', + ' void TryCatchUpWithPrimary();', + '};', + '} // namespace rocksdb', + ), + 'db/db_impl/db_impl_secondary.cc': lines( + '#include "db/db_impl/db_impl_secondary.h"', + 'namespace rocksdb {', + 'void DBImplSecondary::TryCatchUpWithPrimary() {', + ' versions_->SetLastSequence(2);', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MemTableList::IsFlushPending')).toEqual(['rocksdb::MemTableListVersion::NumNotFlushed @0.9']); + expect(calls(cg, 'rocksdb::DBImplSecondary::TryCatchUpWithPrimary')).toEqual(['rocksdb::VersionSet::SetLastSequence @0.9']); + } finally { + cg.close(); + } + }); + + it("a gtest TEST_F body's fixture member", async () => { + const cg = await indexed({ + 'include/rocksdb/iterator.h': lines( + 'namespace rocksdb {', + 'class IteratorBase {', + ' public:', + ' virtual void SeekToFirst() = 0;', + '};', + 'class Iterator : public IteratorBase {};', + 'class MemTableRep {', + ' public:', + ' class Iterator {', + ' public:', + ' virtual void SeekToFirst() = 0;', + ' };', + '};', + 'class DB {', + ' public:', + ' virtual Iterator* NewIterator() = 0;', + '};', + '} // namespace rocksdb', + ), + 'db/db_test_util.h': lines( + '#include "include/rocksdb/iterator.h"', + 'namespace rocksdb {', + 'class DBTestBase {', + ' protected:', + ' DB* db_;', + '};', + '} // namespace rocksdb', + ), + 'db/db_iterator_test.cc': lines( + '#include "db/db_test_util.h"', + 'namespace rocksdb {', + 'class DBIteratorTest : public DBTestBase {};', + 'TEST_F(DBIteratorTest, IterSeekToFirst) {', + ' auto iter = db_->NewIterator();', + ' iter->SeekToFirst();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::TEST_F')).toEqual(['rocksdb::DB::NewIterator @0.9', 'rocksdb::IteratorBase::SeekToFirst @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('a data member of the class itself', () => { + it('not one of a class nested in it, which its header declares first', async () => { + // rocksdb's `MultiScan::MultiScanIterator` has a `scan_opts_` of its own. + const cg = await indexed({ + 'table/multi_scan.h': lines( + 'namespace rocksdb {', + 'class ScanOptions {', + ' public:', + ' int size() const { return 0; }', + '};', + 'class ScanRanges {', + ' public:', + ' int size() const { return 1; }', + '};', + 'class MultiScan {', + ' public:', + ' class MultiScanIterator {', + ' private:', + ' ScanRanges* scan_opts_;', + ' };', + ' int Count() const;', + ' private:', + ' ScanOptions* scan_opts_;', + '};', + '} // namespace rocksdb', + ), + 'table/multi_scan.cc': lines( + '#include "table/multi_scan.h"', + 'namespace rocksdb {', + 'int MultiScan::Count() const { return scan_opts_->size(); }', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::MultiScan::Count')).toEqual(['rocksdb::ScanOptions::size @0.9']); + } finally { + cg.close(); + } + }); +}); + +describe('which class declares the member', () => { + it('the declaration in the caller’s own translation unit, of classes that share a name', async () => { + // google/leveldb's memenv.cc and fault_injection_test.cc each declare a `FileState`. + const cg = await indexed({ + 'include/leveldb/slice.h': lines( + 'namespace leveldb {', + 'class Slice {', + ' public:', + ' int size() const { return 0; }', + '};', + 'class Block {', + ' public:', + ' int size() const { return 1; }', + '};', + '} // namespace leveldb', + ), + 'helpers/memenv/memenv.cc': lines( + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class FileState {', + ' public:', + ' int Size() const;', + ' private:', + ' Slice* blocks_;', + '};', + 'int FileState::Size() const { return blocks_->size(); }', + '} // namespace leveldb', + ), + 'db/fault_injection_test.cc': lines( + '#include "include/leveldb/slice.h"', + 'namespace leveldb {', + 'class FileState {', + ' public:', + ' int Size() const;', + ' private:', + ' Block* blocks_;', + '};', + 'int FileState::Size() const { return blocks_->size(); }', + '} // namespace leveldb', + ), + }); + try { + const sizes = cg.getNodesByKind('method').filter((n) => n.qualifiedName === 'leveldb::FileState::Size'); + const byFile = Object.fromEntries(sizes.map((n) => [n.filePath, cg.getCallees(n.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName)])); + expect(byFile).toEqual({ + 'db/fault_injection_test.cc': ['leveldb::Block::size'], + 'helpers/memenv/memenv.cc': ['leveldb::Slice::size'], + }); + } finally { + cg.close(); + } + }); + + it('not a base that depends on the template’s parameters, where a bare name is not looked up', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'util/heap_test.cc': lines( + '#include "db/db_impl/db_impl.h"', + 'namespace rocksdb {', + 'template ', + 'class Holder {', + ' protected:', + ' InstrumentedMutex mutex_;', + '};', + 'port::Mutex mutex_;', + 'template ', + 'class Guard : public Holder {', + ' public:', + ' void Enter() { mutex_.Lock(); }', + '};', + '} // namespace rocksdb', + ), + }); + try { + // C++ finds the namespace's `mutex_` here: `this->mutex_` would name the base's. + expect(calls(cg, 'rocksdb::Guard::Enter')).not.toContain('rocksdb::InstrumentedMutex::Lock @0.9'); + } finally { + cg.close(); + } + }); +}); + +describe('what keeps the call as it was', () => { + it('a local of the name that the scan cannot read', async () => { + const cg = await indexed({ + ...ROCKSDB, + 'db/db_impl/db_impl_compaction_flush.cc': lines( + '#include "db/db_impl/db_impl.h"', + 'namespace rocksdb {', + 'void DBImpl::FlushMemTable() {', + ' auto mutex_ = GetFlushMutex();', + ' mutex_->Lock();', + '}', + '} // namespace rocksdb', + ), + }); + try { + // `mutex_` here is the local, not the member. + expect(calls(cg, 'rocksdb::DBImpl::FlushMemTable').filter((c) => c.includes('Lock'))).not.toContain('rocksdb::InstrumentedMutex::Lock @0.9'); + } finally { + cg.close(); + } + }); + + it('a parameter of a method the class declares, which is no member', async () => { + // rocksdb's DB declares `CreateColumnFamilies(const std::vector<…>& + // column_families, …)`; DBImpl's `const ColumnFamilySet* const + // column_families` is a local the scan cannot read. + const cg = await indexed({ + ...ROCKSDB, + 'db/db_impl/db_impl.h': ROCKSDB['db/db_impl/db_impl.h'].replace( + ' void FlushMemTable();', + ' void FlushMemTable();\n void CreateColumnFamilies(std::vector* column_families,\n int* handles);\n int CountColumnFamilies();\n ColumnFamilySet* stats() const;', + ), + 'db/db_impl/db_impl_write.cc': lines( + '#include "db/db_impl/db_impl.h"', + 'namespace rocksdb {', + 'int DBImpl::CountColumnFamilies() {', + ' const ColumnFamilySet* const column_families = versions_->GetColumnFamilySet();', + ' const Statistics* const stats = nullptr;', + ' if (stats->load()) return 0;', + ' return column_families->NumberOfColumnFamilies();', + '}', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('NumberOfColumnFamilies'))).toEqual([ + 'rocksdb::ColumnFamilySet::NumberOfColumnFamilies @0.7', + ]); + // Nor is a method the class declares (`stats()`) a member of its name. + expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('load'))).toEqual(['rocksdb::Statistics::load @0.7']); + } finally { + cg.close(); + } + }); + + it("a member of a template parameter's type", async () => { + // rocksdb's `ComparatorWithU64TsImpl` holds a `TComparator cmp_without_ts_`. + const cg = await indexed({ + 'util/comparator.cc': lines( + 'namespace rocksdb {', + 'class Comparator {', + ' public:', + ' int timestamp_size() const { return 0; }', + '};', + 'template ', + 'class ComparatorWithU64TsImpl {', + ' public:', + ' int Check() const { return cmp_without_ts_.timestamp_size(); }', + ' private:', + ' TComparator cmp_without_ts_;', + '};', + '// rocksdb\'s clock cache test reaches its shard through a dependent alias.', + 'class ClockCacheShard {', + ' public:', + ' void Insert(int key) {}', + '};', + 'template ', + 'class ClockCacheTest {', + ' public:', + ' using Shard = typename ClockCache::Shard;', + ' void Insert(int key) { shard_->Insert(key); }', + ' private:', + ' Shard* shard_ = nullptr;', + '};', + '} // namespace rocksdb', + ), + }); + try { + expect(calls(cg, 'rocksdb::ComparatorWithU64TsImpl::Check')).toEqual(['rocksdb::Comparator::timestamp_size @0.7']); + expect(calls(cg, 'rocksdb::ClockCacheTest::Insert')).toEqual(['rocksdb::ClockCacheShard::Insert @0.65']); + } finally { + cg.close(); + } + }); + + it('a member a base class template declares through an alias of its parameter', async () => { + // protobuf's MapIteratorBase holds a `MapFieldBase* map_` through + // `using MapFieldBase = std::conditional_t`. + const cg = await indexed({ + 'src/google/protobuf/map_field.h': lines( + '#include ', + 'namespace google {', + 'namespace protobuf {', + 'namespace internal {', + 'class MapFieldBase {', + ' public:', + ' void SetMapDirty() {}', + '};', + '} // namespace internal', + 'template ', + 'class MapIteratorBase {', + ' protected:', + ' using MapFieldBase = std::conditional_t;', + ' MapFieldBase* map_;', + '};', + 'class MapIterator : public MapIteratorBase {', + ' public:', + ' void MutableValueRef() { map_->SetMapDirty(); }', + '};', + '} // namespace protobuf', + '} // namespace google', + ), + 'src/google/protobuf/map_field.cc': lines('#include "src/google/protobuf/map_field.h"'), + }); + try { + expect(calls(cg, 'google::protobuf::MapIterator::MutableValueRef')).toEqual(['google::protobuf::internal::MapFieldBase::SetMapDirty @0.9']); + } finally { + cg.close(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 70c16bd830..a6bd02fb1d 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -9129,28 +9129,188 @@ function cppWrittenClass(type: CppWrittenType, at: UnresolvedRef, context: Resol * (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 member = cppDataMemberIn(decl, name, context); + if (member) return member; + } + return null; +} + +const CPP_DATA_MEMBERS = new WeakMap>(); +const CPP_NESTED_SPANS = new WeakMap>>(); +const CPP_CLASS_PAREN_DEPTHS = new WeakMap>(); + +/** + * cppDataMemberType for one declaration of the class, `decl`. A declarator + * inside parentheses is a parameter of a method the class declares — which + * has no node of its own, so its lines are the class's — not a member: + * rocksdb's `DB` declares `CreateColumnFamilies(const + * std::vector& column_families, …)`. Nor is a name + * an argument list follows, a method. + */ +function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext): CppWrittenType | null { + if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) return null; + let memo = CPP_DATA_MEMBERS.get(context); + if (!memo) CPP_DATA_MEMBERS.set(context, (memo = new Map())); + const key = `${decl.id}\u0000${name}`; + const hit = memo.get(key); + if (hit !== undefined) return hit; + let member: CppWrittenType | null = null; + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/); + if (lines) { + const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const word = new RegExp(`\\b${escaped}\\b`); + const declarator = buildDeclaratorRegex(escaped); 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))); + const nested = cppNestedSpans(decl, end, context); + const rawLines = cppRawStringLines(decl.filePath, context); + const depths = cppClassParenDepths(decl, lines, end, rawLines, context); 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 (!word.test(lines[l - 1]!) || nested.some(([start, stop]) => l >= start && l <= stop)) continue; + const code = cppCodeOf(cppLiteralsBlanked(lines[l - 1]!, l, rawLines)); + const m = code ? declarator.exec(code) : null; + if (!m || code!.slice(m.index + m[0].length).startsWith('(')) continue; + let depth = depths[l - decl.startLine]!; + if (depth < 0) continue; + for (const c of code!.slice(l === decl.startLine ? decl.startColumn : 0, m.index)) { + if (c === '(') depth++; + else if (c === ')') depth--; + } + const type = depth <= 0 ? normalizeCppTypeName(m[1]!) : null; if (type && type !== 'auto') { - return { text: declared!, scope: cls, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) }; + member = { text: m[1]!, scope: decl.qualifiedName, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) }; + break; } } } + memo.set(key, member); + return member; +} + +/** + * How many parentheses are open where each line of the C++ class + * declaration `decl`, which ends on line `end`, begins, or -1 for one that + * begins inside a block comment. Comments are followed across lines, not + * guessed as cppCodeOf does: it takes a code line that starts with a + * dereference, `*first_key, scratch);`, for one. + */ +function cppClassParenDepths( + decl: Node, + lines: readonly string[], + end: number, + rawLines: ReturnType, + context: ResolutionContext, +): number[] { + let memo = CPP_CLASS_PAREN_DEPTHS.get(context); + if (!memo) CPP_CLASS_PAREN_DEPTHS.set(context, (memo = new Map())); + let depths = memo.get(decl.id); + if (!depths) { + depths = []; + let depth = 0; + let comment = false; + for (let l = decl.startLine; l <= end; l++) { + depths.push(comment ? -1 : depth); + const text = cppLiteralsBlanked(lines[l - 1] ?? '', l, rawLines); + for (let i = l === decl.startLine ? decl.startColumn : 0; i < text.length; i++) { + const c = text[i]!; + if (comment) { + if (c === '*' && text[i + 1] === '/') { + comment = false; + i++; + } + } else if (c === '/' && text[i + 1] === '/') { + break; + } else if (c === '/' && text[i + 1] === '*') { + comment = true; + i++; + } else if (c === '(') { + depth++; + } else if (c === ')' && depth > 0) { + depth--; + } + } + } + memo.set(decl.id, depths); + } + return depths; +} + +/** The lines of the functions and classes nested in the C++ class declaration `decl`, which ends on line `end`. */ +function cppNestedSpans(decl: Node, end: number, context: ResolutionContext): Array<[number, number]> { + let memo = CPP_NESTED_SPANS.get(context); + if (!memo) CPP_NESTED_SPANS.set(context, (memo = new Map())); + let spans = memo.get(decl.id); + if (!spans) { + spans = 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))) + .map((n): [number, number] => [n.startLine, n.endLine ?? n.startLine]); + memo.set(decl.id, spans); + } + return spans; +} + +const CPP_CALLERS_MEMBERS = new WeakMap>(); + +/** + * The type a data member `name` of the C++ class the calling function + * `caller` belongs to has, as written in the class: its own, else one of a + * class it derives from, through its base edges (not a base that depends on + * the class's template parameters, where a bare name isn't looked up). The + * class is read wherever it is declared: rocksdb's `DBImpl` is declared in + * db_impl.h, while its methods are defined in db_impl_write.cc, + * db_impl_compaction_flush.cc, db_impl_open.cc and more. A gtest + * `TEST_F(Fixture, Name)` or `TEST_P` body is a method of a class derived + * from `Fixture`. Null when none of them declares it. + */ +function cppCallersMember(name: string, caller: Node, ref: UnresolvedRef, context: ResolutionContext): CppWrittenType | null { + let memo = CPP_CALLERS_MEMBERS.get(context); + if (!memo) CPP_CALLERS_MEMBERS.set(context, (memo = new Map())); + const key = `${caller.id}\u0000${name}`; + const hit = memo.get(key); + if (hit !== undefined) return hit; + const owner = cppMemberOwner(caller, context) ?? cppTestFixture(caller, ref, context); + const decls = owner ? cppScopeDeclarations(owner, cppClassDecls(owner, context), caller, context).decls : []; + const member = owner ? cppInheritedDataMember(decls, name, context, 0, new Set([owner])) : null; + memo.set(key, member); + return member; +} + +/** The data member `name` of a class, in one of its declarations `decls` or else in a class it derives from. */ +function cppInheritedDataMember(decls: readonly Node[], name: string, context: ResolutionContext, depth: number, seen: Set): CppWrittenType | null { + for (const decl of decls) { + const own = cppDataMemberIn(decl, name, context); + if (own) return own; + } + if (depth >= 4 || !context.getSupertypeNodes) return null; + for (const decl of decls) { + for (const base of context.getSupertypeNodes(decl.id)) { + if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName) || isCppDependentBase(decl, base, context)) continue; + seen.add(base.qualifiedName); + const inherited = cppInheritedDataMember([base], name, context, depth + 1, seen); + if (inherited) return inherited; + } + } return null; } +/** + * The fixture class of a gtest `TEST_F(Fixture, Name)` or `TEST_P(Fixture, + * Name)` body, which the index holds as a function named `TEST_F`: the body + * is a method of a class gtest derives from `Fixture`. Null for any other + * function, and for a fixture C++ lookup doesn't find. + */ +function cppTestFixture(caller: Node, ref: UnresolvedRef, context: ResolutionContext): string | null { + if (caller.kind !== 'function' || (caller.name !== 'TEST_F' && caller.name !== 'TEST_P')) return null; + const lines = context.getFileLines?.(caller.filePath) ?? context.readFile(caller.filePath)?.split(/\r?\n/); + const head = lines?.slice(caller.startLine - 1, caller.startLine + 1).join(' ') ?? ''; + const fixture = /\bTEST_[FP]\s*\(\s*((?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*)\s*,/.exec(head)?.[1]; + if (!fixture) return null; + const site = { scope: cppParentScope(caller.qualifiedName), file: caller.filePath, line: caller.startLine, parameters: new Set() }; + const found = cppClassWritten(fixture.replace(/\s+/g, ''), site, ref, context); + return found?.exact && CPP_CLASS_KINDS.has(found.node.kind) ? found.node.qualifiedName : 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. @@ -9253,10 +9413,8 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC 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); + // (A data member of the caller's class has its type as `declared`.) + return decl.raw !== undefined && decl.callers && !decl.shadowed ? { ...callers, text: decl.raw } : null; } // `new Foo(…)`: a pointer to what it constructs. const created = /^new\s+((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}()]*>)?)\s*(?:[({[][\s\S]*)?$/.exec(text); @@ -9385,7 +9543,9 @@ function isCppTemplateDependentType(type: CppWrittenType, context: ResolutionCon if (isCppTemplateParameterType(type)) return true; const held = cppStdHeldType(type.text); if (held) return isCppTemplateDependentType({ ...type, text: held }, context); - return isCppLibraryType(type.text, context) && (type.text.match(/[A-Za-z_]\w*/g) ?? []).some((word) => type.parameters.has(word)); + // (A trait of one, `std::conditional_t`, too: it names another type.) + return (isCppLibraryType(type.text, context) || cppTypeSegments(type.text)?.[0] === 'std') && + (type.text.match(/[A-Za-z_]\w*/g) ?? []).some((word) => type.parameters.has(word)); } /** Where the C or C++ string or character literal opening at `text[start]` closes (the line's end if it doesn't). */ @@ -9487,10 +9647,11 @@ interface CppReceiverDeclaration { */ shadowed?: boolean; /** - * The receiver's type as written where it is declared, when its own - * declaration says `auto` (`raw` is its text): the element type of the - * range an `auto` range-for iterates, or the type of an `auto` local's - * initializer — the `Iterator*` that `txn->GetIterator(…)` returns. + * The receiver's type as written where it is declared, when that is not + * in the calling function's own text or says `auto` there (`raw` is its + * text): the element type of the range an `auto` range-for iterates, the + * type of an `auto` local's initializer — the `Iterator*` that + * `txn->GetIterator(…)` returns — or a data member's, in its class. */ declared?: CppWrittenType; /** @@ -9744,8 +9905,9 @@ function matchCppDeclaredTypeCall( // 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` variable's type is looked up where it is written: where its - // range's type or its initializer's function is declared. + // A type the calling function doesn't write is looked up where it is + // written: where an `auto` variable's range or initializer gets it, or in + // a data member's class. const site = decl.declared ?? cppCallSite(caller, ref, context); const found = cppClassWritten(aliasedClass ? `::${aliasedClass}` : decl.raw, site, ref, context); if (found === undefined && !aliasedClass && isCppOutsideQualifiedType(decl.raw, site, context)) return null; @@ -10267,8 +10429,18 @@ function inferCppReceiverType( // writes `upb_MiniTableField field = $field_init$;` in the raw string it // emits, above calls on its own `upb::FieldDefPtr field` parameter. const rawLines = cppRawStringLines(ref.filePath, context); + // Past the calling function's first line, C++ looks the name up among the + // members of the function's class before anything outside it. + const caller = context.getNodeById?.(ref.fromNodeId); + const memberCaller = caller && caller.filePath === ref.filePath && (caller.kind === 'method' || caller.kind === 'function') ? caller : null; + let memberLooked = memberCaller === null; for (let i = callLineIndex; i >= 0; i--) { + if (!memberLooked && memberCaller && i < memberCaller.startLine - 1) { + memberLooked = true; + const member = cppMemberReceiverType(receiverName, memberCaller, ref, context, found); + if (member !== undefined) return member; + } const text = lines[i]; if (!text || !receiverPattern.test(text)) continue; const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, rawLines)); @@ -10333,6 +10505,11 @@ function inferCppReceiverType( if (cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true; } } + // (A function that starts on the file's first line.) + if (!memberLooked && memberCaller) { + const member = cppMemberReceiverType(receiverName, memberCaller, ref, context, found); + if (member !== undefined) return member; + } const headerCandidates = [ ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.h'), @@ -10367,6 +10544,35 @@ function inferCppReceiverType( return null; } +/** + * The type of a C++ receiver the calling function doesn't declare, as a data + * member of the function's class (cppCallersMember), recorded in `found` as + * a declaration of the caller's, where the class writes it. Undefined when + * it is no member, or the scan passed a declaration of the name it couldn't + * read (`auto mutex_ = Make();`), which is what the name means there. + */ +function cppMemberReceiverType( + receiverName: string, + caller: Node, + ref: UnresolvedRef, + context: ResolutionContext, + found: CppReceiverDeclaration, +): string | null | undefined { + if (found.shadowed) return undefined; + const member = cppCallersMember(receiverName, caller, ref, context); + const normalized = member && normalizeCppTypeName(member.text); + if (!member || !normalized) return undefined; + // A member of a template parameter's type (`TComparator cmp_;`), or of an + // alias of one (`using Shard = typename Cache::Shard;`, protobuf's + // `using MapFieldBase = std::conditional_t;`), names no + // particular class: the call goes on as before. + const followed = cppFollowWrittenAliases(member, ref, context); + if (isCppTemplateDependentType(followed ?? member, context) || resolveCppAliasedType(member.text, ref, context) === null) return undefined; + noteCppDeclaration(found, member.text, true); + found.declared = member; + return cppDeclaredType(member.text, normalized, true, ref, context, found); +} + /** * Last `::`-separated segment of a (possibly namespace-qualified) C++ name. */ @@ -10968,6 +11174,10 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { CPP_LOOP_ELEMENTS.delete(context); CPP_AUTO_LOCALS.delete(context); CPP_SCOPE_NAMES.delete(context); + CPP_DATA_MEMBERS.delete(context); + CPP_NESTED_SPANS.delete(context); + CPP_CLASS_PAREN_DEPTHS.delete(context); + CPP_CALLERS_MEMBERS.delete(context); SOLIDITY_SUPERS.delete(context); DECLARED_SUPERS.delete(context); INHERITED_METHODS.delete(context); From 03ae96b7ee308e5aec6f3182ef87ad31abe5c202 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Mon, 12 Oct 2026 20:27:20 -0600 Subject: [PATCH 3/3] fix(kernel): read a chain's operator and a statement initializer's variables for C++ receivers A chained call spelled with '.' on a returned smart pointer or optional is the type's own member; only '->' reaches what it holds. A variable declared in a for, if or switch initializer shadows an inherited member of the same name. The call-chain length the docs claim is now stated as four calls. --- CHANGELOG.md | 2 +- README.md | 2 +- __tests__/cpp-auto-local-receiver.test.ts | 58 +++++++++++++++++++ codegraph-kernel/src/resolve/cpp_range_for.rs | 30 +++++++--- codegraph-kernel/src/resolve/cpp_receivers.rs | 8 ++- 5 files changed, 89 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67d5ffa03d..d67127da8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -472,7 +472,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In this fork, the two C++ changes above run in the native resolver. A call on a variable whose type is a template parameter, like `T* iter`, stays unlinked rather than reaching a class that happens to share the parameter's name. A call stays linked as before when the class has no method of that name but one of its base classes comes from an outside library other than the standard library, such as googletest's `testing::Test`. - In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr(…)` or `auto* mutex = static_cast(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. Re-index C++ projects after upgrading. - In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading. -- In this fork, the two C++ changes above also run in the native resolver. A call on a variable, member or chain whose type it cannot read, such as one a template parameter decides (`T*`, `std::shared_ptr`), an alias of a trait of a parameter, or the result of a function template whose return type is its parameter, stays unlinked rather than being guessed by the receiver's name, and a chain of any number of calls (`cfd->current()->storage_info()->LevelFiles()`) is read from its declarations, not only its first inner call. +- In this fork, the two C++ changes above also run in the native resolver. A call on a variable, member or chain whose type it cannot read, such as one a template parameter decides (`T*`, `std::shared_ptr`), an alias of a trait of a parameter, or the result of a function template whose return type is its parameter, stays unlinked rather than being guessed by the receiver's name, and a chain of up to four calls (`cfd->current()->storage_info()->LevelFiles()`) is read from its declarations, not only its first inner call; a longer one is looked up by the last name, as before. A chained call spelled with `.` on a returned `std::unique_ptr` or `std::optional` is that type's own member, not a method of what it holds (`->` still reaches that); and a variable declared in the initializer of a `for`, `if` or `switch` statement is seen before a same-named member the class inherits. - 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 Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. diff --git a/README.md b/README.md index 4a32e50ec2..3c22ed4ed5 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ Call resolution the fork adds: - Kotlin multi-dollar strings keep their interpolation threshold. - C++ typedef and using aliases follow their declaration scope, including nested owners and inherited aliases. Pointer, array and function members are not supertypes. Visible class-scoped aliases and declared complex receivers keep their method owners; unsubstituted template parameters and ambiguous owners stay unresolved. - C++ `->` calls on a `std::unique_ptr`, `std::shared_ptr` or `std::optional` reach the type it holds, looked up through the caller's enclosing classes and their bases, its namespaces, `using` directives and aliases. A held type the project does not declare leaves the call unresolved, as a plain pointer to one does; upstream guesses a callee from the receiver's name there. A partial class template specialization is indexed as an anonymous class, so a call to a member only it declares stays unresolved. -- C++ calls on an `auto` local, on a chained call and on a data member of the caller's class (one a base class declares, or a gtest `TEST_F` / `TEST_P` fixture's) take the type its declaration writes, as upstream's do. Where that type cannot be read (a template parameter's, a `std::shared_ptr` of one, a trait of one, what a function template returns), the call stays unlinked, as does a local of a type the file does not declare; upstream guesses a callee from the receiver's name at 0.7. A chain of any number of calls is read from its declarations, where upstream reads its first inner call. +- C++ calls on an `auto` local, on a chained call and on a data member of the caller's class (one a base class declares, or a gtest `TEST_F` / `TEST_P` fixture's) take the type its declaration writes, as upstream's do. Where that type cannot be read (a template parameter's, a `std::shared_ptr` of one, a trait of one, what a function template returns), the call stays unlinked, as does a local of a type the file does not declare; upstream guesses a callee from the receiver's name at 0.7. A chain of up to four calls is read from its declarations, where upstream reads its first inner call; a longer one is looked up by the last name, as before. - C++ namespace aliases and declarations in macro-opened namespaces are looked up only in the caller's include closure; upstream pools them across all files. - C# namespace `using` directives and `using` aliases apply only inside their enclosing namespace, not to sibling namespaces in the same file (`using static` is still file-wide, as upstream). - A Java field declared with a qualified type (`outside.Repository`) keeps its qualifier, so calls through it never resolve to an unrelated project class with the same simple name. diff --git a/__tests__/cpp-auto-local-receiver.test.ts b/__tests__/cpp-auto-local-receiver.test.ts index 9d58a41275..19ab514fe9 100644 --- a/__tests__/cpp-auto-local-receiver.test.ts +++ b/__tests__/cpp-auto-local-receiver.test.ts @@ -605,6 +605,64 @@ describe('what keeps the call as it was', () => { }); }); +describe('a chained call keeps the operator it was written with, and a loop variable its own type', () => { + it('`.` on a returned std::optional is the optional\'s member; `->` reaches what it holds', async () => { + const cg = await indexed({ + 'src/widget.cc': lines( + '#include ', + 'class Widget {', + ' public:', + ' int value() { return 1; }', + '};', + 'std::optional MakeWidget() { return std::nullopt; }', + 'void Dot() { MakeWidget().value(); }', + 'void Arrow() { MakeWidget()->value(); }', + ), + }); + try { + expect(calls(cg, 'Dot').filter((c) => c.includes('value'))).toEqual([]); + expect(calls(cg, 'Arrow').filter((c) => c.includes('value'))).toEqual(['Widget::value @0.85']); + } finally { + cg.close(); + } + }); + + it('an `auto` or declared variable of a `for` initializer shadows a member of a base class', async () => { + const cg = await indexed({ + 'src/loop.cc': lines( + 'class Widget {', + ' public:', + ' void spin() {}', + '};', + 'class Gadget {', + ' public:', + ' void spin() {}', + '};', + 'Gadget* MakeGadget() { return nullptr; }', + 'class Base {', + ' protected:', + ' Widget* cursor;', + '};', + 'class Derived : public Base {', + ' public:', + ' void Run() {', + ' for (auto cursor = MakeGadget(); cursor != nullptr; cursor = nullptr) cursor->spin();', + ' }', + ' void RunDeclared() {', + ' for (Gadget* cursor = MakeGadget(); cursor != nullptr; cursor = nullptr) cursor->spin();', + ' }', + '};', + ), + }); + try { + expect(calls(cg, 'Derived::Run').filter((c) => c.includes('spin'))).toEqual(['Gadget::spin @0.9']); + expect(calls(cg, 'Derived::RunDeclared').filter((c) => c.includes('spin'))).toEqual(['Gadget::spin @0.9']); + } finally { + cg.close(); + } + }); +}); + describe('a type from outside the project reaches none of its classes', () => { it('one under a namespace the project declares nothing in', async () => { const cg = await indexed({ diff --git a/codegraph-kernel/src/resolve/cpp_range_for.rs b/codegraph-kernel/src/resolve/cpp_range_for.rs index fa2fd11ac7..513c6e53c3 100644 --- a/codegraph-kernel/src/resolve/cpp_range_for.rs +++ b/codegraph-kernel/src/resolve/cpp_range_for.rs @@ -130,8 +130,12 @@ fn without_spaces(text: &str) -> String { text.chars().filter(|c| !c.is_whitespa /// GetBlobFiles()` in `const auto& blob_files = vstorage->GetBlobFiles();`. The same declarations /// `cpp_binding_type` reads. pub(super) fn binding_initializer<'t>(parent: TsNode<'t>, at: TsNode<'t>, name: &str, source: &SourceFile) -> Option> { - if !matches!(parent.kind(), "compound_statement" | "declaration_list" | "translation_unit") { return None; } - for declaration in named_children(parent).into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { + let children = match parent.kind() { + "compound_statement" | "declaration_list" | "translation_unit" => named_children(parent), + _ => statement_initializer(parent).into_iter().collect(), + }; + if children.is_empty() { return None; } + for declaration in children.into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { let mut cursor = declaration.walk(); let declarators: Vec<_> = declaration.children_by_field_name("declarator", &mut cursor).collect(); if let Some(declarator) = declarators.into_iter().find(|d| cpp_declarator_name(*d, source) == name) { @@ -142,6 +146,16 @@ pub(super) fn binding_initializer<'t>(parent: TsNode<'t>, at: TsNode<'t>, name: None } +/// The declaration a `for`, `if`, `switch` or `while` statement (or its condition) opens with, +/// which names variables for the statement as a block's declarations do for the block. +pub(super) fn statement_initializer(node: TsNode<'_>) -> Option> { + match node.kind() { + "for_statement" | "condition_clause" => node.child_by_field_name("initializer"), + "if_statement" | "switch_statement" | "while_statement" => node.child_by_field_name("condition").and_then(|c| c.child_by_field_name("initializer")), + _ => None, + }.filter(|n| n.kind() == "declaration") +} + /// Whether the written type `text` is a template parameter of `parameters`, or a library template /// of one (`std::shared_ptr`, `std::vector`). fn type_depends_on(text: &str, parameters: &HashSet, depth: u32) -> bool { @@ -437,7 +451,7 @@ impl KernelResolver { // The call expression the reference names: the one calling `method` on a call spelled `inner()`. let wanted: String = format!("{}()", chain.0).split_whitespace().collect(); let mut at = descendant_for_position(tree.root_node(), source.text(), ((r.line - 1).max(0) as usize, r.column.max(0) as usize)); - let receiver = loop { + let (receiver, arrow) = loop { if at.kind() == "call_expression" { let function = at.child_by_field_name("function").filter(|f| f.kind() == "field_expression"); let field = function.and_then(|f| f.child_by_field_name("field")); @@ -448,7 +462,8 @@ impl KernelResolver { receiver = match named_children(inner).as_slice() { [only] => Some(*only), _ => None }; } if let Some(receiver) = receiver.filter(|n| n.kind() == "call_expression" && chain_spelling(&source.text()[n.byte_range()]) == wanted) { - break receiver; + let arrow = function.child_by_field_name("operator").is_some_and(|op| &source.text()[op.byte_range()] == "->"); + break (receiver, arrow); } } } @@ -458,9 +473,10 @@ impl KernelResolver { let Some(written) = self.cpp_expression_written(receiver, &source, r, 1)? else { return Ok(None) }; let Some(followed) = self.cpp_follow_written_aliases(written, r)? else { return Ok(None) }; if self.cpp_written_template_dependent(&followed)? { return Ok(None); } - // (`->` on a pointer or a smart pointer reaches what it points at; the chain is spelled with - // `.` for either.) - let object = match cpp_pointee(&followed.text) { Some(text) => CppWritten { text, site: followed.site }, None => followed }; + // `->` on a pointer or a smart pointer reaches what it points at; `.` on a smart pointer or + // an optional is its own member (`optional.value()`), no method of what it holds. The chain + // is spelled with `.` for either, so the operator is the AST's. + let object = match cpp_pointee(&followed.text).filter(|_| arrow) { Some(text) => CppWritten { text, site: followed.site }, None => followed }; let plain = cpp_plain_type(&object.text); if let Some(parts) = type_segments(&plain) { let last = parts.last().map(String::as_str).unwrap_or(""); diff --git a/codegraph-kernel/src/resolve/cpp_receivers.rs b/codegraph-kernel/src/resolve/cpp_receivers.rs index 45f2457ef1..fc6fe5f583 100644 --- a/codegraph-kernel/src/resolve/cpp_receivers.rs +++ b/codegraph-kernel/src/resolve/cpp_receivers.rs @@ -406,8 +406,12 @@ pub(super) fn cpp_declarator_name<'a>(mut node: tree_sitter::Node, source: &'a S } pub(super) fn cpp_binding_type(parent: tree_sitter::Node, at: tree_sitter::Node, name: &str, source: &SourceFile) -> Option { - if matches!(parent.kind(), "compound_statement" | "declaration_list" | "translation_unit") { - for declaration in named_children(parent).into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { + // A `for` or `if` statement's initializer declares a name for the statement, as a block's declaration does. + let block = matches!(parent.kind(), "compound_statement" | "declaration_list" | "translation_unit"); + let initializer = super::cpp_range_for::statement_initializer(parent); + if block || initializer.is_some() { + let children = if block { named_children(parent) } else { initializer.into_iter().collect() }; + for declaration in children.into_iter().rev().filter(|n| n.kind() == "declaration" && n.end_byte() <= at.start_byte()) { // C++ parses a parenthesized initializer as a function declarator. // Recognize only a standard smart pointer initialized from a known pointer value. if let Some(declarator) = declaration.child_by_field_name("declarator").filter(|n| n.kind() == "function_declarator") {