Files
codegraph/__tests__/cpp-operator-arrow-targets.test.ts
T
Colby MchenryandClaude Opus 5.5 0c28480576 fix(cpp): -> on a value of one of the project's classes follows its operator->, like rocksdb's FileSystemPtr and MultiGetContext::Range::Iterator (#2735)
C++ reads `x->m` on a class value as `x.operator->()->m`, and applies
`->` again to a class it returns. Only a library holder's `->` and a
container iterator's (`std::vector<T>::iterator`) were read. A call
through a project iterator or smart-pointer wrapper was guessed by the
method's name:
- rocksdb's `fs_->NewWritableFile(…)` on a `FileSystemPtr`, whose
  `operator->` returns a `std::shared_ptr<FileSystem>`, reached
  `FaultInjectionTestFS`;
- `writable_file_->Truncate(…)` on an `FSWritableFilePtr` reached the
  legacy `WritableFile::Truncate`;
- `iter->lkey->user_key()` through `MultiGetContext::Range::Iterator`,
  whose `KeyContext* operator->()` is a `KeyContext*`, reached
  `BlobCountingIterator::user_key`.

Both a member chain's `->` and a `->` call on a declared receiver end in
cppIteratorTarget. For a type that is no container's iterator, it now
falls back to cppOperatorTarget:
- what the class's `operator->` returns, through the pointer it is, or
  with `->` applied again to a class it is: a library holder, or another
  of these;
- else what its `operator*` returns;
- the return type is read where the class declares it, and the class's
  own template parameters are bound to the type's written arguments
  (cppTemplateBound): `UnownedPtr<DB>`'s `T* operator->()` is a `DB*`;
- a parameter the type doesn't bind names no particular class.

A log-only census on main bdf288a7 found 304 rocksdb and 42 protobuf
dereferences of a project class with an `operator->` that gave no
element. Among the rocksdb calls, 77 were bare-name guesses and 56 were
unlinked.

Kit A/B against bdf288a7:

| repo | removed | added | retargeted | same target, now typed |
|---|---|---|---|---|
| rocksdb | 2 | 49 | 158 | 89 |
| protobuf | 0 | 0 | 7 | 3 |
| leveldb, fmt, redis | identical | | | |

- Retargeted:
  - FaultInjectionTestFS → FileSystem (57), WritableFile → FSWritableFile
    (32), and TruncatedRangeDelIterator through UnownedPtr (16);
  - LookupKey and GetContext through MultiGetContext's iterator;
  - SequentialFile and RandomAccessFile → FSSequentialFile and
    FSRandomAccessFile;
  - protobuf's `map_field_->` calls from WeakRepeatedPtrField to the map
    field bases;
  - overloads (DB::Put, PinnableSlice) chosen on the now-known class.
- Removed: `iter->timestamp->assign(…)` on a `std::string*`, which had
  reached `autovector::assign`.
- Added: Status::PermitUncheckedError, FileSystem::OptimizeFor…,
  Cleanable::RegisterCleanup, and gtest's ParamGeneratorInterface through
  linked_ptr.
- About 40 changed sites read by hand were all right.

Mutations: dropping the fallback, the operator* fallback, the second
`->` or the template binding each fail the new tests. Accepting an
inexact class, found only as the sole one of its name, survives; the
guard stays, as in the member-chain matcher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 08:16:26 +00:00

157 lines
5.1 KiB
TypeScript

/**
* A C++ `->` on a value of one of the project's classes calls that class's
* `operator->`, and the member is looked up on what it returns: rocksdb's
* `MultiGetContext::Range::Iterator` declares `KeyContext* operator->()`, its
* `UnownedPtr<T>` a `T* operator->()`, and its `FileSystemPtr` returns a
* `std::shared_ptr<FileSystem>`, which `->` is applied to again. Only a
* library holder's or container iterator's `->` was read, so such calls were
* guessed by the method's name: `iter->lkey->user_key()` reached
* `BlobCountingIterator::user_key`.
*/
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++ `->` through a project class's operator->", () => {
let root: string;
let cg: CodeGraph;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-operator-arrow-'));
const write = (file: string, text: string): void => {
fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
fs.writeFileSync(path.join(root, file), text);
};
write('src/types.h', lines(
'#include <memory>',
'namespace ns {',
'class LookupKey {',
' public:',
' int user_key() const { return 0; }',
'};',
'struct KeyContext {',
' LookupKey* k;',
'};',
'class Range {',
' public:',
' class Iterator {',
' public:',
' KeyContext* operator->() const { return ctx_; }',
' KeyContext& operator*() const { return *ctx_; }',
' private:',
' KeyContext* ctx_;',
' };',
' Iterator begin() const { return Iterator(); }',
'};',
'class Cursor {',
' public:',
' KeyContext& operator*() const { return *ctx_; }',
' private:',
' KeyContext* ctx_;',
'};',
'template <typename T>',
'class UnownedPtr {',
' public:',
' T* operator->() const { return p_; }',
' private:',
' T* p_;',
'};',
'class DB {',
' public:',
' void Flush() {}',
'};',
'class Handle {',
' public:',
' UnownedPtr<DB> operator->() const { return p_; }',
' private:',
' UnownedPtr<DB> p_;',
'};',
'class FileSystem {',
' public:',
' void Sync() {}',
'};',
'class FileSystemPtr {',
' public:',
' const std::shared_ptr<FileSystem>& operator->() const { return fs_; }',
' private:',
' std::shared_ptr<FileSystem> fs_;',
'};',
'} // namespace ns',
));
write('src/use.cc', lines(
'#include "src/types.h"',
'namespace ns {',
// Each namesake sits where a call matched by its bare name lands on it.
'class KeyNamesake {',
' public:',
' int user_key() const { return 1; }',
'};',
'class FlushNamesake {',
' public:',
' void Flush() {}',
'};',
'class SyncNamesake {',
' public:',
' void Sync() {}',
'};',
'int Chain(const Range& range) {',
' auto i = range.begin();',
' return i->k->user_key();',
'}',
'int Star(const Cursor& c) {',
' auto& kc = *c;',
' return kc.k->user_key();',
'}',
'void Template(UnownedPtr<DB> h) { h->Flush(); }',
'void Holder(FileSystemPtr h) { h->Sync(); }',
'void Twice(Handle h) { h->Flush(); }',
'template <typename T>',
'void Unbound(UnownedPtr<T> p) { p->Flush(); }',
'} // namespace ns',
));
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 `qualifiedName`, as `qualifiedName`. */
const calls = (qualifiedName: string): string[] => {
const caller = cg.getNodesByKind('function').find((n: Node) => n.qualifiedName === qualifiedName);
expect(caller, qualifiedName).toBeDefined();
return cg.getCallees(caller!.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.qualifiedName)
.filter((q) => !q.endsWith('::begin')).sort();
};
it("a chain through an iterator class's operator->", () => {
expect(calls('ns::Chain')).toEqual(['ns::LookupKey::user_key']);
});
it("a chain through a class's operator*, when it has no operator->", () => {
expect(calls('ns::Star')).toEqual(['ns::LookupKey::user_key']);
});
it("a class template's operator-> returning its parameter, bound by the written argument", () => {
expect(calls('ns::Template')).toEqual(['ns::DB::Flush']);
});
it('an operator-> returning a library holder is applied again', () => {
expect(calls('ns::Holder')).toEqual(['ns::FileSystem::Sync']);
});
it('a class an operator-> returns has its own operator-> applied', () => {
expect(calls('ns::Twice')).toEqual(['ns::DB::Flush']);
});
it("a parameter the written type doesn't bind names no particular class", () => {
expect(calls('ns::Unbound')).toEqual([]);
});
});