Files
codegraph/__tests__/cpp-member-chain-base-members.test.ts
T
Colby MchenryandClaude Opus 5.5 e2b0cf5eea fix(cpp): a call on an inherited member reached through a chain, like options.statistics->getTickerCount(…) on a RocksDB Options, links to the member's class (#2628)
Step 3 of member chains, after #2626 and #2627. The type of `x.member` was
read from x's class alone, so a member that class inherits was never found
and the call fell back to its bare name.

A temporary diagnostic on main measured why chain calls still fell through:
2,801 of 4,884 were a member declared in a base class. Nearly all were
rocksdb's `Options`, which declares no members of its own and inherits
`statistics`, `table_factory`, `listeners`, `prefix_extractor` and the rest
from `DBOptions` and `ColumnFamilyOptions`. The other causes are left for
later:
- a chain whose base can't be typed: 875;
- a class the index lacks, e.g. protobuf's generated `_impl_`: 626;
- a member found nowhere: 337.

cppExpressionType's member branch now uses cppInheritedDataMember, the
lookup the calling class's own members already use (#2533):
- the class's own declaration first;
- then the classes it derives from, through their base edges, to depth 4;
- a base that depends on the class's template parameters is skipped
  unless a using-declaration brings the name in.

Kit A/B against main e8ec7fd8:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| rocksdb | 1,410 | 634 | 392 | 184 |
| protobuf | 0 | 2 | 149 | 55 |
| leveldb, fmt, redis | identical | | | |

- Removed: the bare-name guesses on inherited std::shared_ptr and
  std::vector members, such as `options.table_factory.reset(…)` →
  FSWritableFilePtr::reset (654) and `options_.db_paths.emplace_back(…)` →
  autovector::emplace_back (256).
- Added: `options.statistics->getTickerCount(…)` → Statistics (432),
  `rate_limiter` → RateLimiter (51), `comparator` → Comparator (26).
- Retargeted: DummyOldStats -> Statistics, DB -> Env, ClockCacheShard -> Cache.
- protobuf's generated `this_._internal_metadata_.Clear<…>()`,
  `MergeFrom<…>` and `unknown_fields<…>` now reach InternalMetadata, a
  member every message inherits from MessageLite. Before, they reached the
  message's own `Clear` or `Any::MergeFrom`.

All 50 random sites read by hand (36 rocksdb, 14 protobuf) were correct.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:05:39 +00:00

132 lines
4.6 KiB
TypeScript

/**
* A C++ call on a data member that the object's class inherits, reached
* through a member chain, calls a method of the member's class. rocksdb's
* `Options` declares no members of its own: `options.statistics` and
* `options.table_factory` are `DBOptions`' and `ColumnFamilyOptions`'. Only
* the object's own class was read for the member, so such calls were guessed
* by the method's name.
*/
import { describe, it, expect, beforeAll, afterAll } 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 lines = (...l: string[]): string => [...l, ''].join('\n');
describe('a C++ call on a member chain through an inherited member', () => {
let root: string;
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-member-chain-base-'));
const write = (file: string, text: string): void => {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
};
// Each namesake comes first: a call matched by its bare name lands on it.
write('src/types.h', lines(
'class ArenaStats {',
' public:',
' long getTickerCount(int t) const { return 0; }',
'};',
'class Statistics {',
' public:',
' virtual long getTickerCount(int t) const { return 1; }',
'};',
'class BlockNamesake {',
' public:',
' const char* Name() const { return "block"; }',
'};',
'class TableFactory {',
' public:',
' virtual const char* Name() const { return "table"; }',
'};',
'class MemTableNamesake {',
' public:',
' void CreateMemTableRep() {}',
'};',
'class MemTableRepFactory {',
' public:',
' virtual void CreateMemTableRep() {}',
'};',
'class FilePtr {',
' public:',
' void reset() {}',
'};',
'class Other {',
' public:',
' void Flush() {}',
'};',
'class Inner {',
' public:',
' void Flush() {}',
'};',
));
write('src/options.h', lines(
'#include <memory>',
'#include "src/types.h"',
'struct DBOptions {',
' Statistics* statistics = nullptr;',
'};',
'struct AdvancedColumnFamilyOptions {',
' MemTableRepFactory* memtable_factory = nullptr;',
'};',
'struct ColumnFamilyOptions : public AdvancedColumnFamilyOptions {',
' std::shared_ptr<TableFactory> table_factory;',
'};',
'struct Options : public DBOptions, public ColumnFamilyOptions {};',
'struct Shadowing : public DBOptions {',
' Inner statistics;',
'};',
));
write('src/use.cc', lines(
'#include "src/options.h"',
'void Stats(Options& options) { options.statistics->getTickerCount(0); }',
'void Table(Options* options) { options->table_factory->Name(); }',
'void Memtable(const Options& opts) { opts.memtable_factory->CreateMemTableRep(); }',
'void HolderOwn(Options& options) { options.table_factory.reset(); }',
'void Shadowed(Shadowing& s) { s.statistics.Flush(); }',
'struct Holder {',
' Options opts;',
'};',
'void Deep(Holder* h) { h->opts.statistics->getTickerCount(1); }',
));
cg = await CodeGraph.init(root, { index: true });
}, 120_000);
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
/** `calls` callees of the function `name`, as `qualifiedName`. */
const calls = (name: string): string[] => {
const caller = cg.getNodesByKind('function').find((n: Node) => n.name === name);
expect(caller, name).toBeDefined();
return cg.getCallees(caller!.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName).sort();
};
it("a member the object's class inherits from either base", () => {
expect(calls('Stats')).toEqual(['Statistics::getTickerCount']);
expect(calls('Table')).toEqual(['TableFactory::Name']);
});
it('a member inherited through two levels', () => {
expect(calls('Memtable')).toEqual(['MemTableRepFactory::CreateMemTableRep']);
});
it("an inherited library member's own method calls none of the project's", () => {
expect(calls('HolderOwn')).toEqual([]);
});
it("the class's own member before a base's of the same name", () => {
expect(calls('Shadowed')).toEqual(['Inner::Flush']);
});
it('an inherited member further down a chain', () => {
expect(calls('Deep')).toEqual(['Statistics::getTickerCount']);
});
});