From 4b02a717e4c172808f8bab38fe58599f1c3293ff Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Tue, 13 Oct 2026 00:45:53 -0500 Subject: [PATCH] fix(cpp): a call or type name never links to what only another source file can see MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 1 + .../cpp-translation-unit-visibility.test.ts | 473 ++++++++++++++++++ src/resolution/name-matcher.ts | 198 +++++++- 3 files changed, 664 insertions(+), 8 deletions(-) create mode 100644 __tests__/cpp-translation-unit-visibility.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 01aa134aea..9463265143 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -101,6 +101,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading. - In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr(…)` or `auto* mutex = static_cast(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. Re-index C++ projects after upgrading. - In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic shutting_down_`, no longer links to a project method that only shares the method's name. 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. diff --git a/__tests__/cpp-translation-unit-visibility.test.ts b/__tests__/cpp-translation-unit-visibility.test.ts new file mode 100644 index 0000000000..714d71891b --- /dev/null +++ b/__tests__/cpp-translation-unit-visibility.test.ts @@ -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): Promise { + 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 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(); + } + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index a6bd02fb1d..c49632a3ef 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1858,6 +1858,186 @@ const C_STATIC_MEMO = new WeakMap>(); */ 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>(); +/** Per context: a C/C++ file → the line and resolved file of each `#include` it writes. */ +const CPP_INCLUDE_LINES = new WeakMap>>(); +/** Per context: a C/C++ file → the line spans of its unnamed namespaces. */ +const CPP_UNNAMED_NAMESPACES = new WeakMap>>(); + +/** + * 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, @@ -10004,7 +10183,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)) { @@ -11141,6 +11320,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);