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:
Colby Mchenry
2026-10-13 17:44:46 +00:00
committed by GitHub
parent 0cea43e9ba
commit 61cb4188a7
3 changed files with 664 additions and 8 deletions
@@ -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();
}
});
});