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
+1
View File
@@ -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();
}
});
});
+190 -8
View File
@@ -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);