mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-28 22:52:00 +08:00
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:
co-authored by
Claude Opus 5.5
parent
c19f0284f8
commit
79cd737bec
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user