mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
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 mainbdf288a7found 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 againstbdf288a7: | 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>
157 lines
5.1 KiB
TypeScript
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([]);
|
|
});
|
|
});
|