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