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 @@ -102,6 +102,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. Re-index C++ projects after upgrading.
- 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. 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.
- Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading.
- In a repository that mixes languages, a web framework's naming conventions now link only code written in that framework's own languages. Before, Express's also linked Go and Python code: in etcd a Go method's `*zap.Logger` result type linked to the method itself, and in Harbor a Python `self.jaeger.validate()` call linked to another class's `validate`. Links that cross languages on purpose, like React Native and Expo native modules, Swift and Objective-C, Drupal's `routing.yml`, SvelteKit's `$lib/…` imports in `.ts` files and a Razor page's `@model`, are unchanged.
- In C and C++, a call written from the global scope, like `::leveldb::RepairDB(…)` or `::hpb::CreateMessage(…)`, now links to the function of exactly that name, and one through a namespace alias, like protobuf's `::_pbi::…` calls, to the function in the aliased namespace. Before, these calls were dropped unless the repository also held Swift and Objective-C files, and even then a name spelled out in full, like `::google::protobuf::internal::TSanWrite(…)`, linked to nothing, while `::operator delete(…)` linked to one class's own `operator delete`. A global call never links to a function or method of the same name inside a namespace or class, such as a wrapper `open()` that calls the C library's `::open(…)`, and `::std::…` and `::testing::…` calls stay unlinked unless that library's source is part of your project. Re-index C and C++ projects after upgrading.
Expand Down
237 changes: 237 additions & 0 deletions __tests__/cpp-binding-and-wrapper-receivers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* A C++ call on a name a structured binding declares, or on what a
* `std::function` returns, links to the method of the type C++ gives it.
*
* facebook/rocksdb's MultiCfIteratorImpl writes `for (auto& [cfh, iter] :
* cfh_iter_pairs_) { iter->Valid(); … }` over a member declared, across two
* lines, as a `std::vector<std::pair<ColumnFamilyHandle*,
* std::unique_ptr<Iterator>>>`; a binding's names had no type, so the calls
* were guessed by the method's name. `iter` is the pair's second element, a
* `std::unique_ptr<Iterator>`, and `->` reaches `Iterator`'s method. A map's
* elements pair its key and value; a declaration's (`auto [a, b] = f();`)
* are what its initializer returns, a `std::pair` or `std::tuple`. And a
* call through a `std::function<Iterator*()>`, or the other wrappers, has
* the type the function type returns.
*/
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-binding-'));
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` edges of a function or method, as `qualifiedName @confidence`, one per call. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getOutgoingEdgesFrom([callable(cg, caller).id], ['calls'])
.map((e) => `${cg.getNode(e.target)?.qualifiedName} @${(e.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}

const lines = (...text: string[]): string => [...text, ''].join('\n');

// `Valid` is also a namesake class's, so a guess by the name can miss.
const ITERATORS = lines(
'#include <memory>',
'namespace rocksdb {',
'class ColumnFamilyHandle {};',
'class IteratorBase {',
' public:',
' virtual bool Valid() const { return false; }',
'};',
'class Iterator : public IteratorBase {};',
'class MemTableIterator {',
' public:',
' bool Valid() const { return true; }',
'};',
'} // namespace rocksdb',
);

describe('C++ structured bindings and function wrappers as receivers', () => {
it("is the pair element a range-for's binding names, over a member declared across two lines", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/multi_cf_iterator_impl.h': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'class MultiCfIteratorImpl {',
' public:',
' void SeekToFirst() {',
' for (auto& [cfh, iter] : cfh_iter_pairs_) {',
' iter->Valid();',
' }',
' }',
' private:',
' std::vector<std::pair<ColumnFamilyHandle*, std::unique_ptr<Iterator>>>',
' cfh_iter_pairs_;',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::MultiCfIteratorImpl::SeekToFirst')).toEqual(['rocksdb::IteratorBase::Valid @0.9']);
} finally {
cg.close();
}
});

it("is a map's value, and a declaration's pair or tuple element", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/use.cc': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'std::pair<ColumnFamilyHandle*, Iterator*> MakePair() { return {nullptr, nullptr}; }',
'std::tuple<int, Iterator*, bool> MakeTuple() { return {0, nullptr, false}; }',
'void ByName(const absl::flat_hash_map<std::string, Iterator*>& iters) {',
' for (const auto& [name, iter] : iters) {',
' iter->Valid();',
' }',
'}',
'void ByPair() {',
' auto [cfh, iter] = MakePair();',
' iter->Valid();',
'}',
'void ByTuple() {',
' const auto& [count, iter, done] = MakeTuple();',
' iter->Valid();',
'}',
'} // namespace rocksdb',
),
});
try {
for (const caller of ['ByName', 'ByPair', 'ByTuple']) {
expect(calls(cg, `rocksdb::${caller}`).filter((c) => c.includes('Valid'))).toEqual(['rocksdb::IteratorBase::Valid @0.9']);
}
} finally {
cg.close();
}
});

it("is what a std::function's function type returns, called or as an auto local's initializer", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/scan.cc': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'void Scan(std::function<Iterator*()> create_iterator) {',
' create_iterator()->Valid();',
' auto* it = create_iterator();',
' it->Valid();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Scan')).toEqual(['rocksdb::IteratorBase::Valid @0.85', 'rocksdb::IteratorBase::Valid @0.9']);
} finally {
cg.close();
}
});

it("gives no type from another function's binding, or a template's arguments that aren't a pair's", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/use.cc': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'MemTableIterator* iter = nullptr;',
'std::pair<ColumnFamilyHandle*, Iterator*> MakePair() { return {nullptr, nullptr}; }',
'// Members in another order than the template parameters.',
'template <class A, class B> struct Duo { B second; A first; };',
'Duo<Iterator*, MemTableIterator*> MakeDuo() { return {}; }',
'void Earlier() {',
' auto [cfh, iter] = MakePair();',
'}',
'void Later() {',
' iter->Valid();',
'}',
'void Unpack() {',
' auto [p, q] = MakeDuo();',
' p->Valid();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Later').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]);
expect(calls(cg, 'rocksdb::Unpack').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]);
} finally {
cg.close();
}
});

it("reads a member's type from the line above only when that line is a type", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/reader.h': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'class Reader {',
' public:',
' bool Check() const { return it_->Valid(); }',
' private:',
' bool flag_ = true;',
' // A default member initializer continued on the next line.',
' bool ready_ = flag_ &&',
' it_;',
' MemTableIterator* it_ = nullptr;',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Reader::Check')).toEqual(['rocksdb::MemTableIterator::Valid @0.9']);
} finally {
cg.close();
}
});

it("gives no type for a call outside the loop, or a binding of something that isn't a pair or tuple", async () => {
const cg = await indexed({
'include/rocksdb/iterator.h': ITERATORS,
'db/use.cc': lines(
'#include "include/rocksdb/iterator.h"',
'namespace rocksdb {',
'struct Point { Iterator* first; Iterator* second; };',
'void After(const std::vector<std::pair<ColumnFamilyHandle*, Iterator*>>& pairs, MemTableIterator* iter) {',
' for (const auto& [cfh, iter] : pairs) {',
' }',
' iter->Valid();',
'}',
'void Aggregate(Point point) {',
' auto [x, y] = point;',
' y->Valid();',
'}',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::After').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]);
expect(calls(cg, 'rocksdb::Aggregate').filter((c) => c === 'rocksdb::IteratorBase::Valid @0.9')).toEqual([]);
} finally {
cg.close();
}
});
});
Loading