fix(cpp): a call on an auto variable or a chained call reaches the class C++ finds, not a namesake (#2532)

#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<Iterator>(…)`, `static_cast<InstrumentedMutex*>(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<T>`) 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 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-13 00:42:38 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c19f0284f8
commit 79cd737bec
5 changed files with 988 additions and 48 deletions
+1
View File
@@ -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<MessageGenerator>` reached other generators' methods. A call on an element of a standard-library type, like `file.find(…)` over a `std::vector<std::string>`, 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<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(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.
+710
View File
@@ -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<Iterator>(…)` 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<string, string>): Promise<CodeGraph> {
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 <memory>',
'#include "include/rocksdb/db.h"',
'#include "include/rocksdb/memtablerep.h"',
'namespace rocksdb {',
'int CountKeys(DB* db) {',
' int n = 0;',
' auto iter = std::unique_ptr<Iterator>(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<Mode> holds an ExternalTableReaderBase<Mode>.
const cg = await indexed({
'table/external_table.cc': lines(
'#include <memory>',
'namespace rocksdb {',
'template <int Mode>',
'class ExternalTableReaderBase {',
' public:',
' virtual ~ExternalTableReaderBase() {}',
' int NewIterator() { return Mode; }',
'};',
'class IndexFactoryReader {',
' public:',
' int NewIterator() { return 0; }',
'};',
'template <int Mode>',
'class ExternalTableReaderAdapter {',
' public:',
' int Open() {',
' auto reader = std::unique_ptr<ExternalTableReaderBase<Mode>>(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 <typename Key, class Comparator>',
'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<InstrumentedMutex*>(arg);',
' mutex->Unlock();',
' mutex->Lock();',
'}',
'class HashSkipListRep {',
' public:',
' typedef SkipList<const char*, int> 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 <memory>',
'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<Widget>();',
' 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 <memory>',
'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<PinnedFactory>();',
' factory->last_reader()->SetPinnedData(1);',
'}',
'void PinAgain(const std::unique_ptr<PinnedFactory>& 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 <memory>',
'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<VersionSet> 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 <memory>',
'namespace rocksdb {',
'class Customizable {',
' public:',
' template <typename T>',
' T* CheckedCast() { return nullptr; }',
'};',
'class MergeOperator : public Customizable {',
' public:',
' virtual const char* Name() const { return ""; }',
'};',
'struct ColumnFamilyOptions {',
' std::shared_ptr<MergeOperator> 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<test::ChanglingMergeOperator>();',
' merge_operator->SetName("other");',
'}',
'} // namespace rocksdb',
),
});
try {
// Not the shared_ptr<MergeOperator> 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<T>` 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 <typename T, typename... Args>',
' PROTOBUF_FUTURE_ADD_EARLY_NODISCARD PROTOBUF_NDEBUG_INLINE static T*',
' PROTOBUF_NONNULL',
' Create(Arena* arena, Args&&... args) {',
' return new T();',
' }',
'};',
'template <typename Element>',
'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 <typename T>',
'PROTOBUF_FUTURE_ADD_EARLY_NODISCARD std::shared_ptr<T> DownCastMessage(const std::shared_ptr<MessageLite>& from) {',
' return nullptr;',
'}',
'template <typename T>',
'PROTOBUF_FUTURE_ADD_EARLY_NODISCARD const T& DownCastMessage(const MessageLite& from) {',
' return *static_cast<const T*>(&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<RepeatedPtrField<int>>(arena);',
' field->Add();',
'}',
'int ByteSizeLong(const MessageLite& msg) {',
' auto& _this = DownCastMessage<Message>(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<T>`, 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 <typename T>',
'T SerializeAs(const int& msg);',
'template <>',
'absl::Cord SerializeAs<absl::Cord>(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 <ostream>',
'#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();
}
});
});
+5
View File
@@ -76,6 +76,11 @@ function cppNamespaceMacros(context: ResolutionContext): { openers: Map<string,
return macros;
}
/** The outermost namespaces the project's namespace-opening macros open: `fmt` for `FMT_BEGIN_NAMESPACE`. */
export function cppMacroNamespaceHeads(context: ResolutionContext): string[] {
return [...cppNamespaceMacros(context).openers.values()].map((path) => 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);
+4 -1
View File
@@ -349,7 +349,10 @@ export function clearCppTypeAliasMemos(context: ResolutionContext): void {
function collectTemplateHeader(lines: readonly string[], startLine: number, startColumn: number, names: Set<string>): 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 <typename T, typename...
// Args>` / `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 <typename> class Policy`, `typename... Ts`, `int N`.
+268 -47
View File
@@ -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<Foo>(…)`) or a cast
* (`static_cast<Foo*>(…)`, `std::unique_ptr<Foo>(…)`) 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<Foo*>(…)`: 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<Foo>(…)`: 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<Iterator>(…)`: 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<int>(…)`: 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<ResolutionContext, Map<string, CppWrittenType | null>>();
@@ -9287,7 +9309,8 @@ const CPP_LOOP_ELEMENTS = new WeakMap<ResolutionContext, Map<string, CppWrittenT
* variable is. `for (auto cfd : cfds)` over a parameter `const
* autovector<ColumnFamilyData*>& 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<ResolutionContext, Map<string, CppWrittenType | null>>();
/**
* 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<Iterator>(…)` is a `std::unique_ptr<Iterator>`, 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<T>` returns a `const T&`, a `T*` or a
// `std::shared_ptr<T>`, 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<T>`,
* `std::vector<T>`), whose members are the parameter's? A class template of
* the project's is that class whatever its arguments: `ProtectionInfoKVOS<T>`,
* or the one a smart pointer holds (`std::unique_ptr<ExternalTableReaderBase<Mode>>`).
*/
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<C>::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<C>::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<kFlag,
* void*, Slice>`, `std::vector<Slice>::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<ResolutionContext, Set<string>>();
/**
* 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<string> {
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<Iterator>(…)`,
// 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<Foo>();` 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<typeof cppRawStringLines>,
): 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<W>();`,
@@ -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<Iterator> iter; iter->Valid()` reached