fix(cpp): a gtest TEST_F or TEST_P body finds its fixture's types, and a function's local class is seen only from that function (#2551)

A TEST_F / TEST_P body is a member function of a class derived from its
fixture, so its unqualified names find the fixture's nested types (its bases'
included) before the enclosing namespaces'. And since the index names a
function's local class after the function, every TEST_F body's local `FakeWF`
or `MyManager` shared one qualified name: a class local to a function is now
visible only from inside that function, and a namesake class's methods are
its own body's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-14 06:31:30 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 754cc45ad7
commit d94a92ea89
4 changed files with 251 additions and 9 deletions
+48 -3
View File
@@ -75,6 +75,42 @@ function isCFamily(n: Node): boolean {
return n.language === 'cpp' || n.language === 'c';
}
/**
* The declarations code at `site` can see (visibleFrom), where a class local
* to a function counts only in that function's lines: the index names it
* after the function, and every gtest `TEST_F` body in a namespace is
* `ns::TEST_F`, so one body's local `Listener` passed for any other's — in
* the body itself, and in the methods of a local class of its own.
*/
function seenFrom(site: CppNameSite, context: ResolutionContext): (n: Node) => boolean {
const visible = visibleFrom(site.file);
return (n) => {
if (!visible(n)) return false;
const fn = cppFunctionAround(n, context);
return !fn || (fn.filePath === site.file && site.line >= fn.startLine && site.line <= fn.endLine);
};
}
const LOCAL_TO = new WeakMap<ResolutionContext, Map<string, Node | null>>();
/**
* The function a C or C++ class is local to: the one of the scope its
* qualified name is in whose lines hold it. Null for a class of a namespace
* or of another class.
*/
function cppFunctionAround(cls: Node, context: ResolutionContext): Node | null {
let memo = LOCAL_TO.get(context);
if (!memo) LOCAL_TO.set(context, (memo = new Map()));
const hit = memo.get(cls.id);
if (hit !== undefined) return hit;
const scope = cppParentScope(cls.qualifiedName);
const fn = (scope ? context.getNodesByQualifiedName(scope) : []).find((f) =>
(f.kind === 'function' || f.kind === 'method') && f.filePath === cls.filePath &&
f.startLine <= cls.startLine && f.endLine >= cls.startLine) ?? null;
memo.set(cls.id, fn);
return fn;
}
/** The declarations code in `file` can see: a header's, and the file's own. */
function visibleFrom(file: string): (n: Node) => boolean {
return (n) => n.filePath === file || !SOURCE_FILE.test(n.filePath);
@@ -110,6 +146,13 @@ export interface CppNameSite {
parameters: ReadonlySet<string>;
/** A class the name cannot be: the deriving class, in its own base clause. */
self?: Node;
/**
* A class looked in right after `scope` itself, its bases included: a gtest
* `TEST_F` body is a member function of a class derived from its fixture,
* so the fixture's nested types are what its unqualified names find
* before the enclosing namespaces'.
*/
classScope?: string;
}
/** The class a C or C++ type name names. */
@@ -133,8 +176,9 @@ export function cppClassWritten(written: string, site: CppNameSite, ref: Unresol
const global = written.startsWith('::');
if (!global && site.parameters.has(segments[0]!)) return null;
const visible = visibleFrom(site.file);
const visible = seenFrom(site, context);
const scopes = cppScopesWithin(site.scope);
if (site.classScope) scopes.splice(1, 0, site.classScope);
const resolved = (node: Node): CppWrittenClass => ({ node, exact: true });
// The scopes around the name short of the global one, or only the global one for `::Base`.
@@ -234,7 +278,7 @@ function classNode(named: CppNamedClass, site: CppNameSite, context: ResolutionC
.filter((n) => isCFamily(n) && CLASS_KINDS.has(n.kind) && n.id !== site.self?.id);
// A lookup that ended on the class itself (its namespace lost) found nothing.
if (classes.length === 0) return undefined;
const visible = classes.filter(visibleFrom(site.file));
const visible = classes.filter(seenFrom(site, context));
return pick(visible.length > 0 ? visible : classes, site.file);
}
@@ -288,9 +332,10 @@ interface CppUsing {
const USINGS = new WeakMap<ResolutionContext, Map<string, CppUsing[]>>();
/** Drop the per-file `using` memo (see ReferenceResolver.clearCaches). */
/** Drop the per-file `using` memo and the local-class memo (see ReferenceResolver.clearCaches). */
export function clearCppSupertypeMemos(context: ResolutionContext): void {
USINGS.delete(context);
LOCAL_TO.delete(context);
}
const PATH = String.raw`(?:::\s*)?[A-Za-z_]\w*(?:\s*::\s*[A-Za-z_]\w*)*`;
+20 -6
View File
@@ -10163,13 +10163,21 @@ function matchCppHeldTypeCall(raw: string, methodName: string, ref: UnresolvedRe
const CPP_CLASS_KINDS: ReadonlySet<string> = new Set(['class', 'struct', 'union']);
/** Where a type the calling function writes is looked up from: the function itself, so a class local to it is found first. */
/**
* Where a type the calling function writes is looked up from: the function
* itself, so a class local to it is found first, then — in a gtest `TEST_F`
* or `TEST_P` body — the test's fixture (cppTestFixture) and its bases, as
* for the member function of a class derived from it that the body is:
* rocksdb's `new TestUserDefinedIndexFactory()` is the fixture base's own.
*/
function cppCallSite(caller: Node, ref: UnresolvedRef, context: ResolutionContext): CppNameSite {
const fixture = cppTestFixture(caller, ref, context);
return {
scope: caller.kind === 'file' ? '' : caller.qualifiedName,
file: ref.filePath,
line: ref.line,
parameters: cppTemplateParameters(caller, context),
...(fixture ? { classScope: fixture } : {}),
};
}
@@ -10333,10 +10341,12 @@ function cppBasesKnown(cls: Node, context: ResolutionContext, depth = 0, seen =
* call's arguments fit (cppOverloadFor) — else the nearest one of a class
* it derives from, through the base edges every declaration of `cls` has.
* When other classes share `cls`'s qualified name, its own are the ones
* defined where it is: leveldb's db_bench.cc and db_bench_sqlite3.cc each
* declare a `RandomGenerator`, with a `Generate` that takes a `size_t` and
* an `int`. (A chained call `inner(…).name(…)` keeps the first: the
* arguments at its position are the inner call's.)
* defined in its body, else where it is: the local classes of gtest bodies
* in one namespace are each `ns::TEST_F::Listener`, and leveldb's
* db_bench.cc and db_bench_sqlite3.cc each declare a `RandomGenerator`, with
* a `Generate` that takes a `size_t` and an `int`. (A chained call
* `inner(…).name(…)` keeps the first: the arguments at its position are the
* inner call's.)
*/
function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: ResolutionContext, depth = 0, seen = new Set<string>()): Node | null {
const want = `${cls.qualifiedName}::${name}`;
@@ -10346,7 +10356,11 @@ function cppMethodOf(cls: Node, name: string, ref: UnresolvedRef, context: Resol
if (own.length > 0) {
const ordered = preferCallSiteFile(cppVisibleFirst(own, ref, context), ref.filePath);
if (ref.referenceName.includes('().')) return ordered[0]!;
const defined = cppClassDecls(cls.qualifiedName, context).length > 1
const namesakes = cppClassDecls(cls.qualifiedName, context).length > 1;
const inBody = namesakes
? ordered.filter((n) => n.filePath === cls.filePath && n.startLine >= cls.startLine && n.endLine <= cls.endLine)
: [];
const defined = inBody.length > 0 ? inBody : namesakes
? ordered.filter((n) => n.filePath === cls.filePath || cppIncluders(cls.filePath, context).has(n.filePath))
: ordered;
return cppOverloadFor(defined.length > 0 ? defined : ordered, name, ref, context);