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 @@ -104,6 +104,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- 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.
- In C++, a call made with `->` on an `absl::StatusOr`, an `absl::optional` or an `absl::NoDestructor`, like protocolbuffers/protobuf's `resolver->MergeFeatures(…)` on an `absl::StatusOr<FeatureResolver>`, now links to the method of the class it holds, as one on a `std::unique_ptr` or `std::optional` does. A call written without a receiver in a class template now also finds a member of a base that depends on the template's parameters when the class brings it in with a using-declaration, like facebook/rocksdb's `using BasicTypedCacheHelperFns<TValue>::DownCastValue;`, and so does a call on such a member. 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
213 changes: 213 additions & 0 deletions __tests__/cpp-absl-holder-receiver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/**
* A C++ `->` call on an `absl::StatusOr`, an `absl::optional` or an
* `absl::NoDestructor` calls a member of the type it holds, as one on a
* `std::unique_ptr` or `std::optional` does; and a bare call in a class
* template finds a member its dependent base declares when the class brings
* the name in with a using-declaration.
*
* In protocolbuffers/protobuf, `absl::StatusOr<FeatureResolver> resolver =
* FeatureResolver::Create(…); resolver->MergeFeatures(…)` was guessed by the
* method's name: the declared type's last name, `StatusOr`, is no class of
* the project's. In facebook/rocksdb's typed_cache.h, `using
* BasicTypedCacheHelperFns<TValue>::DownCastValue;` makes a bare
* `DownCastValue(v)` the base's, which C++ doesn't look in otherwise: the
* base depends on the class's template parameters.
*/
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-absl-held-'));
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` callees of a function or method, as `qualifiedName @confidence`. */
function calls(cg: CodeGraph, caller: string): string[] {
return cg
.getCallees(callable(cg, caller).id)
.filter((r) => r.edge.kind === 'calls')
.map((r) => `${r.node.qualifiedName} @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`)
.sort();
}

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

// Two classes with a method of each name, so a guess by the name can miss.
const RESOLVERS = lines(
'namespace google {',
'namespace protobuf {',
'class FeatureResolver {',
' public:',
' int MergeFeatures(int parent, int child) const { return parent; }',
'};',
'class FeatureSetResolver {',
' public:',
' int MergeFeatures(int parent, int child) const { return child; }',
'};',
'} // namespace protobuf',
'} // namespace google',
);

describe('C++ absl holders and using-declared members of dependent bases', () => {
it("is the held class's method for `->` on an absl::StatusOr, absl::optional or absl::NoDestructor", async () => {
const cg = await indexed({
'src/feature_resolver.h': RESOLVERS,
'src/feature_resolver_test.cc': lines(
'#include "src/feature_resolver.h"',
'namespace google {',
'namespace protobuf {',
'int Merge(absl::StatusOr<FeatureResolver> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'int MergeOptional(absl::optional<FeatureResolver> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'int MergeStatic() {',
' static absl::NoDestructor<FeatureResolver> resolver;',
' return resolver->MergeFeatures(1, 2);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
for (const caller of ['Merge', 'MergeOptional', 'MergeStatic']) {
expect(calls(cg, `google::protobuf::${caller}`)).toEqual(['google::protobuf::FeatureResolver::MergeFeatures @0.9']);
}
} finally {
cg.close();
}
});

it("is not a project's own StatusOr, nor what a StatusOr of a pointer holds", async () => {
const cg = await indexed({
'src/feature_resolver.h': RESOLVERS,
// protobuf's hpb declares a `StatusOr` of its own.
'hpb/status.h': lines(
'namespace absl {',
'template <typename T>',
'class StatusOr {',
' public:',
' T value() const;',
'};',
'} // namespace absl',
),
'src/use.cc': lines(
'#include "src/feature_resolver.h"',
'#include "hpb/status.h"',
'namespace google {',
'namespace protobuf {',
'int Merge(absl::StatusOr<FeatureResolver> resolver) {',
' return resolver->MergeFeatures(1, 2);',
'}',
'} // namespace protobuf',
'} // namespace google',
),
});
try {
expect(calls(cg, 'google::protobuf::Merge').filter((c) => c.startsWith('google::protobuf::FeatureResolver::') && c.endsWith('@0.9'))).toEqual([]);
} finally {
cg.close();
}
});

it('is the member of the base the using-declaration names, of two dependent bases', async () => {
const cg = await indexed({
'util/runner.h': lines(
'namespace rocksdb {',
'template <class T>',
'class Second {',
' public:',
' void Run() {}',
'};',
'template <class T>',
'class First {',
' public:',
' void Run() {}',
'};',
'template <class T>',
'class Both : public Second<T>, public First<T> {',
' public:',
' using First<T>::Run;',
' void Go() { Run(); }',
'};',
'} // namespace rocksdb',
),
});
try {
expect(calls(cg, 'rocksdb::Both::Go')).toEqual(['rocksdb::First::Run @0.9']);
} finally {
cg.close();
}
});

it('is the dependent base member a using-declaration brings in, for a bare call and a data member', async () => {
const cg = await indexed({
// (The base in a header of its own, where the derived class's file doesn't write its member.)
'cache/typed_cache_base.h': lines(
'namespace rocksdb {',
'class Cache {',
' public:',
' int Insert(int key) { return key; }',
'};',
'class Other {',
' public:',
' int Insert(int key) { return 0; }',
'};',
'template <class TValue>',
'class BasicTypedCacheHelperFns {',
' public:',
' static TValue* DownCastValue(void* v) { return nullptr; }',
' protected:',
' Cache* cache_;',
'};',
'} // namespace rocksdb',
),
'cache/typed_cache.h': lines(
'#include "cache/typed_cache_base.h"',
'namespace rocksdb {',
'inline int* DownCastValue(void* v) { return nullptr; }',
'template <class TValue, class TCreateContext>',
'class FullTypedCacheHelperFns : public BasicTypedCacheHelperFns<TValue> {',
' public:',
' using BasicTypedCacheHelperFns<TValue>::DownCastValue;',
' using BasicTypedCacheHelperFns<TValue>::cache_;',
' static void Delete(void* v) {',
' DownCastValue(v);',
' }',
' int Store(int key) {',
' return cache_->Insert(key);',
' }',
'};',
'} // namespace rocksdb',
),
});
try {
// (Without the using-declarations C++ finds the namespace's `DownCastValue`, as before.)
expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Delete')).toEqual(['rocksdb::BasicTypedCacheHelperFns::DownCastValue @0.9']);
expect(calls(cg, 'rocksdb::FullTypedCacheHelperFns::Store')).toEqual(['rocksdb::Cache::Insert @0.9']);
} finally {
cg.close();
}
});
});
Loading