mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
fix(cpp): a call or type name never links to what only another source file can see (#2538)
Name matching linked C and C++ names to declarations no other
translation unit can name: a class or alias a `.cc` file declares for
itself, its members, and what a source file's unnamed namespace
declares. In rocksdb, other test files' bare `Put(…)`, `Flush()` and
`dbfull()` reached column_family_test.cc's `ColumnFamilyTestBase`
thousands of times, and inlineskiplist.h's `Node*` receivers reached the
`Node` hash_linklist_rep.cc declares in `namespace { … }`.
- isVisibleAcrossFiles rejects such a candidate for a C or C++ ref
(isCppTranslationUnitLocal), as it does a `static` C function: a type
only source files declare; a member written inside such a class; an
out-of-line member of a type the member's own file sees only source
files declare (a scope its file sees no type of may be a namespace:
fmt's posix-mock-test.cc defines posix-mock.h's `test::close` beside
ostream-test.cc's own `struct test`); and, for the rest, what an
unnamed namespace of a source file holds. An alias of the scope keeps
the member, since the class may be declared anywhere.
- A source file's declarations are still seen by the file itself, by a
file that includes it, and by a header it includes below them
(rocksdb's version_set.cc includes version_set_sync_and_async.h after
its `FilePicker` classes).
- The unnamed-namespace spans come from a scanner that skips comments,
preprocessor lines (continued past a CRLF too) and string, character
and raw string literals.
- The typed lookups (cppMethodOf, resolveMethodOnType) take the
candidates the call's file can see first (cppVisibleFirst): rocksdb's
db_stress_shared_state.h and two benchmark `.cc` files each declare a
`SharedState`, and a call in db_stress_common.cc is the header's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- 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.
|
||||
- In C and C++, a call or a type name no longer links to something only another source file can see: a class, struct or alias a `.c` or `.cc` file declares for itself, its members, or anything inside that file's unnamed `namespace { … }`. In facebook/rocksdb, other test files' bare `Put(…)`, `Flush()` and `dbfull()` calls had linked to the methods of a test fixture declared in `column_family_test.cc`, and calls on an `Env*` had linked to a test-only environment class in `memenv.cc`. When a header and a source file each declare a class of the same name, like RocksDB's `SharedState`, a call now links to the header's. A source file's own declarations are still found from the file itself, from a file that `#include`s it, and from a header it includes below them. Re-index C and 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.
|
||||
|
||||
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* What a C or C++ source file declares for itself is no target of a name
|
||||
* written in another file.
|
||||
*
|
||||
* A class or alias a `.cc` file declares, and whatever its unnamed namespace
|
||||
* (`namespace { … }`) declares, belongs to that translation unit: another
|
||||
* file can't name it. Name matching took them anyway. In facebook/rocksdb,
|
||||
* other test files' bare `Put(…)` reached column_family_test.cc's own
|
||||
* `ColumnFamilyTestBase::Put` 7,989 times, and inlineskiplist.h's `Node*`
|
||||
* receivers reached the `Node` hash_linklist_rep.cc declares in an unnamed
|
||||
* namespace. A source file's declarations are still seen by the file itself,
|
||||
* by a file that includes it, and by a header it includes after them
|
||||
* (rocksdb's version_set.cc includes version_set_sync_and_async.h below its
|
||||
* `FilePicker` classes).
|
||||
*/
|
||||
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-tu-'));
|
||||
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 });
|
||||
}
|
||||
|
||||
/** The function or method `qualifiedName`, the one in `file` when given. */
|
||||
function callable(cg: CodeGraph, qualifiedName: string, file?: string): Node {
|
||||
const node = [...cg.getNodesByKind('method'), ...cg.getNodesByKind('function')].find(
|
||||
(n) => n.qualifiedName === qualifiedName && (file === undefined || n.filePath === file),
|
||||
);
|
||||
if (!node) throw new Error(`no function or method ${qualifiedName}`);
|
||||
return node;
|
||||
}
|
||||
|
||||
/** `calls` callees of a function or method, as `qualifiedName [file] @confidence`. */
|
||||
function calls(cg: CodeGraph, caller: string, file?: string): string[] {
|
||||
return cg
|
||||
.getCallees(callable(cg, caller, file).id)
|
||||
.filter((r) => r.edge.kind === 'calls')
|
||||
.map((r) => `${r.node.qualifiedName} [${r.node.filePath}] @${(r.edge.metadata as { confidence?: number } | undefined)?.confidence}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
const lines = (...text: string[]): string => [...text, ''].join('\n');
|
||||
|
||||
describe('C/C++ declarations local to another translation unit', () => {
|
||||
it("is not another source file's class in an unnamed namespace", async () => {
|
||||
const cg = await indexed({
|
||||
'memtable/hash_linklist_rep.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'namespace {',
|
||||
'struct Node {',
|
||||
' Node* Next() { return nullptr; }',
|
||||
'};',
|
||||
'} // namespace',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'memtable/inlineskiplist.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'template <class Comparator>',
|
||||
'class InlineSkipList {',
|
||||
' private:',
|
||||
' struct Node;',
|
||||
' public:',
|
||||
' void Insert() {',
|
||||
' Node* x = head_;',
|
||||
' x->Next();',
|
||||
' }',
|
||||
' private:',
|
||||
' Node* head_;',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::InlineSkipList::Insert').filter((c) => c.includes('hash_linklist_rep.cc'))).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is not another test file's fixture method a bare call names", async () => {
|
||||
const cg = await indexed({
|
||||
'db/column_family_test.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'class ColumnFamilyTestBase {',
|
||||
' public:',
|
||||
' void Put(int key) {}',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db/db_basic_test.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'void Put(int key);',
|
||||
'void Check() {',
|
||||
' Put(1);',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::Check').filter((c) => c.includes('column_family_test.cc'))).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is not a function another source file's unnamed namespace declares", async () => {
|
||||
const cg = await indexed({
|
||||
'conformance/matchers.cc': lines(
|
||||
'namespace conformance {',
|
||||
'namespace {',
|
||||
'int RawPayload(int response) { return response; }',
|
||||
'} // namespace',
|
||||
'} // namespace conformance',
|
||||
),
|
||||
'conformance/matchers_test.cc': lines(
|
||||
'namespace conformance {',
|
||||
'int Describe() {',
|
||||
' return RawPayload(1);',
|
||||
'}',
|
||||
'} // namespace conformance',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'conformance::Describe').filter((c) => c.includes('matchers.cc'))).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is a member of the header's class, not of a source file's namesake", async () => {
|
||||
// rocksdb declares a `SharedState` in db_stress_shared_state.h and two
|
||||
// more of its own in cache_bench_tool.cc and db_bench_tool.cc.
|
||||
const cg = await indexed({
|
||||
'cache/cache_bench_tool.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'class SharedState {',
|
||||
' public:',
|
||||
' int* GetMutex() { return nullptr; }',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db_stress_tool/db_stress_shared_state.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'class SharedState {',
|
||||
' public:',
|
||||
' int* GetMutex() { return nullptr; }',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db_stress_tool/db_stress_common.cc': lines(
|
||||
'#include "db_stress_tool/db_stress_shared_state.h"',
|
||||
'namespace rocksdb {',
|
||||
'void Lock(SharedState* shared) {',
|
||||
' shared->GetMutex();',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::Lock')).toEqual(['rocksdb::SharedState::GetMutex [db_stress_tool/db_stress_shared_state.h] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is the header's class for a receiver a namespace-scope variable holds", async () => {
|
||||
const cg = await indexed({
|
||||
'cache/cache_bench_tool.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'class SharedState {',
|
||||
' public:',
|
||||
' int* GetMutex() { return nullptr; }',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db_stress_tool/db_stress_shared_state.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'class SharedState {',
|
||||
' public:',
|
||||
' int* GetMutex() { return nullptr; }',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db_stress_tool/db_stress_common.cc': lines(
|
||||
'#include "db_stress_tool/db_stress_shared_state.h"',
|
||||
'namespace rocksdb {',
|
||||
'SharedState* shared_state = nullptr;',
|
||||
'void LockShared() {',
|
||||
' shared_state->GetMutex();',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::LockShared')).toEqual(['rocksdb::SharedState::GetMutex [db_stress_tool/db_stress_shared_state.h] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is a header's class, with no include the index follows", async () => {
|
||||
const cg = await indexed({
|
||||
'include/rocksdb/cache.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'class Cache {',
|
||||
' public:',
|
||||
' virtual int Insert(int key) { return key; }',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'cache/user.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'int Use(Cache* cache) {',
|
||||
' return cache->Insert(1);',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::Use')).toEqual(['rocksdb::Cache::Insert [include/rocksdb/cache.h] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is not an out-of-line method of a class another source file declares", async () => {
|
||||
// protobuf's descriptor.cc declares `FileDescriptorTables` outside its
|
||||
// unnamed namespaces and defines the class's methods below it.
|
||||
const cg = await indexed({
|
||||
'src/descriptor.cc': lines(
|
||||
'namespace google {',
|
||||
'namespace protobuf {',
|
||||
'class FileDescriptorTables {',
|
||||
' public:',
|
||||
' const int* FindFieldByLowercaseName(const char* name) const;',
|
||||
'};',
|
||||
'const int* FileDescriptorTables::FindFieldByLowercaseName(const char* name) const {',
|
||||
' return nullptr;',
|
||||
'}',
|
||||
'} // namespace protobuf',
|
||||
'} // namespace google',
|
||||
),
|
||||
'python/message.cc': lines(
|
||||
'namespace google {',
|
||||
'namespace protobuf {',
|
||||
'namespace python {',
|
||||
'const int* Find(CMessage* self, const char* name) {',
|
||||
' return self->message_descriptor->FindFieldByLowercaseName(name);',
|
||||
'}',
|
||||
'} // namespace python',
|
||||
'} // namespace protobuf',
|
||||
'} // namespace google',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'google::protobuf::python::Find')).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('is not a class another source file declares, named as a type', async () => {
|
||||
// rocksdb's db/log_test.cc declares a `StringSource` of its own; other
|
||||
// tests' `new test::StringSource(…)` are test_util's.
|
||||
const cg = await indexed({
|
||||
'db/log_test.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'namespace log {',
|
||||
'class StringSource {',
|
||||
' public:',
|
||||
' explicit StringSource(int contents) {}',
|
||||
'};',
|
||||
'} // namespace log',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db/plain_table_db_test.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'void* Make() {',
|
||||
' return new test::StringSource(1);',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
const make = callable(cg, 'rocksdb::Make');
|
||||
expect(cg.getOutgoingEdgesFrom([make.id]).map((e) => cg.getNode(e.target)?.filePath).filter((f) => f === 'db/log_test.cc')).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('reads an unnamed namespace past comments, preprocessor lines and literals that spell one', async () => {
|
||||
const cg = await indexed({
|
||||
'db/c.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'// namespace { in a line comment',
|
||||
'/* namespace { in a block comment */',
|
||||
'#define ROCKSDB_ANONYMOUS_BEGIN namespace {',
|
||||
'const char* kDoc = "namespace {";',
|
||||
'const char* kRaw = R"(a "b namespace {" c)";',
|
||||
'int Helper(int x) { return x; }',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
// A macro continued onto the next line, in a file checked out with CRLF.
|
||||
'db/crlf.cc': ['namespace rocksdb {', '#define ROCKSDB_ANONYMOUS_BEGIN \\', ' namespace {', 'int Other(int x) { return x; }', '} // namespace rocksdb', ''].join('\r\n'),
|
||||
'db/c_test.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'int Check() {',
|
||||
' return Helper(1) + Other(2);',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::Check')).toEqual(['rocksdb::Helper [db/c.cc] @0.9', 'rocksdb::Other [db/crlf.cc] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("is still the file's own, and a header's it includes after the declaration", async () => {
|
||||
const cg = await indexed({
|
||||
'db/version_set.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'namespace {',
|
||||
'class FilePicker {',
|
||||
' public:',
|
||||
' int GetNextFile() { return 0; }',
|
||||
'};',
|
||||
'} // namespace',
|
||||
'int Pick(FilePicker& fp) {',
|
||||
' return fp.GetNextFile();',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
'#include "db/version_set_sync_and_async.h"',
|
||||
),
|
||||
'db/version_set_sync_and_async.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'int PickAsync(FilePicker& fp) {',
|
||||
' return fp.GetNextFile();',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::Pick')).toEqual(['rocksdb::FilePicker::GetNextFile [db/version_set.cc] @0.9']);
|
||||
expect(calls(cg, 'rocksdb::PickAsync')).toEqual(['rocksdb::FilePicker::GetNextFile [db/version_set.cc] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('is seen by a file that includes the source file', async () => {
|
||||
const cg = await indexed({
|
||||
'util/impl.cc': lines(
|
||||
'namespace {',
|
||||
'class Impl {',
|
||||
' public:',
|
||||
' int Run() { return 0; }',
|
||||
'};',
|
||||
'} // namespace',
|
||||
),
|
||||
'util/impl_test.cc': lines(
|
||||
'#include "util/impl.cc"',
|
||||
'int Test(Impl& impl) {',
|
||||
' return impl.Run();',
|
||||
'}',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'Test')).toEqual(['Impl::Run [util/impl.cc] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps an out-of-line method of a class a header declares, through an alias too', async () => {
|
||||
const cg = await indexed({
|
||||
'include/rocksdb/multi_scan.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'class MultiScan {',
|
||||
' public:',
|
||||
' class MultiScanIterator {',
|
||||
' public:',
|
||||
' void SeekCurrentRange();',
|
||||
' void Next() { SeekCurrentRange(); }',
|
||||
' };',
|
||||
'};',
|
||||
'class Table {',
|
||||
' public:',
|
||||
' int Open();',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db/multi_scan.cc': lines(
|
||||
'#include "include/rocksdb/multi_scan.h"',
|
||||
'namespace rocksdb {',
|
||||
'using MultiScanIterator = MultiScan::MultiScanIterator;',
|
||||
'void MultiScanIterator::SeekCurrentRange() {}',
|
||||
'int Table::Open() { return 0; }',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'db/open.cc': lines(
|
||||
'#include "include/rocksdb/multi_scan.h"',
|
||||
'namespace rocksdb {',
|
||||
'int OpenTable(Table* t) {',
|
||||
' return t->Open();',
|
||||
'}',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'rocksdb::MultiScan::MultiScanIterator::Next').filter((c) => c.includes('SeekCurrentRange'))).toHaveLength(1);
|
||||
expect(calls(cg, 'rocksdb::OpenTable')).toEqual(['rocksdb::Table::Open [db/multi_scan.cc] @0.9']);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a namespace's function a source file defines out of line, beside another file's namesake struct", async () => {
|
||||
// fmt's posix-mock-test.cc defines posix-mock.h's `test::close`, and
|
||||
// ostream-test.cc declares a `struct test` of its own.
|
||||
const cg = await indexed({
|
||||
'test/posix-mock.h': lines('namespace test {', 'int close(int fildes);', '} // namespace test'),
|
||||
'test/posix-mock-test.cc': lines('#include "posix-mock.h"', 'int test::close(int fildes) { return fildes; }'),
|
||||
'test/ostream-test.cc': lines('struct test {};'),
|
||||
'test/os-test.cc': lines('#include "posix-mock.h"', 'int Shut(int fd) {', ' return test::close(fd);', '}'),
|
||||
});
|
||||
try {
|
||||
expect(calls(cg, 'Shut').filter((c) => c.startsWith('test::close'))).toHaveLength(1);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("gives a class no base another source file declares", async () => {
|
||||
const cg = await indexed({
|
||||
'util/base.cc': lines(
|
||||
'namespace rocksdb {',
|
||||
'class Base {',
|
||||
' public:',
|
||||
' virtual void Run() {}',
|
||||
'};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
'util/derived.h': lines(
|
||||
'namespace rocksdb {',
|
||||
'class Derived : public Base {};',
|
||||
'} // namespace rocksdb',
|
||||
),
|
||||
});
|
||||
try {
|
||||
const derived = cg.getNodesByKind('class').find((n) => n.qualifiedName === 'rocksdb::Derived')!;
|
||||
expect(cg.getOutgoingEdgesFrom([derived.id], ['extends']).map((e) => cg.getNode(e.target)?.filePath)).toEqual([]);
|
||||
} finally {
|
||||
cg.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1858,6 +1858,186 @@ const C_STATIC_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
|
||||
*/
|
||||
const C_SOURCE_EXT = /\.(c|cc|cpp|cxx|c\+\+|m|mm)$/i;
|
||||
|
||||
/** Per context: a C/C++ type's qualified name → its declarations. */
|
||||
const CPP_TYPE_DECLS = new WeakMap<ResolutionContext, Map<string, Node[]>>();
|
||||
/** Per context: a C/C++ file → the line and resolved file of each `#include` it writes. */
|
||||
const CPP_INCLUDE_LINES = new WeakMap<ResolutionContext, Map<string, Array<[number, string | null]>>>();
|
||||
/** Per context: a C/C++ file → the line spans of its unnamed namespaces. */
|
||||
const CPP_UNNAMED_NAMESPACES = new WeakMap<ResolutionContext, Map<string, Array<[number, number]>>>();
|
||||
|
||||
/**
|
||||
* Whether only another C or C++ translation unit can name `candidate`: a
|
||||
* type or alias a source file declares, a member of a class only source
|
||||
* files declare, or what a source file's unnamed namespace (`namespace { … }`)
|
||||
* declares. rocksdb's test fixture `ColumnFamilyTestBase` is declared in
|
||||
* column_family_test.cc, and other test files' bare `Put(…)` reached its `Put`
|
||||
* by name; inlineskiplist.h's `Node*` reached hash_linklist_rep.cc's own
|
||||
* `Node`. What a source file declares is seen by the file itself, by a
|
||||
* translation unit that includes it (a test that includes the source it
|
||||
* tests), and by a header it includes after the declaration: rocksdb's
|
||||
* version_set.cc includes version_set_sync_and_async.h below its
|
||||
* `FilePicker` classes.
|
||||
*/
|
||||
function isCppTranslationUnitLocal(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
if (ref.language !== 'c' && ref.language !== 'cpp') return false;
|
||||
const seenFrom = (decl: Node, file: string): boolean => !C_SOURCE_EXT.test(decl.filePath) || decl.filePath === file ||
|
||||
cppIncluders(decl.filePath, context).has(file) || cppIncludesAfter(decl.filePath, decl.startLine, file, context);
|
||||
const seen = (decl: Node): boolean => seenFrom(decl, ref.filePath);
|
||||
if (CPP_TYPE_KINDS.has(candidate.kind)) return !seen(candidate);
|
||||
// A member written in its class's declaration is that declaration's; one
|
||||
// defined out of line is the class's, wherever a declaration of it its own
|
||||
// file sees is (through an alias, the class may be declared anywhere). A
|
||||
// scope its file sees no type of may be a namespace: fmt's posix-mock-test.cc
|
||||
// defines `test::close` of posix-mock.h's `namespace test`, and
|
||||
// ostream-test.cc declares a `struct test` of its own.
|
||||
const enclosing = cppEnclosingClassDecl(candidate, context);
|
||||
if (enclosing) return !seen(enclosing);
|
||||
const decls = cppTypeDecls(cppParentScope(candidate.qualifiedName), context)
|
||||
.filter((d) => seenFrom(d, candidate.filePath));
|
||||
if (decls.some((d) => d.kind === 'type_alias')) return false;
|
||||
if (decls.length > 0) return !decls.some(seen);
|
||||
return !seen(candidate) &&
|
||||
cppUnnamedNamespaceSpans(candidate.filePath, context).some(([start, end]) => candidate.startLine >= start && candidate.startLine <= end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Of the methods a C or C++ type's name gives, those `ref`'s translation
|
||||
* unit can see (isCppTranslationUnitLocal), when there are any. Classes of
|
||||
* one qualified name can be declared apart: rocksdb's db_stress_shared_state.h
|
||||
* and two benchmark `.cc` files each declare a `SharedState`, and a call in
|
||||
* db_stress_common.cc is the header's. (Every candidate for any other ref.)
|
||||
*/
|
||||
function cppVisibleFirst(methods: Node[], ref: UnresolvedRef, context: ResolutionContext): Node[] {
|
||||
if (methods.length < 2 || (ref.language !== 'c' && ref.language !== 'cpp')) return methods;
|
||||
const visible = methods.filter((m) => !isCppTranslationUnitLocal(m, ref, context));
|
||||
return visible.length > 0 ? visible : methods;
|
||||
}
|
||||
|
||||
/** The declarations of the C or C++ type (or alias) named `qualifiedName`. */
|
||||
function cppTypeDecls(qualifiedName: string, context: ResolutionContext): Node[] {
|
||||
if (!qualifiedName) return [];
|
||||
let memo = CPP_TYPE_DECLS.get(context);
|
||||
if (!memo) CPP_TYPE_DECLS.set(context, (memo = new Map()));
|
||||
let decls = memo.get(qualifiedName);
|
||||
if (!decls) {
|
||||
decls = context.getNodesByQualifiedName(qualifiedName)
|
||||
.filter((n) => CPP_TYPE_KINDS.has(n.kind) && (n.language === 'cpp' || n.language === 'c'));
|
||||
memo.set(qualifiedName, decls);
|
||||
}
|
||||
return decls;
|
||||
}
|
||||
|
||||
/** The innermost C or C++ class, struct or union declaration of its file whose lines hold `member`. */
|
||||
function cppEnclosingClassDecl(member: Node, context: ResolutionContext): Node | undefined {
|
||||
let innermost: Node | undefined;
|
||||
for (const cls of cppClassesIn(member.filePath, context)) {
|
||||
if (cls.id === member.id || member.startLine < cls.startLine || member.startLine > (cls.endLine ?? cls.startLine)) continue;
|
||||
if (!innermost || cls.startLine >= innermost.startLine) innermost = cls;
|
||||
}
|
||||
return innermost;
|
||||
}
|
||||
|
||||
/** Whether the C or C++ file `source` `#include`s `header` below line `line`. */
|
||||
function cppIncludesAfter(source: string, line: number, header: string, context: ResolutionContext): boolean {
|
||||
let memo = CPP_INCLUDE_LINES.get(context);
|
||||
if (!memo) CPP_INCLUDE_LINES.set(context, (memo = new Map()));
|
||||
let includes = memo.get(source);
|
||||
if (!includes) {
|
||||
includes = context.getNodesInFile(source)
|
||||
.filter((n) => n.kind === 'import')
|
||||
.map((n): [number, string | null] => [n.startLine, cppIncludedFile(n, context)]);
|
||||
memo.set(source, includes);
|
||||
}
|
||||
return includes.some(([at, file]) => at > line && file === header);
|
||||
}
|
||||
|
||||
/**
|
||||
* The line spans of the unnamed namespaces (`namespace { … }`) a C or C++
|
||||
* file writes, the outermost of nested ones: what one declares has internal
|
||||
* linkage, so no other translation unit names it. Comments, preprocessor
|
||||
* lines and string, character and raw string literals are skipped.
|
||||
*/
|
||||
function cppUnnamedNamespaceSpans(file: string, context: ResolutionContext): Array<[number, number]> {
|
||||
let memo = CPP_UNNAMED_NAMESPACES.get(context);
|
||||
if (!memo) CPP_UNNAMED_NAMESPACES.set(context, (memo = new Map()));
|
||||
const hit = memo.get(file);
|
||||
if (hit) return hit;
|
||||
const spans: Array<[number, number]> = [];
|
||||
const text = context.readFile(file) ?? '';
|
||||
if (/\bnamespace\s*\{/.test(text)) {
|
||||
const open: Array<{ line: number; unnamed: boolean }> = [];
|
||||
let line = 1;
|
||||
let lineStart = true;
|
||||
// The code since the last brace or `;`, without comments and literals.
|
||||
let code = '';
|
||||
let i = 0;
|
||||
while (i < text.length) {
|
||||
const c = text[i]!;
|
||||
if (c === '\n') {
|
||||
line++;
|
||||
lineStart = true;
|
||||
code += ' ';
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (lineStart && c === '#') {
|
||||
// A preprocessor line, continued by a trailing backslash (before a CRLF too).
|
||||
let end = i;
|
||||
while (end < text.length && (text[end] !== '\n' || text[end - 1] === '\\' || (text[end - 1] === '\r' && text[end - 2] === '\\'))) {
|
||||
if (text[end] === '\n') line++;
|
||||
end++;
|
||||
}
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
if (!/\s/.test(c)) lineStart = false;
|
||||
if (c === '/' && text[i + 1] === '/') {
|
||||
const end = text.indexOf('\n', i);
|
||||
i = end < 0 ? text.length : end;
|
||||
continue;
|
||||
}
|
||||
if (c === '/' && text[i + 1] === '*') {
|
||||
const end = text.indexOf('*/', i + 2);
|
||||
const stop = end < 0 ? text.length : end + 2;
|
||||
for (let j = i; j < stop; j++) if (text.charCodeAt(j) === 10) line++;
|
||||
i = stop;
|
||||
continue;
|
||||
}
|
||||
if (c === '"' && /(?:^|[^\w$])(?:u8|[uUL])?R$/.test(text.slice(Math.max(0, i - 4), i))) {
|
||||
// `R"delim( … )delim"`.
|
||||
const paren = text.indexOf('(', i);
|
||||
const end = paren < 0 ? -1 : text.indexOf(`)${text.slice(i + 1, paren)}"`, paren);
|
||||
const stop = end < 0 ? text.length : end + paren - i + 1;
|
||||
for (let j = i; j < stop && j < text.length; j++) if (text.charCodeAt(j) === 10) line++;
|
||||
i = Math.min(stop, text.length);
|
||||
continue;
|
||||
}
|
||||
if (c === '"' || c === "'") {
|
||||
let j = i + 1;
|
||||
while (j < text.length && text[j] !== c && text[j] !== '\n') j += text[j] === '\\' ? 2 : 1;
|
||||
i = j + 1;
|
||||
continue;
|
||||
}
|
||||
if (c === '{') {
|
||||
open.push({ line, unnamed: /(?:^|[^\w$])namespace\s*$/.test(code) });
|
||||
code = '';
|
||||
} else if (c === '}') {
|
||||
const top = open.pop();
|
||||
if (top?.unnamed && !open.some((o) => o.unnamed)) spans.push([top.line, line]);
|
||||
code = '';
|
||||
} else if (c === ';') {
|
||||
code = '';
|
||||
} else {
|
||||
code += c;
|
||||
if (code.length > 200) code = code.slice(-100);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
memo.set(file, spans);
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a C/C++ function definition carries the `static` storage class —
|
||||
* read from its first source line(s), since the extractor records no storage
|
||||
@@ -2761,7 +2941,9 @@ function isSfcPrivate(n: Node, context: ResolutionContext): boolean {
|
||||
* includes it and stays visible. On a 2,109-file betaflight tree 145
|
||||
* cross-file calls resolved onto a `static` in another `.c` (#1730) —
|
||||
* `usbd_get_descriptor` onto the `static get_device_descriptor` of
|
||||
* whichever USB class file ranked first.
|
||||
* whichever USB class file ranked first. So is what a source file's
|
||||
* unnamed namespace declares, and a type only source files declare, with
|
||||
* its members (isCppTranslationUnitLocal).
|
||||
* - **Kotlin, Java, C#, Swift, Scala, Dart, PHP**: `private` is class- or
|
||||
* file-local. An Android `editor.apply()` resolved onto an unrelated class's
|
||||
* `private fun apply`.
|
||||
@@ -2826,11 +3008,8 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex
|
||||
(!isCsharpTypeVisible(candidate, ref, context) || !isCsharpNestedTypeInScope(candidate, ref, context))) return false;
|
||||
const lang = candidate.language as string;
|
||||
if (lang === 'c' || lang === 'cpp') {
|
||||
return (
|
||||
candidate.kind !== 'function' ||
|
||||
!C_SOURCE_EXT.test(candidate.filePath) ||
|
||||
!isStaticCFunction(candidate, context)
|
||||
);
|
||||
if (candidate.kind === 'function' && C_SOURCE_EXT.test(candidate.filePath) && isStaticCFunction(candidate, context)) return false;
|
||||
return !isCppTranslationUnitLocal(candidate, ref, context);
|
||||
}
|
||||
if (lang === 'go') {
|
||||
// By the name's first letter, not the extractor's flag: the flag is unset
|
||||
@@ -8851,7 +9030,7 @@ export function resolveMethodOnType(
|
||||
// in ANOTHER file (#314) — is unaffected: that block returns early whenever
|
||||
// an import FQN pins the class.
|
||||
if (ref.referenceKind === 'function_ref' && matches.length !== 1) return null;
|
||||
const ordered = preferCallSiteFile(matches, ref.filePath);
|
||||
const ordered = preferCallSiteFile(cppVisibleFirst(matches, ref, context), ref.filePath);
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: ordered[0]!.id,
|
||||
@@ -10142,7 +10321,7 @@ function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: Resol
|
||||
const named = context.getMethodMatches?.(cls.name, name, ref.language) ??
|
||||
context.getNodesByName(name).filter((n) => n.kind === 'method' && sameLanguageFamily(n.language, ref.language));
|
||||
const own = named.filter((n) => n.qualifiedName === want);
|
||||
if (own.length > 0) return preferCallSiteFile(own, ref.filePath)[0]!;
|
||||
if (own.length > 0) return preferCallSiteFile(cppVisibleFirst(own, ref, context), ref.filePath)[0]!;
|
||||
seen.add(cls.qualifiedName);
|
||||
if (depth >= 4 || !context.getSupertypeNodes) return null;
|
||||
for (const decl of context.getNodesByQualifiedName(cls.qualifiedName)) {
|
||||
@@ -11418,6 +11597,9 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
AWAITED_TYPE_MEMO.delete(context);
|
||||
AWAITED_FILES.delete(context);
|
||||
C_STATIC_MEMO.delete(context);
|
||||
CPP_TYPE_DECLS.delete(context);
|
||||
CPP_INCLUDE_LINES.delete(context);
|
||||
CPP_UNNAMED_NAMESPACES.delete(context);
|
||||
RUST_TRAIT_IMPL_MEMO.delete(context);
|
||||
RUST_USES.delete(context);
|
||||
RUST_CRATES.delete(context);
|
||||
|
||||
Reference in New Issue
Block a user