Files
codegraph/__tests__/cpp-auto-range-for-receiver.test.ts
Colby Mchenry 01ab45d068 fix(cpp): a range-based for loop over auto elements has the element type of its range (#2492)
#2439 typed a range-for variable when the loop names its type. A loop
over `auto` elements names none, so receiver inference kept scanning as
for an `auto` local with an unreadable initializer: it reached an earlier
declaration of the same name, often another variable's, or fell through
to the guess by the receiver's name. facebook/rocksdb's db_impl_write.cc
loops `for (auto cfd : cfds)` over a `const autovector<ColumnFamilyData*>&`.

The loop's range is now read from its header and typed as written,
template arguments and all, since the index keeps only a return type's
last name:
- a name the calling function declares, or a data member of its class
  read from the class's own lines (`versions_` from db_impl_write.cc);
- an `auto` local's initializer;
- `*x`, `x.m`, `x->m`, `this`, and calls whose declaration writes their
  return type, free or on the object's class (own or inherited);
- aliases followed with their template arguments (cppAliasedTypeWritten);
- the element: a standard or abseil sequence container's first template
  argument, protobuf's RepeatedPtrField and rocksdb's autovector's, or what
  a project class's begin() returns, dereferenced (ColumnFamilySet's
  iterator's operator* gives ColumnFamilyData*). Nullability annotations
  are dropped.

The element goes through the same declaration record as any declared
receiver, so the typed path, #2413's gate for a standard-library element
and #2440's held-type lookup for `->` on a smart-pointer element apply
unchanged. Classes are looked up with #2440's cppClassWritten, methods
through its cppMethodOf, and matchCppHeldTypeCall now reads its held type
through the shared cppStdHeldType.

A/B on main 8a5ba3d0, site-keyed: leveldb identical; fmt +1 / 3
retargeted; rocksdb -68 / +173 / 104 retargeted / 231 confidence only;
protobuf -13 / +25 / 7 / 59. Every removal and retarget was a wrong
guess; 4 of rocksdb's additions reach MemTableRep::Iterator through the
typed path's last-name lookup, as a declared `Iterator* it` does on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 15:19:01 +00:00

534 lines
18 KiB
TypeScript

/**
* A C++ range-based for over `auto` elements has the element type of the
* range it iterates.
*
* #2439 read the type a loop declares, `for (ConformanceTestSuite *suite :
* suites)`. A loop over `auto` elements declares none, so the scan went on
* as for an `auto` local whose initializer it can't read: it reached an
* earlier declaration of the same name, often another variable's, or the
* call fell through to a guess by the receiver's name. In facebook/rocksdb,
* `for (auto cfd : cfds)` iterates a `const autovector<ColumnFamilyData*>&`
* parameter. The element type is read from the range where that can be done
* reliably:
* - a name the calling function declares, or a data member of its class:
* a container's first template argument, aliases followed with their
* template arguments;
* - a call whose declaration writes its return type, `*x`, `x.m`, `x->m`;
* - one of the project's classes, through what its `begin()` returns:
* rocksdb's `ColumnFamilySet`, whose iterator's `operator*` returns a
* `ColumnFamilyData*`.
* A guess can still land on the right method by a shared word, so these tests
* check the typed path's 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-range-for-'));
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');
/**
* facebook/rocksdb's column families, trimmed. `ColumnFamilyHandle` has
* methods of the same names; `DBImpl` is declared in db_impl.h and its
* methods are defined in db_impl_write.cc, so the scan's same-stem header
* never reads `versions_`.
*/
const ROCKSDB = {
'util/autovector.h': lines(
'#include <cstddef>',
'namespace rocksdb {',
'template <class T, size_t kSize = 8>',
'class autovector {',
' public:',
' size_t size() const { return 0; }',
' bool empty() const { return true; }',
'};',
'} // namespace rocksdb',
),
'db/column_family.h': lines(
'#include <cstdint>',
'namespace rocksdb {',
'class ColumnFamilyData {',
' public:',
' bool IsDropped() const;',
' void Ref();',
' uint32_t GetID() const;',
'};',
'class ColumnFamilySet {',
' public:',
' class iterator {',
' public:',
' explicit iterator(ColumnFamilyData* cfd) : current_(cfd) {}',
' iterator& operator++() { current_ = nullptr; return *this; }',
' bool operator!=(const iterator& other) const { return current_ != other.current_; }',
' ColumnFamilyData* operator*() { return current_; }',
' private:',
' ColumnFamilyData* current_;',
' };',
' iterator begin() { return iterator(head_); }',
' iterator end() { return iterator(nullptr); }',
' private:',
' ColumnFamilyData* head_ = nullptr;',
'};',
'class ColumnFamilyHandle {',
' public:',
' bool IsDropped() const;',
' void Ref();',
' uint32_t GetID() const;',
'};',
'} // namespace rocksdb',
),
'db/column_family.cc': lines(
'#include "db/column_family.h"',
'namespace rocksdb {',
'bool ColumnFamilyData::IsDropped() const { return false; }',
'void ColumnFamilyData::Ref() {}',
'uint32_t ColumnFamilyData::GetID() const { return 0; }',
'bool ColumnFamilyHandle::IsDropped() const { return false; }',
'void ColumnFamilyHandle::Ref() {}',
'uint32_t ColumnFamilyHandle::GetID() const { return 0; }',
'} // namespace rocksdb',
),
'db/version_set.h': lines(
'#include <memory>',
'#include "db/column_family.h"',
'namespace rocksdb {',
'class VersionSet {',
' public:',
' ColumnFamilySet* GetColumnFamilySet() { return column_family_set_.get(); }',
' private:',
' std::unique_ptr<ColumnFamilySet> column_family_set_;',
'};',
'} // namespace rocksdb',
),
'db/db_impl.h': lines(
'#include <memory>',
'#include "util/autovector.h"',
'#include "db/version_set.h"',
'namespace rocksdb {',
'class DBImpl {',
' public:',
' void AssignAtomicFlushSeq(const autovector<ColumnFamilyData*>& cfds);',
' void ScheduleFlushes();',
' private:',
' std::unique_ptr<VersionSet> versions_;',
'};',
'} // namespace rocksdb',
),
'db/db_impl_write.cc': lines(
'#include "db/db_impl.h"',
'namespace rocksdb {',
'void UseHandle(ColumnFamilyHandle* cfd) {',
' cfd->Ref();',
'}',
'void DBImpl::AssignAtomicFlushSeq(const autovector<ColumnFamilyData*>& cfds) {',
' for (auto cfd : cfds) {',
' cfd->GetID();',
' }',
'}',
'void DBImpl::ScheduleFlushes() {',
' for (auto cfd : *versions_->GetColumnFamilySet()) {',
' if (!cfd->IsDropped()) cfd->Ref();',
' }',
'}',
'} // namespace rocksdb',
),
};
/** protocolbuffers/protobuf's descriptors, trimmed; `upb` has a `FieldDefPtr` with the same accessors. */
const PROTOBUF = {
'src/google/protobuf/descriptor.h': lines(
'#include <string>',
'namespace google {',
'namespace protobuf {',
'class FieldDescriptor {',
' public:',
' int number() const;',
' const std::string& full_name() const;',
'};',
'class Descriptor {',
' public:',
' int field_count() const;',
'};',
'} // namespace protobuf',
'} // namespace google',
),
'src/google/protobuf/descriptor.cc': lines(
'#include "google/protobuf/descriptor.h"',
'namespace google {',
'namespace protobuf {',
'int FieldDescriptor::number() const { return 0; }',
'const std::string& FieldDescriptor::full_name() const { static std::string s; return s; }',
'int Descriptor::field_count() const { return 0; }',
'} // namespace protobuf',
'} // namespace google',
),
'upb/reflection/def.hpp': lines(
'namespace upb {',
'class FieldDefPtr {',
' public:',
' int number() const { return 0; }',
' const char* full_name() const { return ""; }',
'};',
'} // namespace upb',
),
'src/google/protobuf/compiler/cpp/field_layout.h': lines(
'#include <vector>',
'#include "absl/types/span.h"',
'#include "google/protobuf/descriptor.h"',
'namespace google {',
'namespace protobuf {',
'namespace compiler {',
'namespace cpp {',
'std::vector<const FieldDescriptor*> FieldNumberOrder(const Descriptor* descriptor);',
'class FieldLayout {',
' public:',
' absl::Span<const FieldDescriptor* const absl_nonnull> optimized_order() const { return {}; }',
'};',
'} // namespace cpp',
'} // namespace compiler',
'} // namespace protobuf',
'} // namespace google',
),
'src/google/protobuf/compiler/cpp/message.cc': lines(
'#include "google/protobuf/compiler/cpp/field_layout.h"',
'namespace google {',
'namespace protobuf {',
'namespace compiler {',
'namespace cpp {',
'std::vector<const FieldDescriptor*> FieldNumberOrder(const Descriptor* descriptor) {',
' return {};',
'}',
'int Numbers(const Descriptor* descriptor) {',
' int sum = 0;',
' for (const auto* field : FieldNumberOrder(descriptor)) {',
' sum += field->number();',
' }',
' return sum;',
'}',
'void Names(const FieldLayout& layout) {',
' for (const auto* field : layout.optimized_order()) {',
' field->full_name();',
' }',
'}',
'} // namespace cpp',
'} // namespace compiler',
'} // namespace protobuf',
'} // namespace google',
),
};
describe('an `auto` range-for variable has the element type of its range', () => {
it('reproduction: rocksdb\'s `for (auto cfd : cfds)` over an autovector parameter', async () => {
const cg = await indexed(ROCKSDB);
try {
// The scan went past the loop to UseHandle's `ColumnFamilyHandle* cfd`.
expect(calls(cg, 'rocksdb::DBImpl::AssignAtomicFlushSeq')).toEqual(['rocksdb::ColumnFamilyData::GetID @0.9']);
expect(calls(cg, 'rocksdb::UseHandle')).toEqual(['rocksdb::ColumnFamilyHandle::Ref @0.9']);
} finally {
cg.close();
}
});
it('a project class iterated through its begin(), reached from a member another header declares', async () => {
const cg = await indexed(ROCKSDB);
try {
// `versions_` is DBImpl's `std::unique_ptr<VersionSet>`; GetColumnFamilySet()
// returns a `ColumnFamilySet*`, whose iterator's `operator*` returns a
// `ColumnFamilyData*`. (The range's own call is left out here: no
// receiver scan reads db_impl.h from db_impl_write.cc.)
expect(calls(cg, 'rocksdb::DBImpl::ScheduleFlushes').filter((c) => !c.startsWith('rocksdb::VersionSet::'))).toEqual([
'rocksdb::ColumnFamilyData::IsDropped @0.9',
'rocksdb::ColumnFamilyData::Ref @0.9',
]);
} finally {
cg.close();
}
});
it('a call whose declaration writes its return type, and a nullability annotation in an element', async () => {
const cg = await indexed(PROTOBUF);
try {
expect(calls(cg, 'google::protobuf::compiler::cpp::Numbers')).toEqual([
'google::protobuf::FieldDescriptor::number @0.9',
'google::protobuf::compiler::cpp::FieldNumberOrder @0.9',
]);
expect(calls(cg, 'google::protobuf::compiler::cpp::Names')).toEqual([
'google::protobuf::FieldDescriptor::full_name @0.9',
'google::protobuf::compiler::cpp::FieldLayout::optimized_order @0.9',
]);
} finally {
cg.close();
}
});
it('an `auto` local\'s initializer, an alias with template arguments, and a smart pointer element', async () => {
const cg = await indexed({
'db/blob_file_meta.h': lines(
'#include <cstdint>',
'namespace rocksdb {',
'class BlobFileMetaData {',
' public:',
' uint64_t GetBlobFileNumber() const { return 0; }',
'};',
'class BlobFileAddition {',
' public:',
' uint64_t GetBlobFileNumber() const { return 0; }',
'};',
'} // namespace rocksdb',
),
'db/version_storage.h': lines(
'#include <memory>',
'#include <vector>',
'#include "db/blob_file_meta.h"',
'namespace rocksdb {',
'class VersionStorageInfo {',
' public:',
' using BlobFiles = std::vector<std::shared_ptr<BlobFileMetaData>>;',
' const BlobFiles& GetBlobFiles() const { return blob_files_; }',
' private:',
' BlobFiles blob_files_;',
'};',
'} // namespace rocksdb',
),
'db/db_filesnapshot.cc': lines(
'#include "db/version_storage.h"',
'namespace rocksdb {',
'uint64_t Largest(const VersionStorageInfo* vstorage) {',
' uint64_t largest = 0;',
' const auto& blob_files = vstorage->GetBlobFiles();',
' for (const auto& meta : blob_files) {',
' largest = meta->GetBlobFileNumber();',
' }',
' return largest;',
'}',
'} // namespace rocksdb',
),
});
try {
// `->` on the `std::shared_ptr<BlobFileMetaData>` element reaches the type it holds.
expect(calls(cg, 'rocksdb::Largest')).toEqual([
'rocksdb::BlobFileMetaData::GetBlobFileNumber @0.9',
'rocksdb::VersionStorageInfo::GetBlobFiles @0.9',
]);
} finally {
cg.close();
}
});
it('a data member of another object, and loops inside loops', async () => {
const cg = await indexed({
'db/compaction/subcompaction_state.h': lines(
'namespace rocksdb {',
'class Output {',
' public:',
' void Finish() {}',
'};',
'class SubcompactionState {',
' public:',
' void RemoveLastEmptyOutput() {}',
' Output outputs[2];',
'};',
'class FlushJob {',
' public:',
' void RemoveLastEmptyOutput() {}',
'};',
'} // namespace rocksdb',
),
'db/compaction/compaction_state.h': lines(
'#include <vector>',
'#include "db/compaction/subcompaction_state.h"',
'namespace rocksdb {',
'class CompactionState {',
' public:',
' std::vector<SubcompactionState> sub_compact_states;',
'};',
'} // namespace rocksdb',
),
'db/compaction/compaction_job.h': lines(
'#include "db/compaction/compaction_state.h"',
'namespace rocksdb {',
'class CompactionJob {',
' public:',
' void RemoveEmptyOutputs();',
' private:',
' CompactionState* compact_;',
'};',
'} // namespace rocksdb',
),
'db/compaction/compaction_job.cc': lines(
'#include "db/compaction/compaction_job.h"',
'namespace rocksdb {',
'void CompactionJob::RemoveEmptyOutputs() {',
' for (auto& state : compact_->sub_compact_states) {',
' state.RemoveLastEmptyOutput();',
' }',
'}',
'void Finish(const std::vector<CompactionState*>& compactions) {',
' for (const auto* compaction : compactions) {',
' for (auto& state : compaction->sub_compact_states) {',
' state.RemoveLastEmptyOutput();',
' }',
' }',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::CompactionJob::RemoveEmptyOutputs')).toEqual(['rocksdb::SubcompactionState::RemoveLastEmptyOutput @0.9']);
expect(calls(cg, 'rocksdb::Finish')).toEqual(['rocksdb::SubcompactionState::RemoveLastEmptyOutput @0.9']);
} finally {
cg.close();
}
});
});
describe('what an `auto` element calls when it is not the project\'s', () => {
const SLICE = {
'util/slice.h': lines(
'#include <cstddef>',
'namespace rocksdb {',
'class Slice {',
' public:',
' size_t size() const { return 0; }',
' const char* c_str() const { return ""; }',
'};',
'class TableReader {',
' public:',
' bool Valid() const { return true; }',
' void reset() {}',
'};',
'class TruncatedRangeDelIterator {',
' public:',
' bool Valid() const { return true; }',
'};',
'} // namespace rocksdb',
),
};
it('a standard-library element\'s own member links to nothing', async () => {
const cg = await indexed({
...SLICE,
'db/names.cc': lines(
'#include <string>',
'#include <thread>',
'#include <vector>',
'#include "util/slice.h"',
'namespace rocksdb {',
'size_t Total(const std::vector<std::string>& files) {',
' size_t total = 0;',
' for (const auto& file : files) {',
' total += file.size();',
' }',
' return total;',
'}',
'} // namespace rocksdb',
),
});
try {
// The guess took `Slice::size` by the method's name.
expect(calls(cg, 'rocksdb::Total')).toEqual([]);
} finally {
cg.close();
}
});
it('a smart pointer element: `->` reaches the type it holds, `.` the holder\'s own member', async () => {
const cg = await indexed({
...SLICE,
'db/range_del_aggregator.h': lines(
'#include <memory>',
'#include <vector>',
'#include "util/slice.h"',
'namespace rocksdb {',
'class RangeDelAggregator {',
' public:',
' bool IsRangeOverlapped();',
' void Reset();',
' private:',
' std::vector<std::unique_ptr<TruncatedRangeDelIterator>> iters_;',
'};',
'} // namespace rocksdb',
),
'db/range_del_aggregator.cc': lines(
'#include "db/range_del_aggregator.h"',
'namespace rocksdb {',
'bool RangeDelAggregator::IsRangeOverlapped() {',
' for (auto& iter : iters_) {',
' if (iter->Valid()) return true;',
' }',
' return false;',
'}',
'void RangeDelAggregator::Reset() {',
' for (auto& iter : iters_) iter.reset();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::RangeDelAggregator::IsRangeOverlapped')).toEqual(['rocksdb::TruncatedRangeDelIterator::Valid @0.9']);
// `std::unique_ptr::reset`, not TableReader's.
expect(calls(cg, 'rocksdb::RangeDelAggregator::Reset')).toEqual([]);
} finally {
cg.close();
}
});
it('an element a template argument decides is not read', async () => {
const cg = await indexed({
...SLICE,
'db/each.h': lines(
'#include <vector>',
'#include "util/slice.h"',
'namespace rocksdb {',
'template <class T>',
'bool AllValid(const std::vector<T*>& items) {',
' for (auto* item : items) {',
' if (!item->Valid()) return false;',
' }',
' return true;',
'}',
'} // namespace rocksdb',
),
});
try {
// T is whatever the caller iterates: no typed edge, as before.
expect(calls(cg, 'rocksdb::AllValid').filter((c) => c.endsWith('@0.9'))).toEqual([]);
} finally {
cg.close();
}
});
});