From c77ddd7e84f86936e46dbd803c38ead1c490a64d Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 00:37:49 -0500 Subject: [PATCH] fix(c,cpp): a file named like a test is visible to the code that includes it isVisibleAcrossFiles hides every symbol in a test-suite file from non-test code, decided by the file's name. A C or C++ header is part of every translation unit that includes it, whatever its name: protobuf's conformance framework lives in conformance/conformance_test.h/.cc and test_runner.h, and the 9 `suite_.ReportFailure(...)` calls in the suites found ConformanceTestSuite::ReportFailure through the supertype walk only to have it dropped by the test-suite check. The check now lets a C/C++ candidate through when the reference's file includes it, directly or through other headers. A definition in a source file counts through the same-named test-suite header it includes (conformance_test.cc -> conformance_test.h), so a test that includes the production header it tests (jemalloc's test/unit/hash.c and hash.h) does not become its implementation. The include graph (new cpp-includers.ts) is read from the import nodes extraction mints for every #include, each resolved the way the graph's include edge is, walked from the included file up, from a narrow query of six columns. protobuf +675 edges, none removed or retargeted: 112 in conformance/ and 563 from *_unittest.cc files (which isTestPath does not recognize as tests) into the test utilities they include. redis +1 (expr.c includes fastjson_test.c). leveldb, fmt, llm.c, jq, rocksdb, typeorm, tokio, okhttp and excalidraw are byte-identical. The same exception for Strategy 3's receiver-name guess was measured and left out: it moved rocksdb guesses onto test::StringFS, a test double production code includes. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-included-test-suite.test.ts | 259 ++++++++++++++++++++++ src/db/queries.ts | 16 ++ src/resolution/cpp-includers.ts | 124 +++++++++++ src/resolution/index.ts | 4 + src/resolution/name-matcher.ts | 36 ++- src/resolution/types.ts | 7 + 7 files changed, 446 insertions(+), 1 deletion(-) create mode 100644 __tests__/cpp-included-test-suite.test.ts create mode 100644 src/resolution/cpp-includers.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c468bda..fb7f07601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - In C++, a call on a variable, parameter or member declared through a `typedef` or `using` alias now links to the method of the type the alias names, with the alias looked up the way C++ does it: in the calling function, its class and the classes that class inherits from, then the namespaces around it. In google/leveldb, `table_.Insert(buf)` on a `Table table_;` declared next to `typedef SkipList Table;` now reaches `SkipList::Insert` instead of the unrelated `HandleTable::Insert`, and `Table::Iterator iter(&table_);` reaches `SkipList::Iterator`'s constructor. A class that only shares the alias's name is no longer taken for it, and when the aliased type has no such method, as with an alias of `std::vector` or of a template parameter, no method is guessed from the receiver's name. Re-index C++ projects after upgrading. - In C++, a base class like `Message` in `class DynamicMessage final : public Message` now links to the class C++ itself would pick: the one declared in the namespaces around the class, or brought in by the file's `using` declarations and namespace aliases, and the class a `typedef` or `using` alias names. Before, a base class was matched by its name alone, so it could land on a class of the same name in another namespace or another language: in protocolbuffers/protobuf, `DynamicMessage` derived from the JSON parser's internal `Message` class, and in google/leveldb and RocksDB, iterators derived from an unrelated nested `Iterator` class such as the skip list's. The wrong base showed up in `codegraph_explore`'s type hierarchy and in impact, and tied the base's methods to the wrong overrides. A template parameter used as a base no longer links to a class that shares its name, and a base written from the global scope, like `public ::testing::Test`, now links to its class. Re-index C++ projects after upgrading. +- In C and C++, a header or source file named like a test, such as protocolbuffers/protobuf's `conformance_test.h` and `test_runner.h`, now counts as part of the code that `#include`s it, directly or through other headers. Before, calls from that code into it were dropped, so protobuf's conformance suites' `suite_.ReportFailure(…)` and `RunValidInputTest(…)` calls, and its unit tests' calls to the shared `TestUtil` helpers, linked to nothing. A test file that only tests include is still kept out of reach of the rest of your code. 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 Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. - In Go, working out which structs implement an interface now counts the methods of the interfaces it embeds, and the methods a struct gets from the types it embeds. Before, only each type's own methods counted: etcd's `AuthReadTx`, which embeds `UnsafeAuthReader` and adds `RLock` and `RUnlock`, was listed as implemented by every read-write lock in the project, an interface that only embeds others had no implementations at all, and a struct that gets its methods from an embedded base, like prometheus's service discoveries embedding `refresh.Discovery` or gin's `Engine` embedding `RouterGroup`, was missing. `codegraph_explore`, impact and the type hierarchy now list the right implementations, and a call through the interface reaches a method the embedding struct overrides, like gin's `Engine.Use`. Re-index Go projects after upgrading. diff --git a/__tests__/cpp-included-test-suite.test.ts b/__tests__/cpp-included-test-suite.test.ts new file mode 100644 index 000000000..19efb98f0 --- /dev/null +++ b/__tests__/cpp-included-test-suite.test.ts @@ -0,0 +1,259 @@ +/** + * A C or C++ file is part of every translation unit that includes it, whatever + * its name says. protobuf's conformance framework lives in + * `conformance/conformance_test.h` / `.cc` (`ConformanceTestSuite`) and + * `conformance/test_runner.h`, named like test suites, and the suites and + * runners that use it include them: `suite_.ReportFailure(…)` in + * binary_json_conformance_suite.cc is `ConformanceTestSuite::ReportFailure`. + * The test-suite rule ("production code never names a test suite's symbol") + * dropped that edge. A test-named file no production file includes stays out + * of reach, and so does a test that only includes the header it tests. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-cpp-included-test-')); + const files: Record = { + 'conformance/test_runner.h': `#ifndef CONFORMANCE_TEST_RUNNER_H +#define CONFORMANCE_TEST_RUNNER_H + +#include + +namespace conformance { + +class ConformanceTestRunner { + public: + virtual ~ConformanceTestRunner() = default; + virtual void RunTest(const std::string& test_name, const std::string& input, std::string* output) = 0; +}; + +} // namespace conformance + +#endif +`, + 'conformance/conformance_test.h': `#ifndef CONFORMANCE_CONFORMANCE_TEST_H +#define CONFORMANCE_CONFORMANCE_TEST_H + +#include + +#include "conformance/test_runner.h" + +namespace conformance { + +class ConformanceTestSuite { + public: + virtual ~ConformanceTestSuite() = default; + bool RunSuite(ConformanceTestRunner* runner, std::string* output); + void ReportFailure(int test, int level); + + protected: + virtual void RunSuiteImpl() = 0; + int failures_ = 0; +}; + +int FailureCount(const ConformanceTestSuite& suite); + +} // namespace conformance + +#endif +`, + 'conformance/conformance_test.cc': `#include "conformance/conformance_test.h" + +namespace conformance { + +bool ConformanceTestSuite::RunSuite(ConformanceTestRunner* runner, std::string* output) { + RunSuiteImpl(); + return failures_ == 0; +} + +void ConformanceTestSuite::ReportFailure(int test, int level) { + failures_ += test + level; +} + +int FailureCount(const ConformanceTestSuite& suite) { + return 0; +} + +} // namespace conformance +`, + // Another class's ReportFailure: where the call went by guess before. + 'conformance/fork_pipe_runner.h': `#include + +#include "conformance/test_runner.h" + +namespace conformance { + +class ForkPipeRunner : public ConformanceTestRunner { + public: + void RunTest(const std::string& test_name, const std::string& input, std::string* output) override; + + private: + std::string ReportFailure(bool timed_out, const std::string& what_failed); +}; + +} // namespace conformance +`, + 'conformance/fork_pipe_runner.cc': `#include "conformance/fork_pipe_runner.h" + +namespace conformance { + +void ForkPipeRunner::RunTest(const std::string& test_name, const std::string& input, std::string* output) { + *output = ReportFailure(false, test_name); +} + +std::string ForkPipeRunner::ReportFailure(bool timed_out, const std::string& what_failed) { + return what_failed; +} + +} // namespace conformance +`, + 'conformance/binary_json_conformance_suite.h': `#include "conformance/conformance_test.h" + +namespace conformance { + +class BinaryAndJsonConformanceSuite : public ConformanceTestSuite { + private: + void RunSuiteImpl() override; +}; + +class BinaryAndJsonConformanceSuiteImpl { + public: + explicit BinaryAndJsonConformanceSuiteImpl(BinaryAndJsonConformanceSuite* suite); + void RunValidBinaryInputTest(int test); + + private: + BinaryAndJsonConformanceSuite& suite_; +}; + +} // namespace conformance +`, + 'conformance/binary_json_conformance_suite.cc': `#include "conformance/binary_json_conformance_suite.h" + +namespace conformance { + +void BinaryAndJsonConformanceSuite::RunSuiteImpl() { + BinaryAndJsonConformanceSuiteImpl impl(this); + impl.RunValidBinaryInputTest(1); +} + +void BinaryAndJsonConformanceSuiteImpl::RunValidBinaryInputTest(int test) { + suite_.ReportFailure(test, 0); +} + +} // namespace conformance +`, + 'conformance/conformance_test_main.cc': `#include "conformance/binary_json_conformance_suite.h" +#include "conformance/conformance_test.h" + +int RunConformance(const conformance::BinaryAndJsonConformanceSuite& suite) { + return FailureCount(suite); +} +`, + // Production code that does not include conformance_test.h. + 'tools/report.cc': `int Summarize(int suite) { + return FailureCount(suite); +} +`, + // A C test-named file a production file includes (redis' expr.c includes + // fastjson_test.c for its test build). + 'modules/fastjson_test.c': `int run_fastjson_test(void) { + return 0; +} +`, + 'modules/expr.c': `#include "fastjson_test.c" + +int expr_selftest(void) { + return run_fastjson_test(); +} +`, + // A real test suite nothing includes: its helper is not production's. + 'src/db.h': `int open_db(void); +`, + 'src/db.c': `#include "db.h" + +int open_db(void) { + return make_options(); +} +`, + 'src/db_test.c': `#include "db.h" + +int make_options(void) { + return 1; +} +`, + // A test that includes the header it tests, named like it, does not + // implement it: jemalloc's test/unit/hash.c and its hash.h. + 'include/hash.h': `unsigned hash(const char* key); +`, + 'src/hash.c': `#include "hash.h" + +unsigned hash(const char* key) { + return hash_selftest(); +} +`, + 'test/unit/hash.c': `#include "hash.h" + +unsigned hash_selftest(void) { + return hash("abc"); +} +`, + }; + 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); + } + cg = await CodeGraph.init(root, { index: true }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** `qualifiedName (file)` of every call the file's symbols make. */ +const callsFrom = (file: string): string[] => { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg.getOutgoingEdgesFrom(ids).filter((e) => e.kind === 'calls').map((e) => { + const target = cg.getNode(e.target)!; + return `${target.qualifiedName} (${target.filePath})`; + }); +}; + +describe('a C/C++ file named like a test that the program includes', () => { + it('links a call through the base class its suite derives from', () => { + const calls = callsFrom('conformance/binary_json_conformance_suite.cc'); + expect(calls).toContain('conformance::ConformanceTestSuite::ReportFailure (conformance/conformance_test.cc)'); + expect(calls).not.toContain('conformance::ForkPipeRunner::ReportFailure (conformance/fork_pipe_runner.cc)'); + }); + + it('links a free function the included header declares', () => { + expect(callsFrom('conformance/conformance_test_main.cc')).toContain('conformance::FailureCount (conformance/conformance_test.cc)'); + }); + + it('links a test-named source file that a production file includes', () => { + expect(callsFrom('modules/expr.c')).toContain('run_fastjson_test (modules/fastjson_test.c)'); + }); + + it('keeps it out of reach of a file that does not include it', () => { + expect(callsFrom('tools/report.cc').some((c) => c.includes('conformance_test.cc'))).toBe(false); + }); +}); + +describe('a C/C++ test suite nothing in the program includes', () => { + it('is still out of reach of production code', () => { + expect(callsFrom('src/db.c').some((c) => c.includes('src/db_test.c'))).toBe(false); + }); + + it('does not implement the production header it includes to test', () => { + expect(callsFrom('src/hash.c').some((c) => c.includes('test/unit/hash.c'))).toBe(false); + // The test still reaches the code it tests. + expect(callsFrom('test/unit/hash.c')).toContain('hash (src/hash.c)'); + }); +}); diff --git a/src/db/queries.ts b/src/db/queries.ts index 77fef9bba..93da4774a 100644 --- a/src/db/queries.ts +++ b/src/db/queries.ts @@ -300,6 +300,7 @@ export class QueryBuilder { deleteRefsByRowIdsFull?: SqliteStatement; getAllFilePaths?: SqliteStatement; getAllNodeNames?: SqliteStatement; + getCppIncludeNodes?: SqliteStatement; getDominantFile?: SqliteStatement; getChangeStamp?: SqliteStatement; getTopRouteFile?: SqliteStatement; @@ -3623,6 +3624,21 @@ export class QueryBuilder { return rows.map((r) => r.name); } + /** + * The import node of every C / C++ `#include`, narrowed to the path it + * spells and where it is written: the include graph reads nothing else, and + * decoding every import node of a large C++ project costs several times more. + */ + getCppIncludeNodes(): Array> { + if (!this.stmts.getCppIncludeNodes) { + this.stmts.getCppIncludeNodes = this.db.prepare( + `SELECT id, name, file_path AS filePath, language, start_line AS startLine, start_column AS startColumn + FROM nodes WHERE kind = 'import' AND language IN ('c', 'cpp')` + ); + } + return this.stmts.getCppIncludeNodes.all() as Array>; + } + /** * Stream the distinct node names one row at a time — the incremental * counterpart to {@link getAllNodeNames} for callers that need to yield diff --git a/src/resolution/cpp-includers.ts b/src/resolution/cpp-includers.ts new file mode 100644 index 000000000..d3fd4b5ef --- /dev/null +++ b/src/resolution/cpp-includers.ts @@ -0,0 +1,124 @@ +/** + * Which files include a C or C++ file: the files whose `#include` names it, + * then the files that include those, out to the translation units. + * + * Read from the `import` node extraction mints for every `#include`, each + * resolved by the import resolver exactly as the graph's include edge is, so + * "includes" here is the include edge a reader sees. The name matcher asks it + * whether a C or C++ file named like a test is part of a reference's + * translation unit: protobuf's conformance framework lives in + * `conformance/conformance_test.h` and `test_runner.h`, which the suites and + * runners that use it include. + * + * The walk starts from the included file, not from the reference's: a handful + * of such files are asked about, by references from many files, and a file + * nothing includes — a project's own tests, mostly — costs one map lookup. + */ +import type { Language, Node } from '../types'; +import type { ResolutionContext } from './types'; + +/** One `#include` directive, as its import node records it. */ +interface Include { + id: string; + /** The path as written: `conformance/conformance_test.h`. */ + spec: string; + file: string; + language: Language; + line: number; + column: number; +} + +interface Memo { + /** Every C / C++ `#include`, by the file name it spells (`a/b.h` → `b.h`). */ + byName: Map | null; + /** Import node id → the indexed file its `#include` names (null: none). */ + targets: Map; + /** File → the files that include it, directly or through other includes. */ + includers: Map>; +} + +const MEMO = new WeakMap(); + +export function clearCppIncluderMemos(context: ResolutionContext): void { + MEMO.delete(context); +} + +function memoFor(context: ResolutionContext): Memo { + let memo = MEMO.get(context); + if (!memo) MEMO.set(context, (memo = { byName: null, targets: new Map(), includers: new Map() })); + return memo; +} + +const isCFamily = (language: string): boolean => language === 'c' || language === 'cpp'; +const fileName = (filePath: string): string => filePath.slice(Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')) + 1); + +const toInclude = (n: Pick): Include => + ({ id: n.id, spec: n.name, file: n.filePath, language: n.language, line: n.startLine, column: n.startColumn }); + +/** The indexed file an `#include` names, the way its include edge resolves it. */ +function includedFile(include: Include, context: ResolutionContext, memo: Memo): string | null { + const hit = memo.targets.get(include.id); + if (hit !== undefined) return hit; + const resolved = context.resolveImport?.({ + fromNodeId: include.id, + referenceName: include.spec, + referenceKind: 'imports', + line: include.line, + column: include.column, + filePath: include.file, + language: include.language, + }); + const target = resolved ? context.getNodeById?.(resolved.targetNodeId) : null; + const file = target?.kind === 'file' ? target.filePath : null; + memo.targets.set(include.id, file); + return file; +} + +function includesByName(context: ResolutionContext, memo: Memo): Map { + if (memo.byName) return memo.byName; + const byName = new Map(); + const nodes = context.getCppIncludeNodes?.() ?? context.iterateNodesByKind?.('import') ?? context.getNodesByKind('import'); + for (const n of nodes) { + if (!isCFamily(n.language)) continue; + const name = fileName(n.name); + const list = byName.get(name); + if (list) list.push(toInclude(n)); + else byName.set(name, [toInclude(n)]); + } + memo.byName = byName; + return byName; +} + +/** The indexed file the `#include` an import node records names, or null. */ +export function cppIncludedFile(include: Node, context: ResolutionContext): string | null { + return include.kind === 'import' && isCFamily(include.language) ? includedFile(toInclude(include), context, memoFor(context)) : null; +} + +/** + * Every file that includes `filePath`, directly or through the files that + * include it: the translation units (and headers) it is part of. + */ +export function cppIncluders(filePath: string, context: ResolutionContext): ReadonlySet { + const memo = memoFor(context); + const hit = memo.includers.get(filePath); + if (hit) return hit; + const byName = includesByName(context, memo); + const found = new Set(); + const queue = [filePath]; + for (let i = 0; i < queue.length; i++) { + const target = queue[i]!; + const name = fileName(target); + const dot = name.lastIndexOf('.'); + // `#include "foo"` can name `foo.h`: the include resolver tries extensions. + for (const spelled of dot > 0 ? [name, name.slice(0, dot)] : [name]) { + for (const include of byName.get(spelled) ?? []) { + if (include.file === filePath || found.has(include.file)) continue; + if (includedFile(include, context, memo) !== target) continue; + found.add(include.file); + queue.push(include.file); + } + } + } + memo.includers.set(filePath, found); + return found; +} diff --git a/src/resolution/index.ts b/src/resolution/index.ts index d8eca46e4..131f676cb 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -31,6 +31,7 @@ import { clearVbnetReceiverMemos, isVbMemberRead, isVbPathCall, matchVbMemberRea import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters'; import { gateDartLocal, clearDartLocalScopeMemos } from './dart-local-scope'; import { clearCppTypeAliasMemos } from './cpp-type-aliases'; +import { clearCppIncluderMemos } from './cpp-includers'; import { matchShopifyThemeFile } from './shopify-themes'; import { resolveViaImport, resolvePhpImportedStaticCall, resolvePhpQualifiedClassRef, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isDartImportRef, isLuaRequireRef, isJsPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver'; import { ResolverPool, minRefsForPool, shouldEngageAdaptively } from './resolver-pool'; @@ -534,6 +535,7 @@ export class ReferenceResolver { clearDartLocalScopeMemos(this.context); clearCppTypeAliasMemos(this.context); clearCppSupertypeMemos(this.context); + clearCppIncluderMemos(this.context); } } @@ -964,6 +966,8 @@ export class ReferenceResolver { getCppIncludeDirs: () => { return loadCppIncludeDirs(this.projectRoot); }, + + getCppIncludeNodes: () => this.queries.getCppIncludeNodes(), }; } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 08dc5575f..17913a6d0 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -15,6 +15,7 @@ import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary import { isDartLocallyBound } from './dart-local-scope'; import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers'; import { cppAliasedTypeName, isCppPointerType, resolveCppAliasedType } from './cpp-type-aliases'; +import { cppIncludedFile, cppIncluders } from './cpp-includers'; import { isTestPath } from '../search/query-utils'; import { isMinifiedContent } from '../extraction/generated-detection'; import { getCargoWorkspaceCrateMap } from './frameworks/cargo-workspace'; @@ -1563,6 +1564,37 @@ function isTestSuitePath(filePath: string): boolean { return /(?:^|\/)(?:tests?|__tests__|specs?|e2e)\//.test(lower) || /(?:^|\/)[A-Za-z0-9]*(?:Test|Tests|Spec)\//.test(filePath); } +const fileStem = (filePath: string): string => { + const name = filePath.slice(filePath.lastIndexOf('/') + 1); + const dot = name.lastIndexOf('.'); + return dot > 0 ? name.slice(0, dot) : name; +}; + +/** + * Whether a C or C++ file named like a test suite is part of `ref`'s + * translation unit after all. A file is in every translation unit that + * includes it, whatever its name says: protobuf's conformance framework is + * `conformance_test.h` (`ConformanceTestSuite`) and `test_runner.h`, which the + * suites and runners include, so binary_json_conformance_suite.cc's + * `suite_.ReportFailure(…)` is `ConformanceTestSuite::ReportFailure`. A + * definition in a source file counts through the header it implements: the + * one named like it that it includes (`conformance_test.cc` → + * `conformance_test.h`), when that header is a test suite's too — jemalloc's + * test `test/unit/hash.c` includes the library's `hash.h` to test it. + */ +function isIncludedCppTestSuite(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean { + if ((candidate.language !== 'c' && candidate.language !== 'cpp') || (ref.language !== 'c' && ref.language !== 'cpp')) return false; + if (cppIncluders(candidate.filePath, context).has(ref.filePath)) return true; + const stem = fileStem(candidate.filePath); + for (const include of context.getNodesInFile(candidate.filePath)) { + if (include.kind !== 'import' || fileStem(include.name) !== stem) continue; + const header = cppIncludedFile(include, context); + if (header && header !== candidate.filePath && fileStem(header) === stem && isTestSuitePath(header) && + cppIncluders(header, context).has(ref.filePath)) return true; + } + return false; +} + const MINIFIED_SCRIPTS = new WeakMap>(); /** A minified / bundled script, by name (`jquery.min.js`) or by its text. */ @@ -2238,7 +2270,9 @@ export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, contex if (isMinifiedScript(candidate.filePath, context)) return false; // A test suite is not linked into the program: typeorm's `Record` is // not a test entity `Record`, tokio's `Output` not a `runtime/tests` type. - if (isTestSuitePath(candidate.filePath) && !isTestPath(ref.filePath)) return false; + // A C or C++ file the reference's translation unit includes is, whatever + // its name. + if (isTestSuitePath(candidate.filePath) && !isTestPath(ref.filePath) && !isIncludedCppTestSuite(candidate, ref, context)) return false; // A Svelte component's instance script, or a Vue SFC's `