fix(cpp): a call on a data member of the caller's class reaches its type's method, wherever the class is declared (#2533)

Receiver inference read back through the call's file and the header of the
same name only, so a member of a class declared elsewhere — rocksdb's
DBImpl is declared in db_impl.h, its methods in db_impl_write.cc,
db_impl_compaction_flush.cc, db_impl_open.cc and more — was never read, and
calls on it (`versions_->GetColumnFamilySet()`, `mutex_.Lock()`) were
guessed by the receiver's name.

- Where the scan leaves the calling function, the name is looked up as a
  data member of the function's class before anything outside it
  (cppCallersMember): the class's own declaration lines wherever it is
  declared (the declaration in the caller's own translation unit, of
  classes sharing a name), then the classes it derives from through their
  base edges, skipping a base that depends on the template's parameters.
  A gtest `TEST_F` / `TEST_P` body is a method of a class derived from its
  fixture, so `db_` there is the fixture's.
- The member goes into the declaration record as the caller's own, with
  its class as the lookup site, so #2493's lookup, the library-type gate and
  #2440's held-type path apply. A member of a template parameter's type,
  or of an alias of one, and a name the scan saw an unreadable declaration
  of, stay as before. (isCppTemplateDependentType now also counts a std
  trait of a parameter: protobuf's `using MapFieldBase =
  std::conditional_t<kIsMutable, …>`.)
- cppDataMemberIn reads only declarations outside parentheses and not
  followed by an argument list (method declarations have no nodes, so their
  parameters are in the class's lines), skips nested functions and classes,
  follows block comments across lines, and blanks string literals.
- A member declared below an inline method that uses it, and a member of
  the class itself rather than of a class nested in it, are found the same
  way; cppExpressionType's own member fallback became redundant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-13 00:46:13 +00:00
committed by GitHub
parent 79cd737bec
commit df3ce972f7
3 changed files with 744 additions and 23 deletions
+1
View File
@@ -100,6 +100,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on a variable, parameter or member declared as one of your classes, like `iter->Valid()` on an `Iterator* iter` or `it.Valid()` on a `const Iterator& it`, now links to that method of the class C++ itself would find for the type where the call is written, or of a class it inherits from. Before, the method was picked by the type's last name, so a class of the same name nested elsewhere could win: in facebook/rocksdb, calls on an `Iterator` reached `MemTableRep::Iterator`, which declares `Valid` and `Next` itself while the `Iterator` the code means inherits them, google/leveldb's reached `SkipList::Iterator`, and in protocolbuffers/protobuf the Java generator's `context->options()` reached the hpb generator's `Context` and `text_parser.Parse(…)` on a `TextFormat::Parser` reached the `.proto` parser. When that class and the classes it derives from have no method of that name, the call no longer links to an unrelated class's method that shares it, as protobuf's `field->full_name()` on a `FieldDescriptor*` did. A type is also found through a `using` declaration the file writes, like protobuf's `using google::protobuf::Descriptor;`, before a class of the same name declared at global scope elsewhere in the repository; this also applies to base classes and to the type a smart pointer holds. Re-index C++ projects after upgrading.
- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading.
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<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.
- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic<bool> shutting_down_`, no longer links to a project method that only shares the method's name. Re-index C++ projects after upgrading.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged.
- In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading.
+510
View File
@@ -0,0 +1,510 @@
/**
* A C++ call on a data member of the calling method's class reaches the
* method of the member's type, wherever the class is declared.
*
* Receiver inference reads back through the call's file, then the header of
* the same name (`foo.cc` → `foo.h`). In facebook/rocksdb, `DBImpl` is
* declared in db_impl.h and its methods are defined in db_impl_write.cc,
* db_impl_compaction_flush.cc, db_impl_open.cc and more, so `versions_`,
* `mutex_` and the rest were never read there: their calls were guessed by
* the receiver's name. Neither was a member declared below an inline method
* that uses it, or one a base class declares. A gtest `TEST_F(Fixture, …)`
* body is a method of a class derived from `Fixture`, so `db_` there is the
* fixture's member.
*
* C++ looks a name up in the function first, then in its class and the
* classes it derives from: a parameter or local of the name, or one the scan
* can't read, still decides. A guess can land on the right method by a shared
* word, so these tests check the confidence, not only the target.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import type { Node } from '../src/types';
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
});
async function indexed(files: Record<string, string>): Promise<CodeGraph> {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-member-'));
roots.push(root);
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
return CodeGraph.init(root, { index: true });
}
function callable(cg: CodeGraph, qualifiedName: string): Node {
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find((n) => n.qualifiedName === qualifiedName);
if (!node) throw new Error(`no function or method ${qualifiedName}`);
return node;
}
/** `calls` callees of a function or method, as `qualifiedName @confidence`, one per call site. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}
const lines = (...text: string[]): string => [...text, ''].join('\n');
/** rocksdb's DBImpl, trimmed: declared in db_impl.h, its methods defined in other files. */
const ROCKSDB = {
'db/version_set.h': lines(
'namespace rocksdb {',
'class ColumnFamilySet {',
' public:',
' int NumberOfColumnFamilies() const { return 0; }',
'};',
'class VersionSet {',
' public:',
' ColumnFamilySet* GetColumnFamilySet() { return nullptr; }',
' void SetLastSequence(int s) {}',
'};',
'class ColumnFamilyHandleImpl {',
' public:',
' ColumnFamilySet* GetColumnFamilySet() { return nullptr; }',
' void SetLastSequence(int s) {}',
'};',
'class Statistics {',
' public:',
' bool load() const { return false; }',
'};',
'} // namespace rocksdb',
),
'monitoring/instrumented_mutex.h': lines(
'namespace rocksdb {',
'class InstrumentedMutex {',
' public:',
' void Lock() {}',
' void Unlock() {}',
'};',
'namespace port {',
'class Mutex {',
' public:',
' void Lock() {}',
' void Unlock() {}',
'};',
'} // namespace port',
'} // namespace rocksdb',
),
'db/db_impl/db_impl.h': lines(
'#include <atomic>',
'#include <memory>',
'#include "db/version_set.h"',
'#include "monitoring/instrumented_mutex.h"',
'namespace rocksdb {',
'class DBImpl {',
' public:',
' void WriteImpl();',
' void FlushMemTable();',
' protected:',
' std::unique_ptr<VersionSet> versions_;',
' InstrumentedMutex mutex_;',
' std::atomic<bool> shutting_down_;',
'};',
'} // namespace rocksdb',
),
};
describe('a data member of the calling method’s class', () => {
it('reproduction: one the class declares in another header', async () => {
const cg = await indexed({
...ROCKSDB,
'db/db_impl/db_impl_write.cc': lines(
'#include "db/db_impl/db_impl.h"',
'namespace rocksdb {',
'// Another function\'s local of the name, which the method does not see.',
'void UnlockStale(port::Mutex* mutex_) { mutex_->Unlock(); }',
'void DBImpl::WriteImpl() {',
' mutex_.Lock();',
' versions_->SetLastSequence(1);',
' versions_->GetColumnFamilySet()->NumberOfColumnFamilies();',
' if (shutting_down_.load()) return;',
' mutex_.Unlock();',
'}',
'} // namespace rocksdb',
),
});
try {
// A `std::atomic<bool>` member calls no method of the project's.
expect(calls(cg, 'rocksdb::DBImpl::WriteImpl')).toEqual([
'rocksdb::ColumnFamilySet::NumberOfColumnFamilies @0.85',
'rocksdb::InstrumentedMutex::Lock @0.9',
'rocksdb::InstrumentedMutex::Unlock @0.9',
'rocksdb::VersionSet::GetColumnFamilySet @0.9',
'rocksdb::VersionSet::SetLastSequence @0.9',
]);
} finally {
cg.close();
}
});
it('one declared below the inline method that uses it, and one a base class declares', async () => {
const cg = await indexed({
...ROCKSDB,
'db/memtable_list.h': lines(
'namespace rocksdb {',
'class MemTableListVersion {',
' public:',
' int NumNotFlushed() const { return 0; }',
'};',
'class MemTable {',
' public:',
' int NumNotFlushed() const { return 1; }',
'};',
'class MemTableList {',
' public:',
' bool IsFlushPending() const { return current_->NumNotFlushed() > 0; }',
' // A line that starts with a dereference is code, not a comment.',
' int Pick(const int* first) const {',
' return Choose(0,',
' *first);',
' }',
' private:',
' int Choose(int a, int b) const { return a + b; }',
' /*',
' MemTable* current_;',
' */',
' MemTableListVersion* current_;',
'};',
'} // namespace rocksdb',
),
'db/db_impl/db_impl_secondary.h': lines(
'#include "db/db_impl/db_impl.h"',
'namespace rocksdb {',
'class DBImplSecondary : public DBImpl {',
' public:',
' void TryCatchUpWithPrimary();',
'};',
'} // namespace rocksdb',
),
'db/db_impl/db_impl_secondary.cc': lines(
'#include "db/db_impl/db_impl_secondary.h"',
'namespace rocksdb {',
'void DBImplSecondary::TryCatchUpWithPrimary() {',
' versions_->SetLastSequence(2);',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::MemTableList::IsFlushPending')).toEqual(['rocksdb::MemTableListVersion::NumNotFlushed @0.9']);
expect(calls(cg, 'rocksdb::DBImplSecondary::TryCatchUpWithPrimary')).toEqual(['rocksdb::VersionSet::SetLastSequence @0.9']);
} finally {
cg.close();
}
});
it("a gtest TEST_F body's fixture member", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': lines(
'namespace rocksdb {',
'class IteratorBase {',
' public:',
' virtual void SeekToFirst() = 0;',
'};',
'class Iterator : public IteratorBase {};',
'class MemTableRep {',
' public:',
' class Iterator {',
' public:',
' virtual void SeekToFirst() = 0;',
' };',
'};',
'class DB {',
' public:',
' virtual Iterator* NewIterator() = 0;',
'};',
'} // namespace rocksdb',
),
'db/db_test_util.h': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'class DBTestBase {',
' protected:',
' DB* db_;',
'};',
'} // namespace rocksdb',
),
'db/db_iterator_test.cc': lines(
'#include "db/db_test_util.h"',
'namespace rocksdb {',
'class DBIteratorTest : public DBTestBase {};',
'TEST_F(DBIteratorTest, IterSeekToFirst) {',
' auto iter = db_->NewIterator();',
' iter->SeekToFirst();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::TEST_F')).toEqual(['rocksdb::DB::NewIterator @0.9', 'rocksdb::IteratorBase::SeekToFirst @0.9']);
} finally {
cg.close();
}
});
});
describe('a data member of the class itself', () => {
it('not one of a class nested in it, which its header declares first', async () => {
// rocksdb's `MultiScan::MultiScanIterator` has a `scan_opts_` of its own.
const cg = await indexed({
'table/multi_scan.h': lines(
'namespace rocksdb {',
'class ScanOptions {',
' public:',
' int size() const { return 0; }',
'};',
'class ScanRanges {',
' public:',
' int size() const { return 1; }',
'};',
'class MultiScan {',
' public:',
' class MultiScanIterator {',
' private:',
' ScanRanges* scan_opts_;',
' };',
' int Count() const;',
' private:',
' ScanOptions* scan_opts_;',
'};',
'} // namespace rocksdb',
),
'table/multi_scan.cc': lines(
'#include "table/multi_scan.h"',
'namespace rocksdb {',
'int MultiScan::Count() const { return scan_opts_->size(); }',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::MultiScan::Count')).toEqual(['rocksdb::ScanOptions::size @0.9']);
} finally {
cg.close();
}
});
});
describe('which class declares the member', () => {
it('the declaration in the caller’s own translation unit, of classes that share a name', async () => {
// google/leveldb's memenv.cc and fault_injection_test.cc each declare a `FileState`.
const cg = await indexed({
'include/leveldb/slice.h': lines(
'namespace leveldb {',
'class Slice {',
' public:',
' int size() const { return 0; }',
'};',
'class Block {',
' public:',
' int size() const { return 1; }',
'};',
'} // namespace leveldb',
),
'helpers/memenv/memenv.cc': lines(
'#include "include/leveldb/slice.h"',
'namespace leveldb {',
'class FileState {',
' public:',
' int Size() const;',
' private:',
' Slice* blocks_;',
'};',
'int FileState::Size() const { return blocks_->size(); }',
'} // namespace leveldb',
),
'db/fault_injection_test.cc': lines(
'#include "include/leveldb/slice.h"',
'namespace leveldb {',
'class FileState {',
' public:',
' int Size() const;',
' private:',
' Block* blocks_;',
'};',
'int FileState::Size() const { return blocks_->size(); }',
'} // namespace leveldb',
),
});
try {
const sizes = cg.getNodesByKind('method').filter((n) => n.qualifiedName === 'leveldb::FileState::Size');
const byFile = Object.fromEntries(sizes.map((n) => [n.filePath, cg.getCallees(n.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName)]));
expect(byFile).toEqual({
'db/fault_injection_test.cc': ['leveldb::Block::size'],
'helpers/memenv/memenv.cc': ['leveldb::Slice::size'],
});
} finally {
cg.close();
}
});
it('not a base that depends on the template’s parameters, where a bare name is not looked up', async () => {
const cg = await indexed({
...ROCKSDB,
'util/heap_test.cc': lines(
'#include "db/db_impl/db_impl.h"',
'namespace rocksdb {',
'template <typename T>',
'class Holder {',
' protected:',
' InstrumentedMutex mutex_;',
'};',
'port::Mutex mutex_;',
'template <typename T>',
'class Guard : public Holder<T> {',
' public:',
' void Enter() { mutex_.Lock(); }',
'};',
'} // namespace rocksdb',
),
});
try {
// C++ finds the namespace's `mutex_` here: `this->mutex_` would name the base's.
expect(calls(cg, 'rocksdb::Guard::Enter')).not.toContain('rocksdb::InstrumentedMutex::Lock @0.9');
} finally {
cg.close();
}
});
});
describe('what keeps the call as it was', () => {
it('a local of the name that the scan cannot read', async () => {
const cg = await indexed({
...ROCKSDB,
'db/db_impl/db_impl_compaction_flush.cc': lines(
'#include "db/db_impl/db_impl.h"',
'namespace rocksdb {',
'void DBImpl::FlushMemTable() {',
' auto mutex_ = GetFlushMutex();',
' mutex_->Lock();',
'}',
'} // namespace rocksdb',
),
});
try {
// `mutex_` here is the local, not the member.
expect(calls(cg, 'rocksdb::DBImpl::FlushMemTable').filter((c) => c.includes('Lock'))).not.toContain('rocksdb::InstrumentedMutex::Lock @0.9');
} finally {
cg.close();
}
});
it('a parameter of a method the class declares, which is no member', async () => {
// rocksdb's DB declares `CreateColumnFamilies(const std::vector<…>&
// column_families, …)`; DBImpl's `const ColumnFamilySet* const
// column_families` is a local the scan cannot read.
const cg = await indexed({
...ROCKSDB,
'db/db_impl/db_impl.h': ROCKSDB['db/db_impl/db_impl.h'].replace(
' void FlushMemTable();',
' void FlushMemTable();\n void CreateColumnFamilies(std::vector<int>* column_families,\n int* handles);\n int CountColumnFamilies();\n ColumnFamilySet* stats() const;',
),
'db/db_impl/db_impl_write.cc': lines(
'#include "db/db_impl/db_impl.h"',
'namespace rocksdb {',
'int DBImpl::CountColumnFamilies() {',
' const ColumnFamilySet* const column_families = versions_->GetColumnFamilySet();',
' const Statistics* const stats = nullptr;',
' if (stats->load()) return 0;',
' return column_families->NumberOfColumnFamilies();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('NumberOfColumnFamilies'))).toEqual([
'rocksdb::ColumnFamilySet::NumberOfColumnFamilies @0.7',
]);
// Nor is a method the class declares (`stats()`) a member of its name.
expect(calls(cg, 'rocksdb::DBImpl::CountColumnFamilies').filter((c) => c.includes('load'))).toEqual(['rocksdb::Statistics::load @0.7']);
} finally {
cg.close();
}
});
it("a member of a template parameter's type", async () => {
// rocksdb's `ComparatorWithU64TsImpl<TComparator>` holds a `TComparator cmp_without_ts_`.
const cg = await indexed({
'util/comparator.cc': lines(
'namespace rocksdb {',
'class Comparator {',
' public:',
' int timestamp_size() const { return 0; }',
'};',
'template <typename TComparator>',
'class ComparatorWithU64TsImpl {',
' public:',
' int Check() const { return cmp_without_ts_.timestamp_size(); }',
' private:',
' TComparator cmp_without_ts_;',
'};',
'// rocksdb\'s clock cache test reaches its shard through a dependent alias.',
'class ClockCacheShard {',
' public:',
' void Insert(int key) {}',
'};',
'template <class ClockCache>',
'class ClockCacheTest {',
' public:',
' using Shard = typename ClockCache::Shard;',
' void Insert(int key) { shard_->Insert(key); }',
' private:',
' Shard* shard_ = nullptr;',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::ComparatorWithU64TsImpl::Check')).toEqual(['rocksdb::Comparator::timestamp_size @0.7']);
expect(calls(cg, 'rocksdb::ClockCacheTest::Insert')).toEqual(['rocksdb::ClockCacheShard::Insert @0.65']);
} finally {
cg.close();
}
});
it('a member a base class template declares through an alias of its parameter', async () => {
// protobuf's MapIteratorBase<kIsMutable> holds a `MapFieldBase* map_` through
// `using MapFieldBase = std::conditional_t<kIsMutable, internal::MapFieldBase, …>`.
const cg = await indexed({
'src/google/protobuf/map_field.h': lines(
'#include <type_traits>',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'class MapFieldBase {',
' public:',
' void SetMapDirty() {}',
'};',
'} // namespace internal',
'template <bool kIsMutable>',
'class MapIteratorBase {',
' protected:',
' using MapFieldBase = std::conditional_t<kIsMutable, internal::MapFieldBase, const internal::MapFieldBase>;',
' MapFieldBase* map_;',
'};',
'class MapIterator : public MapIteratorBase<true> {',
' public:',
' void MutableValueRef() { map_->SetMapDirty(); }',
'};',
'} // namespace protobuf',
'} // namespace google',
),
'src/google/protobuf/map_field.cc': lines('#include "src/google/protobuf/map_field.h"'),
});
try {
expect(calls(cg, 'google::protobuf::MapIterator::MutableValueRef')).toEqual(['google::protobuf::internal::MapFieldBase::SetMapDirty @0.9']);
} finally {
cg.close();
}
});
});
+233 -23
View File
@@ -9129,28 +9129,188 @@ function cppWrittenClass(type: CppWrittenType, at: UnresolvedRef, context: Resol
* (a base class may).
*/
function cppDataMemberType(cls: string, name: string, context: ResolutionContext): CppWrittenType | null {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const word = new RegExp(`\\b${escaped}\\b`);
const declarator = buildDeclaratorRegex(escaped);
for (const decl of context.getNodesByQualifiedName(cls)) {
if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) continue;
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/);
if (!lines) continue;
const member = cppDataMemberIn(decl, name, context);
if (member) return member;
}
return null;
}
const CPP_DATA_MEMBERS = new WeakMap<ResolutionContext, Map<string, CppWrittenType | null>>();
const CPP_NESTED_SPANS = new WeakMap<ResolutionContext, Map<string, Array<[number, number]>>>();
const CPP_CLASS_PAREN_DEPTHS = new WeakMap<ResolutionContext, Map<string, number[]>>();
/**
* cppDataMemberType for one declaration of the class, `decl`. A declarator
* inside parentheses is a parameter of a method the class declares — which
* has no node of its own, so its lines are the class's — not a member:
* rocksdb's `DB` declares `CreateColumnFamilies(const
* std::vector<ColumnFamilyDescriptor>& column_families, …)`. Nor is a name
* an argument list follows, a method.
*/
function cppDataMemberIn(decl: Node, name: string, context: ResolutionContext): CppWrittenType | null {
if ((decl.language !== 'cpp' && decl.language !== 'c') || !CPP_CLASS_KINDS.has(decl.kind)) return null;
let memo = CPP_DATA_MEMBERS.get(context);
if (!memo) CPP_DATA_MEMBERS.set(context, (memo = new Map()));
const key = `${decl.id}\u0000${name}`;
const hit = memo.get(key);
if (hit !== undefined) return hit;
let member: CppWrittenType | null = null;
const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/);
if (lines) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const word = new RegExp(`\\b${escaped}\\b`);
const declarator = buildDeclaratorRegex(escaped);
const end = Math.min(decl.endLine ?? decl.startLine, lines.length);
const inner = context.getNodesInFile(decl.filePath).filter((n) => n.id !== decl.id && n.startLine >= decl.startLine &&
(n.endLine ?? n.startLine) <= end && (isCppFunctionNode(n) || CPP_CLASS_KINDS.has(n.kind)));
const nested = cppNestedSpans(decl, end, context);
const rawLines = cppRawStringLines(decl.filePath, context);
const depths = cppClassParenDepths(decl, lines, end, rawLines, context);
for (let l = decl.startLine; l <= end; l++) {
if (!word.test(lines[l - 1]!) || inner.some((n) => l >= n.startLine && l <= (n.endLine ?? n.startLine))) continue;
const declared = cppCodeOf(lines[l - 1]!)?.match(declarator)?.[1];
const type = declared && normalizeCppTypeName(declared);
if (!word.test(lines[l - 1]!) || nested.some(([start, stop]) => l >= start && l <= stop)) continue;
const code = cppCodeOf(cppLiteralsBlanked(lines[l - 1]!, l, rawLines));
const m = code ? declarator.exec(code) : null;
if (!m || code!.slice(m.index + m[0].length).startsWith('(')) continue;
let depth = depths[l - decl.startLine]!;
if (depth < 0) continue;
for (const c of code!.slice(l === decl.startLine ? decl.startColumn : 0, m.index)) {
if (c === '(') depth++;
else if (c === ')') depth--;
}
const type = depth <= 0 ? normalizeCppTypeName(m[1]!) : null;
if (type && type !== 'auto') {
return { text: declared!, scope: cls, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) };
member = { text: m[1]!, scope: decl.qualifiedName, file: decl.filePath, line: l, parameters: cppTemplateParameters(decl, context) };
break;
}
}
}
memo.set(key, member);
return member;
}
/**
* How many parentheses are open where each line of the C++ class
* declaration `decl`, which ends on line `end`, begins, or -1 for one that
* begins inside a block comment. Comments are followed across lines, not
* guessed as cppCodeOf does: it takes a code line that starts with a
* dereference, `*first_key, scratch);`, for one.
*/
function cppClassParenDepths(
decl: Node,
lines: readonly string[],
end: number,
rawLines: ReturnType<typeof cppRawStringLines>,
context: ResolutionContext,
): number[] {
let memo = CPP_CLASS_PAREN_DEPTHS.get(context);
if (!memo) CPP_CLASS_PAREN_DEPTHS.set(context, (memo = new Map()));
let depths = memo.get(decl.id);
if (!depths) {
depths = [];
let depth = 0;
let comment = false;
for (let l = decl.startLine; l <= end; l++) {
depths.push(comment ? -1 : depth);
const text = cppLiteralsBlanked(lines[l - 1] ?? '', l, rawLines);
for (let i = l === decl.startLine ? decl.startColumn : 0; i < text.length; i++) {
const c = text[i]!;
if (comment) {
if (c === '*' && text[i + 1] === '/') {
comment = false;
i++;
}
} else if (c === '/' && text[i + 1] === '/') {
break;
} else if (c === '/' && text[i + 1] === '*') {
comment = true;
i++;
} else if (c === '(') {
depth++;
} else if (c === ')' && depth > 0) {
depth--;
}
}
}
memo.set(decl.id, depths);
}
return depths;
}
/** The lines of the functions and classes nested in the C++ class declaration `decl`, which ends on line `end`. */
function cppNestedSpans(decl: Node, end: number, context: ResolutionContext): Array<[number, number]> {
let memo = CPP_NESTED_SPANS.get(context);
if (!memo) CPP_NESTED_SPANS.set(context, (memo = new Map()));
let spans = memo.get(decl.id);
if (!spans) {
spans = context.getNodesInFile(decl.filePath)
.filter((n) => n.id !== decl.id && n.startLine >= decl.startLine && (n.endLine ?? n.startLine) <= end &&
(isCppFunctionNode(n) || CPP_CLASS_KINDS.has(n.kind)))
.map((n): [number, number] => [n.startLine, n.endLine ?? n.startLine]);
memo.set(decl.id, spans);
}
return spans;
}
const CPP_CALLERS_MEMBERS = new WeakMap<ResolutionContext, Map<string, CppWrittenType | null>>();
/**
* The type a data member `name` of the C++ class the calling function
* `caller` belongs to has, as written in the class: its own, else one of a
* class it derives from, through its base edges (not a base that depends on
* the class's template parameters, where a bare name isn't looked up). The
* class is read wherever it is declared: rocksdb's `DBImpl` is declared in
* db_impl.h, while its methods are defined in db_impl_write.cc,
* db_impl_compaction_flush.cc, db_impl_open.cc and more. A gtest
* `TEST_F(Fixture, Name)` or `TEST_P` body is a method of a class derived
* from `Fixture`. Null when none of them declares it.
*/
function cppCallersMember(name: string, caller: Node, ref: UnresolvedRef, context: ResolutionContext): CppWrittenType | null {
let memo = CPP_CALLERS_MEMBERS.get(context);
if (!memo) CPP_CALLERS_MEMBERS.set(context, (memo = new Map()));
const key = `${caller.id}\u0000${name}`;
const hit = memo.get(key);
if (hit !== undefined) return hit;
const owner = cppMemberOwner(caller, context) ?? cppTestFixture(caller, ref, context);
const decls = owner ? cppScopeDeclarations(owner, cppClassDecls(owner, context), caller, context).decls : [];
const member = owner ? cppInheritedDataMember(decls, name, context, 0, new Set([owner])) : null;
memo.set(key, member);
return member;
}
/** The data member `name` of a class, in one of its declarations `decls` or else in a class it derives from. */
function cppInheritedDataMember(decls: readonly Node[], name: string, context: ResolutionContext, depth: number, seen: Set<string>): CppWrittenType | null {
for (const decl of decls) {
const own = cppDataMemberIn(decl, name, context);
if (own) return own;
}
if (depth >= 4 || !context.getSupertypeNodes) return null;
for (const decl of decls) {
for (const base of context.getSupertypeNodes(decl.id)) {
if (!CPP_CLASS_KINDS.has(base.kind) || seen.has(base.qualifiedName) || isCppDependentBase(decl, base, context)) continue;
seen.add(base.qualifiedName);
const inherited = cppInheritedDataMember([base], name, context, depth + 1, seen);
if (inherited) return inherited;
}
}
return null;
}
/**
* The fixture class of a gtest `TEST_F(Fixture, Name)` or `TEST_P(Fixture,
* Name)` body, which the index holds as a function named `TEST_F`: the body
* is a method of a class gtest derives from `Fixture`. Null for any other
* function, and for a fixture C++ lookup doesn't find.
*/
function cppTestFixture(caller: Node, ref: UnresolvedRef, context: ResolutionContext): string | null {
if (caller.kind !== 'function' || (caller.name !== 'TEST_F' && caller.name !== 'TEST_P')) return null;
const lines = context.getFileLines?.(caller.filePath) ?? context.readFile(caller.filePath)?.split(/\r?\n/);
const head = lines?.slice(caller.startLine - 1, caller.startLine + 1).join(' ') ?? '';
const fixture = /\bTEST_[FP]\s*\(\s*((?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*)\s*,/.exec(head)?.[1];
if (!fixture) return null;
const site = { scope: cppParentScope(caller.qualifiedName), file: caller.filePath, line: caller.startLine, parameters: new Set<string>() };
const found = cppClassWritten(fixture.replace(/\s+/g, ''), site, ref, context);
return found?.exact && CPP_CLASS_KINDS.has(found.node.kind) ? found.node.qualifiedName : null;
}
/**
* The type `*x` has when `x` has the type `type`: what a pointer points at,
* or what a standard smart pointer or optional holds. Null for any other type.
@@ -9253,10 +9413,8 @@ function cppExpressionType(expr: string, at: UnresolvedRef, context: ResolutionC
if (decl.initializer && isCppCallersDeclaration(at.filePath, decl.initializer.line, at, context)) {
return cppExpressionType(decl.initializer.text, { ...at, line: decl.initializer.line, column: 0 }, context, depth + 1);
}
if (decl.raw !== undefined && decl.callers) return decl.shadowed ? null : { ...callers, text: decl.raw };
// Declared nowhere in the calling method (`versions_` in db_impl_write.cc,
// whose class is declared in db_impl.h): a data member of its class.
return decl.shadowed || !owner ? null : cppDataMemberType(owner, name[1]!, context);
// (A data member of the caller's class has its type as `declared`.)
return decl.raw !== undefined && decl.callers && !decl.shadowed ? { ...callers, text: decl.raw } : null;
}
// `new Foo(…)`: a pointer to what it constructs.
const created = /^new\s+((?:::\s*)?[A-Za-z_][\w:]*(?:\s*<[^;{}()]*>)?)\s*(?:[({[][\s\S]*)?$/.exec(text);
@@ -9385,7 +9543,9 @@ function isCppTemplateDependentType(type: CppWrittenType, context: ResolutionCon
if (isCppTemplateParameterType(type)) return true;
const held = cppStdHeldType(type.text);
if (held) return isCppTemplateDependentType({ ...type, text: held }, context);
return isCppLibraryType(type.text, context) && (type.text.match(/[A-Za-z_]\w*/g) ?? []).some((word) => type.parameters.has(word));
// (A trait of one, `std::conditional_t<kIsMutable, …>`, too: it names another type.)
return (isCppLibraryType(type.text, context) || cppTypeSegments(type.text)?.[0] === 'std') &&
(type.text.match(/[A-Za-z_]\w*/g) ?? []).some((word) => type.parameters.has(word));
}
/** Where the C or C++ string or character literal opening at `text[start]` closes (the line's end if it doesn't). */
@@ -9487,10 +9647,11 @@ interface CppReceiverDeclaration {
*/
shadowed?: boolean;
/**
* The receiver's type as written where it is declared, when its own
* declaration says `auto` (`raw` is its text): the element type of the
* range an `auto` range-for iterates, or the type of an `auto` local's
* initializer — the `Iterator*` that `txn->GetIterator(…)` returns.
* The receiver's type as written where it is declared, when that is not
* in the calling function's own text or says `auto` there (`raw` is its
* text): the element type of the range an `auto` range-for iterates, the
* type of an `auto` local's initializer — the `Iterator*` that
* `txn->GetIterator(…)` returns — or a data member's, in its class.
*/
declared?: CppWrittenType;
/**
@@ -9744,8 +9905,9 @@ function matchCppDeclaredTypeCall(
// own the index doesn't hold: the class it names, by its qualified name.
const aliasedClass = decl.alias && cppAliasedClass(decl.alias, context);
if (decl.aliased && !aliasedClass) return undefined;
// An `auto` variable's type is looked up where it is written: where its
// range's type or its initializer's function is declared.
// A type the calling function doesn't write is looked up where it is
// written: where an `auto` variable's range or initializer gets it, or in
// a data member's class.
const site = decl.declared ?? cppCallSite(caller, ref, context);
const found = cppClassWritten(aliasedClass ? `::${aliasedClass}` : decl.raw, site, ref, context);
if (found === undefined && !aliasedClass && isCppOutsideQualifiedType(decl.raw, site, context)) return null;
@@ -10267,8 +10429,18 @@ function inferCppReceiverType(
// writes `upb_MiniTableField field = $field_init$;` in the raw string it
// emits, above calls on its own `upb::FieldDefPtr field` parameter.
const rawLines = cppRawStringLines(ref.filePath, context);
// Past the calling function's first line, C++ looks the name up among the
// members of the function's class before anything outside it.
const caller = context.getNodeById?.(ref.fromNodeId);
const memberCaller = caller && caller.filePath === ref.filePath && (caller.kind === 'method' || caller.kind === 'function') ? caller : null;
let memberLooked = memberCaller === null;
for (let i = callLineIndex; i >= 0; i--) {
if (!memberLooked && memberCaller && i < memberCaller.startLine - 1) {
memberLooked = true;
const member = cppMemberReceiverType(receiverName, memberCaller, ref, context, found);
if (member !== undefined) return member;
}
const text = lines[i];
if (!text || !receiverPattern.test(text)) continue;
const line = cppCodeOf(cppLiteralsBlanked(text, i + 1, rawLines));
@@ -10333,6 +10505,11 @@ function inferCppReceiverType(
if (cppRebindsReceiver(line, escapedReceiver)) found.shadowed = true;
}
}
// (A function that starts on the file's first line.)
if (!memberLooked && memberCaller) {
const member = cppMemberReceiverType(receiverName, memberCaller, ref, context, found);
if (member !== undefined) return member;
}
const headerCandidates = [
ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.h'),
@@ -10367,6 +10544,35 @@ function inferCppReceiverType(
return null;
}
/**
* The type of a C++ receiver the calling function doesn't declare, as a data
* member of the function's class (cppCallersMember), recorded in `found` as
* a declaration of the caller's, where the class writes it. Undefined when
* it is no member, or the scan passed a declaration of the name it couldn't
* read (`auto mutex_ = Make();`), which is what the name means there.
*/
function cppMemberReceiverType(
receiverName: string,
caller: Node,
ref: UnresolvedRef,
context: ResolutionContext,
found: CppReceiverDeclaration,
): string | null | undefined {
if (found.shadowed) return undefined;
const member = cppCallersMember(receiverName, caller, ref, context);
const normalized = member && normalizeCppTypeName(member.text);
if (!member || !normalized) return undefined;
// A member of a template parameter's type (`TComparator cmp_;`), or of an
// alias of one (`using Shard = typename Cache::Shard;`, protobuf's
// `using MapFieldBase = std::conditional_t<kIsMutable, …>;`), names no
// particular class: the call goes on as before.
const followed = cppFollowWrittenAliases(member, ref, context);
if (isCppTemplateDependentType(followed ?? member, context) || resolveCppAliasedType(member.text, ref, context) === null) return undefined;
noteCppDeclaration(found, member.text, true);
found.declared = member;
return cppDeclaredType(member.text, normalized, true, ref, context, found);
}
/**
* Last `::`-separated segment of a (possibly namespace-qualified) C++ name.
*/
@@ -10968,6 +11174,10 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
CPP_LOOP_ELEMENTS.delete(context);
CPP_AUTO_LOCALS.delete(context);
CPP_SCOPE_NAMES.delete(context);
CPP_DATA_MEMBERS.delete(context);
CPP_NESTED_SPANS.delete(context);
CPP_CLASS_PAREN_DEPTHS.delete(context);
CPP_CALLERS_MEMBERS.delete(context);
SOLIDITY_SUPERS.delete(context);
DECLARED_SUPERS.delete(context);
INHERITED_METHODS.delete(context);