Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic<bool> shutting_down_`, no longer links to a project method that only shares the method's name. A call on a member reached through other members now links the same way, like RocksDB's `thread->rand.Next()` on a `Random64` or `meta->largest.user_key()` on an `InternalKey`. Before, it went to whichever class had a method of that name, like `ArenaWrappedDBIter::Next` or `ClippingIterator::user_key`, or, inside a method, to that method's own class. When the member's class has no such method, the call no longer links to another class's. The member is read from the class the calling file sees: RocksDB's `db_bench_tool.cc` and its stress tool each declare their own `ThreadState`, whose `rand` is a `Random64` in one and a `Random` in the other. It is also found in the classes that class derives from. RocksDB's `Options` inherits its members from `DBOptions` and `ColumnFamilyOptions`, so `options.statistics->getTickerCount(…)` now reaches `Statistics`, and `options.table_factory.reset(…)` on a `std::shared_ptr` no longer links to a project method of that name. Re-index C++ projects after upgrading.
- In C++, a member declared inside an anonymous union or struct is now found as a member of the class around it, as C++ looks it up. Every protocolbuffers/protobuf generated message keeps its fields in `union { Impl_ _impl_; };`, so calls like `_impl_.name_.Set(…)` now link to `ArenaStringPtr::Set`. Before, they were guessed by the method's name, and `this_._impl_.type_url_.Destroy()` linked to `Arena::Destroy`. The same goes for fmtlib/fmt's `value`, which keeps its argument in a union. Re-index C++ projects after upgrading.
- In C++, a type a header names for a class its source file defines, like the `Rep` of a pimpl's `std::unique_ptr<Rep> rep_` or `Rep* rep_`, is now found for the code in that source file, so calls through it link to the right classes. facebook/rocksdb's `rep->write_batch.Put(…)` in `WriteBatchWithIndex` now reaches `WriteBatch::Put`, google/leveldb's `rep_->index_block->NewIterator(…)` reaches `Block::NewIterator` instead of the calling `Table`'s own, and googletest's `impl()->total_test_count()` reaches `UnitTestImpl`. A call on a standard-library member of such a class, like protocolbuffers/protobuf's `tables_->known_bad_files_.clear()`, no longer links to a project method of that name. Re-index C++ projects after upgrading.
- In C++, a `->` on a value of one of your own classes now follows that class's `operator->`, or its `operator*`, as C++ does, so calls through your iterators and smart-pointer wrappers link to the right classes. facebook/rocksdb's `fs_->NewWritableFile(…)` on a `FileSystemPtr` now reaches `FileSystem::NewWritableFile` instead of a test file system's, and `iter->lkey->user_key()` through a `MultiGetContext::Range::Iterator` reaches `LookupKey::user_key`. A class template's `T* operator->()` is read for the type it's given, like `UnownedPtr<DB>`'s. Re-index C++ projects after upgrading.
- In C++, a variable, parameter or member whose declaration puts a qualifier or an annotation around its name is now read for its type, so calls on it link to the declared class's methods: a `const` after the `*` or `&`, like facebook/rocksdb's `Iterator* const iter = iter_guard.get();` or google/leveldb's `Env* const env_;`, a type written before its `const`, like `Slice const& key`, a nullability annotation after the `*`, like protocolbuffers/protobuf's `io::EpsCopyOutputStream* PROTOBUF_NONNULL stream`, and a thread-safety annotation after the name, like LevelDB's `MemTable* imm_ GUARDED_BY(mutex_);`. Before, such a declaration was skipped, or the annotation was taken for its type, so the call took the type of an earlier variable of the same name or was guessed by the method's name: RocksDB's `iter->SeekToFirst()` reached the unrelated `ArenaWrappedDBIter`, and LevelDB's `env_->NewWritableFile(…)` reached `InMemoryEnv`. Re-index C++ projects after upgrading.
- In C++, a call on a name a structured binding declares now links to the method of the type C++ gives it. That covers a range-based `for` over pairs or a map, like facebook/rocksdb's `for (auto& [cfh, iter] : cfh_iter_pairs_) { iter->Valid(); }`, and a declaration like protocolbuffers/protobuf's `auto [dst_ext, is_new] = Insert(…);`. A call on what a `std::function` or another function wrapper returns, like `create_iterator()->Valid()` on a `std::function<Iterator*()>`, links the same way, and so does a call on a class member whose type is written on the line above its name. Before, these calls were guessed by the method's name, and calls on a library container declared that way, like a `std::priority_queue` member's `pop()`, reached a project class's method of that name. Re-index C++ projects after upgrading.
- In C++, a call written without a receiver in a gtest `TEST_F` or `TEST_P` body now links to the test fixture's method, its own or one it inherits, as C++ finds it: facebook/rocksdb's tests call `DBTestBase`'s `Put(…)`, `Flush()` and `NewIterator(…)` this way. A variable such a call initializes, like `auto iter = NewIterator(…)`, takes the type the method returns, so `iter->SeekToFirst()` reaches the iterator class. Before, those calls were guessed by name and often reached a namesake in another class, like RocksDB's `ColumnFamilyTestBase::Put` or protocolbuffers/protobuf's `EncodeDecodeTest::ExpectNoErrors`, and `iter->SeekToFirst()` reached the unrelated `MemTableRep::Iterator`. A `->` call on a value of a type from outside the project, like protobuf's `m->value()` on an `absl::StatusOr<Any>`, no longer links to a class of the project that shares the type's name, like protobuf's own `hpb::StatusOr`. Re-index C++ projects after upgrading.
Expand Down
156 changes: 156 additions & 0 deletions __tests__/cpp-operator-arrow-targets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* 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([]);
});
});
60 changes: 57 additions & 3 deletions src/resolution/name-matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14610,16 +14610,70 @@ function cppArrowTarget(

/**
* The element a container's iterator of the type `iterator` points at, read
* from the container's template arguments (cppArrowTarget). Null for any
* other type, and for an element a class template's own `begin()` or
* from the container's template arguments (cppArrowTarget), or what `->` on
* a value of one of the project's classes reaches (cppOperatorTarget). Null
* for any other type, and for an element a class template's own `begin()` or
* `operator*` writes with its template parameter.
*/
function cppIteratorTarget(iterator: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null {
const container = CPP_ITERATOR_OF.exec(cppWithoutTypeKeywords(iterator.text))?.[1];
const element = container ? cppRangeElementOf({ ...iterator, text: container }, at, context, 1) : null;
const element = container ? cppRangeElementOf({ ...iterator, text: container }, at, context, 1) : cppOperatorTarget(iterator, at, context);
return element && (element.scope === iterator.scope || !isCppTemplateParameterType(element)) ? element : null;
}

/**
* What `->` on a value of one of the project's classes of the type `type`
* reaches, as C++ reads `x->m`: what the class's `operator->` returns,
* through the pointer it is, or with `->` applied again to a class it is (a
* library holder's held type, or another of these); else what its
* `operator*` returns. The return type is read where the class declares it,
* with the class's own template parameters bound to the arguments `type` is
* written with (cppTemplateBound). rocksdb's `MultiGetContext::Range::Iterator`
* declares `KeyContext* operator->()`, its `UnownedPtr<DB>` a `T*
* operator->()`, a `DB*`, and its `FileSystemPtr` returns a
* `std::shared_ptr<FileSystem>`. Null when the class declares neither, or
* what it returns can't be read.
*/
function cppOperatorTarget(type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext, hops = 0): CppWrittenType | null {
const written = cppWithoutTypeKeywords(type.text);
if (hops > 2 || !written || isCppPointerType(written) || isCppLibraryType(written, context)) return null;
const found = cppClassWritten(written, type, at, context);
if (!found?.exact || !CPP_CLASS_KINDS.has(found.node.kind)) return null;
const arrow = cppMethodsOf(found.node, 'operator->', at, context);
const returned = cppReturnTypeOf(arrow.length > 0 ? arrow : cppMethodsOf(found.node, 'operator*', at, context), context);
const bound = returned && cppTemplateBound(returned, found.node, { ...type, text: written }, at, context);
if (!bound) return null;
if (arrow.length === 0) return { ...bound, text: bound.text.replace(/&+\s*$/, '').trim() };
return cppPointee(bound) ?? cppOperatorTarget(bound, at, context, hops + 1);
}

/**
* The type `returned`, which the class template `cls` writes, with `cls`'s
* own template parameters bound to the arguments of `type`, a specialization
* of it: the `T*` of `UnownedPtr<T>` is a `DB*` for an `UnownedPtr<DB>`. An
* alias the class declares (`pointer`) is followed first, and an argument is
* looked up where `type` is written. A type of a class that is no template
* comes back as written; null for a parameter `type` doesn't bind.
*/
function cppTemplateBound(returned: CppWrittenType, cls: Node, type: CppWrittenType, at: UnresolvedRef, context: ResolutionContext): CppWrittenType | null {
const params = cppOwnTemplateParameters(cls, context);
if (params.length === 0) return returned;
const args = cppTemplateArguments(type.text) ?? [];
const followed = cppFollowWrittenAliases(returned, at, context) ?? returned;
const core = followed.text.replace(/\b(?:const|volatile|typename)\b|[*&]/g, ' ').trim();
const index = params.indexOf(core);
if (index >= 0) {
const arg = args[index]?.trim();
return arg ? { ...type, text: followed.text.replace(new RegExp(`\\b${core}\\b`), arg) } : null;
}
let text = followed.text;
params.forEach((param, i) => {
const arg = args[i]?.trim();
if (param && arg) text = text.replace(new RegExp(`\\b${param}\\b`, 'g'), arg);
});
return { ...followed, text };
}

/**
* What dereferencing a value of the type `type` gives, for `*x` and `x->`:
* an array's first element, what a pointer points at or a holder holds
Expand Down