From 32b839acae9ffcba9af51c5fdf08f9434a079e86 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Mon, 19 Oct 2026 22:12:26 +0000 Subject: [PATCH] fix(c,cpp): a name in C or C++ code outside tests links to what the file's own includes declare (#2684) PR #2675 narrowed a C or C++ test's same-named candidates to the ones its translation unit declares. Production code now gets the same rule: protobuf's Python extension called the copies of upb's C API that the PHP extension bundles in php-upb.c, and its Java generator's calls of the free HasHasbit() went to ImmutableFieldGenerator::HasHasbit, a method beside the caller. Refinements the wider scope needed: - a source implements the same-stem header beside it, when it includes one: pyext/message.cc is no implementation of google/protobuf/message.h; - a header's own overloads reach the definitions its source holds: conformance/binary_wireformat.h's template Varint calls the .cc's; - narrowing never leaves a call with only functions its arguments can't fill, past one outside the unit that they can: glog's three-argument GetStackTrace(...) is not utilities.h's GetStackTrace(). This also mends 9 breakpad unittest calls #2675 had narrowed the same way; - nor, for a C call, to definitions only a flag nothing sets compiles, past one the build without it does: that stays #2615's choice. A file-level "call" (a macro, or a declaration read as a call) is left as it was. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/cpp-translation-unit.test.ts | 368 +++++++++++++++++++++++++ src/resolution/name-matcher.ts | 132 +++++---- 3 files changed, 451 insertions(+), 50 deletions(-) create mode 100644 __tests__/cpp-translation-unit.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f42c05aac..4c2c27d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In C++, an alias that names a class through a class derived from the one that declares it, like protocolbuffers/protobuf's `using ConformanceRequestSetting = BinaryAndJsonConformanceSuite::ConformanceRequestSetting;`, now leads to that class, `ConformanceTestSuite::ConformanceRequestSetting`. Before, an alias was followed only to a class declared under exactly the name it spells, so objects created through this one linked to the alias rather than the class. Re-index C++ projects after upgrading. - In C++, a name written in a googletest `TEST_F` or `TEST_P` body now finds the test fixture's nested types, those of its base classes included, before anything in the surrounding namespace, as C++ does for the member function such a body is: facebook/rocksdb's `new TestUserDefinedIndexFactory()` links to the class nested in the fixture's base. A class declared inside a test body, or inside any function, is now only found from that function, so the local classes many RocksDB tests each declare under one name, like `FakeWF` or `MyManager`, no longer stand in for one another, and a call like `fwf->SetIOError(true)` reaches the `FakeWF` its own test declares. Re-index C++ projects after upgrading. - In C and C++ tests, a name that several files declare now links to the one the test's own includes declare, when there is one: protocolbuffers/protobuf's C# generator tests call the `TestSourceDir()` that `googletest.h` declares, not another test helper's they never include; its upb tests call `upb_Arena_New()` and the rest of upb's C API in upb's own files, not the copies bundled for PHP and Ruby, which had left those calls unlinked; and facebook/rocksdb's C API test reaches `rocksdb_open` and its siblings in `db/c.cc`. Re-index C and C++ projects after upgrading. +- In C and C++ code outside tests too, a name that several files declare now links to the one the file's own includes declare, when there is one: protocolbuffers/protobuf's Python extension calls functions like `upb_Encode()` in upb's own files rather than the copies bundled for PHP, and facebook/rocksdb's stress tool calls the `StringToHex()` its own header declares rather than `LDBCommand::StringToHex`. Re-index C and C++ projects after upgrading. - In C++, a declaration continued on a line that starts with `*`, like facebook/rocksdb's `const auto& el =` over `*reinterpret_cast*>(jhandle);`, is now read, so calls such as `el->OnFlushCompleted(…)` link to `EventListener`'s methods; such a line used to be skipped as part of a comment. Code commented out in a `/* … */` block, and the text of a string literal, like protocolbuffers/protobuf's `[[deprecated("Please use SpaceUsedLong() instead")]]`, are no longer read as declarations, which had sent a call to the wrong method or to none. Re-index C++ projects after upgrading. - In C++, a call with a comparison or a shift among its arguments, like protocolbuffers/protobuf's `p->Emit({{"declare", has_bit_word_index < 0 ? … : ""}, …}, "…")` or `WriteVarint(num << 3, s)`, is now read with the right number of arguments, so it links to the overload that fits rather than one taking fewer. A parameter or variable whose type has template arguments is now read even after another such parameter on the same line, like facebook/rocksdb's `const autovector& cf_ids`, or with empty ones, like fmt's `parse_context<>& ctx`: calls on it link to that class's methods, and calls on a `std::vector` declared that way no longer link to a project class's method of the same name. Re-index C++ projects after upgrading. - In C++, a call passing `nullptr` now links to the overload that takes a `std::nullptr_t`, as C++ picks it, rather than one taking a pointer, and a call no longer links to an overload declared `= delete`, which no call that compiles can reach: protocolbuffers/protobuf's `ptr.reset(nullptr)` on an `Arena::UniquePtr` reaches `reset(std::nullptr_t)`, and `field.Add("foo")` on a `RepeatedPtrField` reaches `Add(Element&&)` instead of the deleted `Add(const Element&)`. Re-index C++ projects after upgrading. diff --git a/__tests__/cpp-translation-unit.test.ts b/__tests__/cpp-translation-unit.test.ts new file mode 100644 index 000000000..7c1f24bc0 --- /dev/null +++ b/__tests__/cpp-translation-unit.test.ts @@ -0,0 +1,368 @@ +/** + * A C or C++ name is one its translation unit declares, in production code + * as in tests (cpp-test-translation-unit.test.ts). Of several same-named + * candidates, what the file's includes declare goes first: protobuf's Python + * extension calls `upb_Encode()` from upb/wire/encode.h, not the copy the PHP + * extension bundles in php-upb.c; its Java generator calls the `HasHasbit()` + * helpers.h declares, not `ImmutableFieldGenerator::HasHasbit`, a method + * beside the caller. A source implements the header beside it (the + * Python extension's pyext/message.cc is no implementation of + * google/protobuf/message.h), a header's inline overload reaches the + * definition its own source holds, and a call keeps a function outside the + * unit that takes its arguments over one inside that can't (glog's + * three-argument `GetStackTrace(…)` is not utilities.h's `GetStackTrace()`). + */ +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-tu-')); + const files: Record = { + // upb's encoder, and the copy the PHP extension's amalgamation carries. + 'upb/wire/encode.h': `#ifndef UPB_WIRE_ENCODE_H_ +#define UPB_WIRE_ENCODE_H_ + +#include + +int upb_Encode(const void* msg, int options, char** buf, size_t* size); + +#endif +`, + 'upb/wire/encode.c': `#include "upb/wire/encode.h" + +int upb_Encode(const void* msg, int options, char** buf, size_t* size) { + *size = 0; + return 0; +} +`, + 'php/ext/google/protobuf/php-upb.h': `#include + +int upb_Encode(const void* msg, int options, char** buf, size_t* size); +`, + 'php/ext/google/protobuf/php-upb.c': `#include "php-upb.h" + +int upb_Encode(const void* msg, int options, char** buf, size_t* size) { + *size = 0; + return 0; +} +`, + 'python/message.c': `#include "upb/wire/encode.h" + +int PyUpb_Message_Serialize(const void* msg) { + char* buf; + size_t size; + return upb_Encode(msg, 0, &buf, &size); +} +`, + // A free function the Java generator's helpers.h declares, and a field + // generator's method of the same name beside the caller. + 'src/google/protobuf/compiler/java/helpers.h': `namespace google { +namespace protobuf { +class FieldDescriptor; +namespace compiler { +namespace java { + +inline bool HasHasbit(const FieldDescriptor* descriptor) { + return descriptor != nullptr; +} + +} // namespace java +} // namespace compiler +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/compiler/java/full/field_generator.h': `namespace google { +namespace protobuf { +class FieldDescriptor; +namespace compiler { +namespace java { + +class ImmutableFieldGenerator { + public: + bool HasHasbit() const; + + private: + const FieldDescriptor* descriptor_; +}; + +} // namespace java +} // namespace compiler +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/compiler/java/full/field_generator.cc': `#include "google/protobuf/compiler/java/full/field_generator.h" + +#include "google/protobuf/compiler/java/helpers.h" + +namespace google { +namespace protobuf { +namespace compiler { +namespace java { + +bool ImmutableFieldGenerator::HasHasbit() const { + return ::google::protobuf::compiler::java::HasHasbit(descriptor_); +} + +} // namespace java +} // namespace compiler +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/compiler/java/full/enum_field.cc': `#include "google/protobuf/compiler/java/helpers.h" +#include "google/protobuf/compiler/java/full/field_generator.h" + +namespace google { +namespace protobuf { +namespace compiler { +namespace java { + +namespace { + +int SetEnumVariables(const FieldDescriptor* descriptor) { + if (HasHasbit(descriptor)) { + return 1; + } + return 0; +} + +} // namespace + +} // namespace java +} // namespace compiler +} // namespace protobuf +} // namespace google +`, + // A header's template overload calls the one its own source defines. + 'conformance/binary_wireformat.h': `#include +#include + +namespace google { +namespace protobuf { +namespace conformance { + +using Wire = std::string; + +Wire Varint(uint64_t x); + +// Overloads to avoid forcing sign/size conversion warnings on call-sites. +template +Wire Varint(T x) { + return Varint(static_cast(x)); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google +`, + 'conformance/binary_wireformat.cc': `#include "conformance/binary_wireformat.h" + +namespace google { +namespace protobuf { +namespace conformance { + +Wire Varint(uint64_t x) { + return std::string(1, static_cast(x)); +} + +} // namespace conformance +} // namespace protobuf +} // namespace google +`, + // glog's three-parameter GetStackTrace is declared in stacktrace.h and + // defined in a header stacktrace.cc includes; utilities.h declares a + // GetStackTrace() of no parameters. + 'src/stacktrace.h': `namespace google { +namespace glog_internal_namespace_ { + +int GetStackTrace(void** result, int max_depth, int skip_count); + +} // namespace glog_internal_namespace_ +} // namespace google +`, + 'src/stacktrace_generic-inl.h': `#include "stacktrace.h" + +namespace google { +namespace glog_internal_namespace_ { + +int GetStackTrace(void** result, int max_depth, int skip_count) { + return 0; +} + +} // namespace glog_internal_namespace_ +} // namespace google +`, + 'src/stacktrace.cc': `#include "stacktrace.h" + +#include "stacktrace_generic-inl.h" +`, + 'src/utilities.h': `#include + +namespace google { + +std::string GetStackTrace(); + +} // namespace google +`, + 'src/utilities.cc': `#include "utilities.h" + +namespace google { + +std::string GetStackTrace() { + return ""; +} + +} // namespace google +`, + 'src/logging.cc': `#include + +#include "stacktrace.h" +#include "utilities.h" + +namespace google { + +using namespace glog_internal_namespace_; + +int RecordCrashReason(void** stack) { + std::string trace = GetStackTrace(); + int depth = GetStackTrace(stack, 32, 4); + return depth + static_cast(trace.size()); +} + +} // namespace google +`, + // The Python extension's message.cc implements pyext/message.h beside + // it, though it includes google/protobuf/message.h too. + 'src/google/protobuf/message.h': `namespace google { +namespace protobuf { + +class Message { + public: + virtual ~Message() {} +}; + +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/any.pb.h': `#include "google/protobuf/message.h" + +namespace google { +namespace protobuf { + +class Any : public Message { + public: + void MergeFrom(const Any& from) {} +}; + +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/descriptor.pb.h': `#include "google/protobuf/message.h" + +namespace google { +namespace protobuf { + +class FileDescriptorSet : public Message { + public: + void CopyFrom(const FileDescriptorSet& from); +}; + +} // namespace protobuf +} // namespace google +`, + 'src/google/protobuf/descriptor.pb.cc': `#include "google/protobuf/descriptor.pb.h" + +namespace google { +namespace protobuf { + +void FileDescriptorSet::CopyFrom(const FileDescriptorSet& from) { + if (&from == this) return; + MergeFrom(from); +} + +} // namespace protobuf +} // namespace google +`, + 'python/google/protobuf/pyext/message.h': `namespace google { +namespace protobuf { +namespace python { +namespace cmessage { + +int MergeFrom(void* self, void* arg); + +} // namespace cmessage +} // namespace python +} // namespace protobuf +} // namespace google +`, + 'python/google/protobuf/pyext/message.cc': `#include "google/protobuf/message.h" +#include "google/protobuf/pyext/message.h" + +namespace google { +namespace protobuf { +namespace python { +namespace cmessage { + +int MergeFrom(void* self, void* arg) { return 0; } + +} // namespace cmessage +} // namespace python +} // namespace protobuf +} // namespace google +`, + }; + 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++ reference links what its translation unit declares', () => { + it('calls the C function its includes declare, not a bundled copy', () => { + const calls = callsFrom('python/message.c'); + expect(calls).toContain('upb_Encode (upb/wire/encode.c)'); + expect(calls.some((c) => c.includes('php-upb'))).toBe(false); + }); + + it('calls the free function its includes declare, not a class\'s method beside it', () => { + const calls = callsFrom('src/google/protobuf/compiler/java/full/enum_field.cc'); + expect(calls).toContain('google::protobuf::compiler::java::HasHasbit (src/google/protobuf/compiler/java/helpers.h)'); + expect(calls.some((c) => c.includes('ImmutableFieldGenerator'))).toBe(false); + }); + + it('lets a header\'s overload call the definition its own source holds', () => { + const calls = callsFrom('conformance/binary_wireformat.h'); + expect(calls).toContain('google::protobuf::conformance::Varint (conformance/binary_wireformat.cc)'); + }); + + it('keeps a call off a function its arguments cannot fill', () => { + const calls = callsFrom('src/logging.cc'); + expect(calls).toContain('google::GetStackTrace (src/utilities.cc)'); + expect(calls).toContain('google::glog_internal_namespace_::GetStackTrace (src/stacktrace_generic-inl.h)'); + }); + + it('reads a source as implementing the header beside it, not a namesake elsewhere', () => { + const calls = callsFrom('src/google/protobuf/descriptor.pb.cc'); + expect(calls.some((c) => c.includes('pyext/message.cc'))).toBe(false); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 8fa8b991b..64e83ebbe 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1617,38 +1617,34 @@ const CPP_STEM_HEADERS = new WeakMap>() /** * The headers a C or C++ file includes under its own name * (`conformance_test.cc` → `conformance_test.h`): the declarations its - * definitions implement. + * definitions implement. One beside it, when it includes one, is the only + * such header: protobuf's pyext/message.cc implements pyext/message.h, not + * the google/protobuf/message.h it also includes; fmt's src/format.cc, with + * none beside it, implements include/fmt/format.h. */ function cppSameStemHeaders(filePath: string, context: ResolutionContext): string[] { let memo = CPP_STEM_HEADERS.get(context); if (!memo) CPP_STEM_HEADERS.set(context, (memo = new Map())); let headers = memo.get(filePath); if (!headers) { - headers = []; + const found: string[] = []; const stem = fileStem(filePath); for (const include of context.getNodesInFile(filePath)) { if (include.kind !== 'import' || fileStem(include.name) !== stem) continue; const header = cppIncludedFile(include, context); - if (header && header !== filePath && fileStem(header) === stem) headers.push(header); + if (header && header !== filePath && fileStem(header) === stem) found.push(header); } + const dir = filePath.slice(0, filePath.lastIndexOf('/') + 1); + const beside = found.filter((header) => header.slice(0, header.lastIndexOf('/') + 1) === dir); + headers = beside.length > 0 ? beside : found; memo.set(filePath, headers); } return headers; } -const TEST_FILES = new WeakMap>(); const TEST_SUITE_FILES = new WeakMap>(); -/** isTestPath, memoized: it is asked of every reference with an ambiguous C or C++ name. */ -function isTestFile(filePath: string, context: ResolutionContext): boolean { - let memo = TEST_FILES.get(context); - if (!memo) TEST_FILES.set(context, (memo = new Map())); - let hit = memo.get(filePath); - if (hit === undefined) memo.set(filePath, (hit = isTestPath(filePath))); - return hit; -} - -/** isTestSuitePath, memoized: it is asked of every candidate of such a name. */ +/** isTestSuitePath, memoized: it is asked of every candidate of an ambiguous C or C++ name. */ function isTestSuiteFile(filePath: string, context: ResolutionContext): boolean { let memo = TEST_SUITE_FILES.get(context); if (!memo) TEST_SUITE_FILES.set(context, (memo = new Map())); @@ -1675,8 +1671,10 @@ function isInCppTranslationUnit(candidate: Node, ref: UnresolvedRef, context: Re ((candidate.kind === 'function' || candidate.kind === 'variable' || candidate.kind === 'constant') && isStaticCFunction(candidate, context))) return false; const suite = isTestSuiteFile(candidate.filePath, context); - return cppSameStemHeaders(candidate.filePath, context) - .some((header) => (!suite || isTestSuiteFile(header, context)) && cppIncluders(header, context).has(ref.filePath)); + // (The header may be the reference's own: json.h's inline overloads call + // the ones json.cc defines.) + return cppSameStemHeaders(candidate.filePath, context).some((header) => (!suite || isTestSuiteFile(header, context)) && + (header === ref.filePath || cppIncluders(header, context).has(ref.filePath))); } /** @@ -1701,29 +1699,49 @@ function isCppNameAloneTarget(n: Node, ref: UnresolvedRef, context: ResolutionCo } /** - * Same-named candidates for a C or C++ reference in a test, narrowed to the - * ones its translation unit declares, when there are any. A test is a - * program of its own, so the name it uses is the one its includes declare: - * protobuf's csharp generator tests call `TestSourceDir()`, which - * googletest.h, an include of theirs, declares, and test_util2.h's - * `TestUtil::TestSourceDir`, a test suite none of them includes, took the - * calls; a test of the hpb backend calls upb's `upb_Arena_New()` from - * upb/mem/arena.h, not the copies php-upb.h and ruby-upb.h bundle. For a - * name the reference writes alone (`byName`), only what the name alone can - * mean counts (isCppNameAloneTarget), and a call the extractor recorded - * without the receiver it is written on (`columns()[0].value()`) is left to - * the name strategies. When the unit declares none of them — a test whose - * includes don't resolve — the candidates stay as they are. Not for an - * `#include`, which names a file, nor for production code, which - * isVisibleAcrossFiles keeps from test suites. + * Same-named candidates for a C or C++ reference, narrowed to the ones its + * translation unit declares, when there are any: the name a file uses is + * one its includes declare. protobuf's csharp generator tests call the + * `TestSourceDir()` googletest.h declares, not test_util2.h's + * `TestUtil::TestSourceDir`, a test suite none of them includes; its Python + * and Ruby extensions and upb's own code call `upb_Encode()` in + * upb/wire/encode.c, not the copy php-upb.c bundles; rocksdb's stress tool + * calls the `StringToHex()` its db_stress_common.h declares, not + * `LDBCommand::StringToHex`. For a name the reference writes alone + * (`byName`), only what the name alone can mean counts + * (isCppNameAloneTarget), and a call the extractor recorded without the + * receiver it is written on (`columns()[0].value()`) is left to the name + * strategies. When the unit declares none of them — a file whose includes + * don't resolve — the candidates stay as they are. Not for an `#include`, + * which names a file. */ -function inCppTestTranslationUnit(candidates: Node[], ref: UnresolvedRef, context: ResolutionContext, byName = false): Node[] { - if (candidates.length < 2 || (ref.language !== 'c' && ref.language !== 'cpp') || - ref.referenceKind === 'imports' || !isTestFile(ref.filePath, context)) return candidates; +function withinCppTranslationUnit(candidates: Node[], ref: UnresolvedRef, context: ResolutionContext, byName = false): Node[] { + if (candidates.length < 2 || (ref.language !== 'c' && ref.language !== 'cpp') || ref.referenceKind === 'imports') return candidates; if (byName && ref.referenceKind === 'calls' && cppImplicitReceiver(ref, context) !== 'bare') return candidates; + // A "call" outside any function is a macro, or a declaration read as a call + // (rocksdb's c.h `extern … rocksdb_t*` above `rocksdb_open(…);`): no call. + if (ref.referenceKind === 'calls' && context.getNodeById?.(ref.fromNodeId)?.kind === 'file') return candidates; const within = candidates.filter((n) => isInCppTranslationUnit(n, ref, context) && (!byName || n.filePath === ref.filePath || isCppNameAloneTarget(n, ref, context))); - return within.length > 0 ? within : candidates; + if (within.length === 0 || within.length === candidates.length) return candidates; + // Nor, for a C call, to definitions only a flag nothing sets compiles, past + // one the build without it does: that is cppFunctionOverload's choice + // (lua-cjson's fpconv.h defines `fpconv_init` under `#ifdef + // USE_INTERNAL_FPCONV`; redis's build calls fpconv.c's). + if (ref.language === 'c' && ref.referenceKind === 'calls' && within.every((n) => isCFeatureVariant(n, ref, context)) && + !isOffByDefault(ref.filePath, ref.line, context) && + candidates.some((n) => !within.includes(n) && n.kind === 'function' && !isStaticCFunction(n, context) && + !isCFeatureVariant(n, ref, context))) return candidates; + // Nor to functions too few or too many arguments for the call, past one + // that takes them: glog's `GetStackTrace(stack, ARRAYSIZE(stack), 1)` is + // the one stacktrace.h declares, which the stacktrace_*-inl.h headers its + // stacktrace.cc includes define, not utilities.h's `GetStackTrace()`. + if (ref.referenceKind === 'calls') { + const args = cppCallArguments(ref, within[0]!.name, context); + if (args && within.every((n) => cppArgumentCountFits(n, n.name, args, context) === false) && + candidates.some((n) => !within.includes(n) && cppArgumentCountFits(n, n.name, args, context) === true)) return candidates; + } + return within; } const MINIFIED_SCRIPTS = new WeakMap>(); @@ -8146,7 +8164,7 @@ export function matchByExactName( : swiftShape && swiftShape.shape !== 'chained' ? nearestSwiftMembers(filtered, ref, context) : kotlinBare ? lexicalKotlinMembers(filtered, ref, context) : cppSpecializationPool(filtered, ref, context); // (Above the ceiling below nothing is picked, so no translation unit is read.) - const candidates = ranked.length <= AMBIGUOUS_NAME_CEILING ? inCppTestTranslationUnit(ranked, ref, context, true) : ranked; + const candidates = ranked.length <= AMBIGUOUS_NAME_CEILING ? withinCppTranslationUnit(ranked, ref, context, true) : ranked; if (candidates.length === 0) { return null; @@ -9240,7 +9258,7 @@ export function resolveMethodOnType( // intentionally in ANOTHER file (#314) — is unaffected: those blocks return // early whenever an import pins the class. if (ref.referenceKind === 'function_ref' && matches.length !== 1) return null; - const ordered = preferCallSiteFile(inCppTestTranslationUnit(cppVisibleFirst(matches, ref, context), ref, context), ref.filePath); + const ordered = preferCallSiteFile(withinCppTranslationUnit(cppVisibleFirst(matches, ref, context), ref, context), ref.filePath); return { original: ref, targetNodeId: ordered[0]!.id, @@ -13185,7 +13203,6 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { clearCppRawStringMemos(context); CPP_CLASSES_IN_FILE.delete(context); CPP_STEM_HEADERS.delete(context); - TEST_FILES.delete(context); TEST_SUITE_FILES.delete(context); CPP_BASE_SPECS.delete(context); CPP_USING_MEMBERS.delete(context); @@ -14784,7 +14801,7 @@ export function matchMethodCall( // resolves to `a/`'s method (#1079). const strat1 = nmTimedT('mc-class', ref, (): ResolvedRef | null => { let classCandidates = preferCallSiteFile( - inCppTestTranslationUnit(context.getNodesByName(objectOrClass!).filter(isMethodOwnerKind), ref, context), + withinCppTranslationUnit(context.getNodesByName(objectOrClass!).filter(isMethodOwnerKind), ref, context), ref.filePath, ); // A C# class the call's namespaces can see before one they can't: @@ -14887,7 +14904,7 @@ export function matchMethodCall( if (capitalizedReceiver !== objectOrClass) { const strat2 = nmTimedT('mc-capital', ref, (): ResolvedRef | null => { const fuzzyClassCandidates = preferCallSiteFile( - inCppTestTranslationUnit(context.getNodesByName(capitalizedReceiver).filter(isMethodOwnerKind), ref, context), + withinCppTranslationUnit(context.getNodesByName(capitalizedReceiver).filter(isMethodOwnerKind), ref, context), ref.filePath, ); for (const classNode of fuzzyClassCandidates) { @@ -15022,7 +15039,7 @@ export function matchMethodCall( // the first seen) resolves to the call site's own file rather than the // first-indexed duplicate (#1079). const std = isStdMethodName(ref.language, methodName!) && !/^(?:self|Self|this|base)$/.test(objectOrClass!); - for (const method of preferCallSiteFile(inCppTestTranslationUnit(targetMethods, ref, context), ref.filePath)) { + for (const method of preferCallSiteFile(withinCppTranslationUnit(targetMethods, ref, context), ref.filePath)) { if (std && !receiverNamesOwner(receiverLink(objectOrClass!), method, context)) continue; // The owner type's own name — not its namespace (`eShop.ClientApp…` // shares `Client` with every `httpClient`) nor the method's. @@ -18569,20 +18586,35 @@ function cppOverloadParameters(n: Node, name: string, context: ResolutionContext return found; } +/** A C++ parameter that is a pack: `T&&... args`, not the `...` inside `format_string`. */ +function isCppParameterPack(p: string): boolean { + let flat = p; + for (let prev = ''; prev !== flat;) { prev = flat; flat = flat.replace(/<[^<>]*>/g, ''); } + return flat.includes('...'); +} + +/** + * Whether as many arguments as `args` can fill the parameters of the C or C++ + * function `n`, its defaults and a pack counted; null when `n` is no + * function or its parameter list can't be read. + */ +function cppArgumentCountFits(n: Node, name: string, args: string[], context: ResolutionContext): boolean | null { + if (n.kind !== 'function' && n.kind !== 'method') return null; + const list = cppOverloadParameters(n, name, context); + if (list === null) return null; + const params = splitCppTopLevel(cppWithoutComments(list)).filter((p) => p !== 'void'); + const required = params.filter((p) => !isCppParameterPack(p) && !/=/.test(p)).length; + return args.length >= required && (params.some(isCppParameterPack) || args.length <= params.length); +} + /** How well a call's arguments fit an overload's parameters: arity, and a string literal's first slot. */ function cppOverloadFit(n: Node, name: string, args: string[], context: ResolutionContext): number { if (n.kind !== 'function' && n.kind !== 'method') return -1; const list = cppOverloadParameters(n, name, context); if (list === null) return 0; const params = splitCppTopLevel(cppWithoutComments(list)).filter((p) => p !== 'void'); - // A pack is `T&&... args`, not the `...` inside `format_string`. - const isPack = (p: string): boolean => { - let flat = p; - for (let prev = ''; prev !== flat;) { prev = flat; flat = flat.replace(/<[^<>]*>/g, ''); } - return flat.includes('...'); - }; - const variadic = params.some(isPack); - const required = params.filter((p) => !isPack(p) && !/=/.test(p)).length; + const variadic = params.some(isCppParameterPack); + const required = params.filter((p) => !isCppParameterPack(p) && !/=/.test(p)).length; let score = 0; if (args.length < required || (!variadic && args.length > params.length)) score -= 3; // Each string literal against its parameter: a string type by name over a @@ -18596,7 +18628,7 @@ function cppOverloadFit(n: Node, name: string, args: string[], context: Resoluti for (let i = 0; i < args.length && i < params.length; i++) { const arg = args[i]!; const param = params[i]!; - if (isPack(param)) break; + if (isCppParameterPack(param)) break; if (!/^(?:u8|u|U|L)?R?"|^FMT_STRING\s*\(/.test(arg)) continue; const wideArg = /^LR?"/.test(arg); const wideParam = /\bw(?:string|char_t|format|string_view)|wchar_t/.test(param);