Files
codegraph/__tests__/cpp-smart-pointer-receiver.test.ts
T
Colby Mchenry 85201e95b3 fix(cpp): a smart pointer's held class lacking a method gets no edge only when its bases are known (#2530)
matchCppHeldTypeCall (#2440) returned null (no edge) for a `->` call on a
`std::unique_ptr`/`std::shared_ptr`/`std::optional` whose held class
lacks the method, unless that class is a class template. #2493 found the
gap in the same rule for raw pointers and values: a base the index can't
link may declare the method. protobuf's `class FlatAllocator : public
decltype(ApplyTypeList<FlatAllocatorImpl>(…))` has no extends edge to
`FlatAllocatorImpl`, and rocksdb's `WritePreparedTxnDB` derives from an
alias each `#if` arm declares differently.

Both paths now share one verdict, cppLacksMethod: no class template, and
cppBasesKnown (every base on the way is linked to a class, or is a library
type). Otherwise the call goes on as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:16:09 +00:00

482 lines
16 KiB
TypeScript

/**
* A C++ `->` call on a receiver declared as a standard smart pointer or
* optional — `std::unique_ptr<Iterator> iter; iter->Valid()` — calls a member
* of the type it holds.
*
* facebook/rocksdb has about 14,000 of these calls on a declaration in the
* calling function or its class. Receiver inference read the declared type as
* `unique_ptr`, which has no project methods, so the call fell through to a
* guess by the method's name and the receiver's words: `iter->Valid()` went
* to `ArenaWrappedDBIter::Valid`, `db_copy->Get()` to a test's `Get`.
*
* The held type is looked up as C++ looks a name up where the call is
* written — the scopes around the call, the file's `using`s, aliases
* followed — and the method is taken from that class or the classes it
* derives from. Its last name alone is not enough: rocksdb's `Iterator` is
* also the name of `MemTableRep::Iterator` and other nested classes, while the
* `Iterator` the call means inherits `Valid` from `IteratorBase`. A template
* parameter, a library type and an alias of a type outside the project hold
* no project class, so those calls get no edge; neither does a call the held
* class has no method for (unless a class template's specialization, or a
* base the index doesn't link, may declare it). A held type the project
* doesn't declare — a generated message — leaves the call as it was.
*/
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-held-'));
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`. */
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)
.sort();
}
const lines = (...l: string[]): string => [...l, ''].join('\n');
/** rocksdb's iterator family, trimmed: the public Iterator, a nested namesake, and an implementation a guess reaches. */
const ROCKSDB = {
'include/rocksdb/iterator_base.h': lines(
'namespace rocksdb {',
'class IteratorBase {',
' public:',
' virtual ~IteratorBase() {}',
' virtual bool Valid() const = 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 Next() = 0;',
' };',
'};',
'} // namespace rocksdb',
),
'db/arena_wrapped_db_iter.h': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'class ArenaWrappedDBIter : public Iterator {',
' public:',
' bool Valid() const override { return true; }',
' void Next() override {}',
' int value() const override { return 0; }',
'};',
'} // namespace rocksdb',
),
'include/rocksdb/db.h': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'class DB {',
' public:',
' virtual int Get(int key) = 0;',
' virtual Iterator* NewIterator() = 0;',
'};',
'class DBGetBatch {',
' public:',
' int Get(int key) { return key; }',
'};',
'} // namespace rocksdb',
),
};
describe('a `->` call on a standard smart pointer or optional reaches the type it holds', () => {
it('reproduction: `std::unique_ptr<Iterator> iter; iter->Valid()` is the Iterator the scope sees, not a namesake', async () => {
const cg = await indexed({
...ROCKSDB,
'db/db_impl.cc': lines(
'#include <memory>',
'#include "include/rocksdb/db.h"',
'#include "db/arena_wrapped_db_iter.h"',
'namespace rocksdb {',
'int CountKeys(DB* db) {',
' std::unique_ptr<Iterator> iter(db->NewIterator());',
' int n = 0;',
' for (; iter->Valid(); iter->Next()) n += iter->value();',
' return n;',
'}',
'} // namespace rocksdb',
),
});
try {
// `Valid` and `Next` are IteratorBase's, which Iterator derives from.
expect(calls(cg, 'rocksdb::CountKeys')).toEqual([
'rocksdb::DB::NewIterator',
'rocksdb::Iterator::value',
'rocksdb::IteratorBase::Next',
'rocksdb::IteratorBase::Valid',
]);
} finally {
cg.close();
}
});
it('`std::shared_ptr`, `std::optional`, a parameter, and a member the class declares in its header', async () => {
const cg = await indexed({
...ROCKSDB,
// Classes the receivers' names point a guess at.
'table/table_db.h': lines(
'namespace rocksdb {',
'class TableDB {',
' public:',
' int Get(int key) { return key; }',
'};',
'class CuckooTableDB {',
' public:',
' int Get(int key) { return key; }',
'};',
'} // namespace rocksdb',
),
'db/db_impl.h': lines(
'#include <memory>',
'#include "include/rocksdb/db.h"',
'namespace rocksdb {',
'class DBImpl {',
' public:',
' int Lookup(int key);',
' private:',
' std::shared_ptr<DB> table_db_;',
'};',
'} // namespace rocksdb',
),
'db/db_impl.cc': lines(
'#include <optional>',
'#include "db/db_impl.h"',
'#include "table/table_db.h"',
'namespace rocksdb {',
'int DBImpl::Lookup(int key) {',
' return table_db_->Get(key);',
'}',
'int Probe(const std::shared_ptr<DB>& cuckoo_db) {',
' std::optional<DBGetBatch> batch;',
' return cuckoo_db->Get(1) + batch->Get(2);',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::DBImpl::Lookup')).toEqual(['rocksdb::DB::Get']);
expect(calls(cg, 'rocksdb::Probe')).toEqual(['rocksdb::DB::Get', 'rocksdb::DBGetBatch::Get']);
} finally {
cg.close();
}
});
it('a nested class is found first from inside the class that declares it', async () => {
const cg = await indexed({
...ROCKSDB,
'memtable/skiplistrep.cc': lines(
'#include <memory>',
'#include "include/rocksdb/iterator.h"',
'#include "include/rocksdb/memtablerep.h"',
'namespace rocksdb {',
'class SkipListRep : public MemTableRep {',
' public:',
' bool Check();',
' private:',
' std::unique_ptr<Iterator> iter_;',
'};',
'bool SkipListRep::Check() { return iter_->Valid(); }',
'} // namespace rocksdb',
),
});
try {
// MemTableRep's own `Iterator`, which SkipListRep inherits, hides rocksdb::Iterator.
expect(calls(cg, 'rocksdb::SkipListRep::Check')).toEqual(['rocksdb::MemTableRep::Iterator::Valid']);
} finally {
cg.close();
}
});
it('a held type named through an alias, a `using namespace`, and a class local to the function', async () => {
const cg = await indexed({
...ROCKSDB,
'table/internal_iterator.h': lines(
'namespace rocksdb {',
'template <class TValue>',
'class InternalIteratorBase {',
' public:',
' virtual void SeekToFirst() = 0;',
'};',
'using InternalIterator = InternalIteratorBase<int>;',
'class MergingIterator : public InternalIterator {',
' public:',
' void SeekToFirst() override {}',
'};',
'} // namespace rocksdb',
),
'table/scan.cc': lines(
'#include <memory>',
'#include "table/internal_iterator.h"',
'namespace rocksdb {',
'void ScanTable(InternalIterator* raw) {',
' std::unique_ptr<InternalIterator> it(raw);',
' it->SeekToFirst();',
'}',
'class EventListener {',
' public:',
' void OnFlushCompleted() {}',
'};',
'void Notify() {',
' class FlushListener {',
' public:',
' void OnFlushCompleted() {}',
' };',
' std::shared_ptr<FlushListener> listener;',
' listener->OnFlushCompleted();',
'}',
'} // namespace rocksdb',
),
'tools/dump.cc': lines(
'#include <memory>',
'#include "include/rocksdb/db.h"',
'#include "include/rocksdb/memtablerep.h"',
'using namespace rocksdb;',
'int Dump(DB* db) {',
' std::unique_ptr<Iterator> it(db->NewIterator());',
' return it->Valid() ? 1 : 0;',
'}',
),
});
try {
expect(calls(cg, 'rocksdb::ScanTable')).toEqual(['rocksdb::InternalIteratorBase::SeekToFirst']);
expect(calls(cg, 'rocksdb::Notify')).toEqual(['rocksdb::Notify::FlushListener::OnFlushCompleted']);
expect(calls(cg, 'Dump')).toEqual(['rocksdb::DB::NewIterator', 'rocksdb::IteratorBase::Valid']);
} finally {
cg.close();
}
});
});
describe('a held type that is no class of the project\'s gets no guess', () => {
it('a template parameter, a library type, and an alias of one', async () => {
const cg = await indexed({
...ROCKSDB,
'table/block.h': lines(
'namespace rocksdb {',
'class Block {',
' public:',
' unsigned size() const { return 0; }',
' int front() const { return 0; }',
'};',
'} // namespace rocksdb',
),
'db_stress_tool/stress.cc': lines(
'#include <deque>',
'#include <memory>',
'#include <vector>',
'#include "db/arena_wrapped_db_iter.h"',
'#include "table/block.h"',
'namespace rocksdb {',
'template <class IterType>',
'void TestIterate(IterType* raw) {',
' std::unique_ptr<IterType> iter(raw);',
' iter->Next();',
'}',
'using BlockList = std::vector<int>;',
'class BlockBasedTableIterator {',
' public:',
' int First();',
' private:',
' std::unique_ptr<std::deque<int>> block_handles_;',
' std::shared_ptr<BlockList> block_list_;',
'};',
'int BlockBasedTableIterator::First() {',
' return block_handles_->front() + static_cast<int>(block_list_->size());',
'}',
'} // namespace rocksdb',
),
});
try {
// Not ArenaWrappedDBIter::Next, Block::front or Block::size.
expect(calls(cg, 'rocksdb::TestIterate')).toEqual([]);
expect(calls(cg, 'rocksdb::BlockBasedTableIterator::First')).toEqual([]);
} finally {
cg.close();
}
});
it('a class that has no such method, nor does any class it derives from', async () => {
const cg = await indexed({
...ROCKSDB,
'db/compaction.cc': lines(
'#include <memory>',
'#include "include/rocksdb/db.h"',
'#include "db/arena_wrapped_db_iter.h"',
'namespace rocksdb {',
'class CompactionIterator {',
' public:',
' void Release() {}',
'};',
'void Compact(DB* raw) {',
' std::unique_ptr<DB> db(raw);',
' db->Release();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Compact')).toEqual([]);
} finally {
cg.close();
}
});
});
describe('what keeps the call as it was', () => {
it('a type the project does not declare, such as a generated message, and a class template\'s specialization', async () => {
// protocolbuffers/protobuf's tests hold generated messages, which derive
// from Message; facebook/rocksdb's order-maintenance tree has members only
// a specialization declares.
const cg = await indexed({
'src/google/protobuf/message.h': lines(
'namespace google {',
'namespace protobuf {',
'class Message {',
' public:',
' unsigned SpaceUsedLong() const { return 0; }',
'};',
'} // namespace protobuf',
'} // namespace google',
),
'src/google/protobuf/extension_set_unittest.cc': lines(
'#include <memory>',
'#include "src/google/protobuf/message.h"',
'namespace google {',
'namespace protobuf {',
'unsigned SpaceUsed() {',
' std::unique_ptr<unittest::TestAllExtensions> message;',
' return message->SpaceUsedLong();',
'}',
'} // namespace protobuf',
'} // namespace google',
),
'util/omt.h': lines(
'#include <memory>',
'namespace toku {',
'template <typename omtdata_t, bool marks>',
'class omt_node {',
' public:',
' void clear_stolen_bits() {}',
'};',
'template <typename omtdata_t>',
'class omt_node<omtdata_t, true> {',
' public:',
' bool get_marked() const { return false; }',
'};',
'bool HasMarks() {',
' std::unique_ptr<omt_node<int, true>> node;',
' return node->get_marked();',
'}',
'} // namespace toku',
),
});
try {
expect(calls(cg, 'google::protobuf::SpaceUsed')).toEqual(['google::protobuf::Message::SpaceUsedLong']);
expect(cg.getCallees(callable(cg, 'toku::HasMarks').id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.name)).toEqual(['get_marked']);
} finally {
cg.close();
}
});
it('`->` through a raw pointer to a smart pointer is the smart pointer\'s own member', async () => {
const cg = await indexed({
...ROCKSDB,
'cache/cache.h': lines(
'namespace rocksdb {',
'class Cache {',
' public:',
' void reset() {}',
'};',
'} // namespace rocksdb',
),
'db/version_set.cc': lines(
'#include <memory>',
'#include "include/rocksdb/iterator.h"',
'#include "cache/cache.h"',
'namespace rocksdb {',
'void Drop(std::unique_ptr<Iterator>* iter_cache) {',
' iter_cache->reset();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Drop')).toEqual([]);
} finally {
cg.close();
}
});
it('a held class with a base the index does not link, which may declare the method', async () => {
// protobuf's FlatAllocator derives from a `decltype(…)` that names FlatAllocatorImpl.
const cg = await indexed({
'src/google/protobuf/descriptor.cc': lines(
'#include <memory>',
'namespace google {',
'namespace protobuf {',
'namespace internal {',
'template <typename... T>',
'class FlatAllocatorImpl {',
' public:',
' void PlanArray(int array_size) {}',
'};',
'template <typename... T>',
'FlatAllocatorImpl<T...> ApplyTypeList(int);',
'class FlatAllocator : public decltype(ApplyTypeList<char, int>(0)) {};',
'} // namespace internal',
'void PlanAllocationSize(std::unique_ptr<internal::FlatAllocator>& alloc) {',
' alloc->PlanArray(1);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::PlanAllocationSize')).toEqual(['google::protobuf::internal::FlatAllocatorImpl::PlanArray']);
} finally {
cg.close();
}
});
});