fix(cpp): a call through a namespace links no class's method by its name (#2730)

A C++ call written through a namespace, `x::m()`, reached matchMethodCall's
name strategies when no declaration of `x::m` was found by qualified name.
They can only return a method of some class, which a namespace never holds:
the capitalized-receiver class (`internal` → a class `Internal`), the one
method of the name in the project, or the best receiver-word guess. So
protocolbuffers/protobuf's `absl::StripPrefix(name, …)` linked to its Rust
generator's `MultiCasePrefixStripper::StripPrefix` and `absl::Status(…)` to
`upb::Status`'s constructor. fmtlib/fmt's `fmt::ptr(p)` linked to a test
scanner's `ptr()` method, and `fmt::day(…)` to a test's `date::day`, ahead of
the step that reads the namespaces a macro opens and finds fmt's own `ptr`
function and `day` alias in `FMT_BEGIN_NAMESPACE`.

Those strategies now return nothing when the qualifier is a namespace
(isCppNamespaceQualifier): a lowercase name that no C or C++ type or alias
node has, and that the calling file declares as neither a template parameter
nor an alias (cppNamesBroughtIn, now split into type names and namespace
names, since a namespace the file opens confirms it is one). rocksdb's
`hash_table::Insert(…)` through `typedef HashTable<…> hash_table;` keeps its
edge. So does the constructor of a class the call names in that namespace
(isCppConstructorThrough): protobuf's `internal::SourceWrapper<absl::Cord>(&data)`
constructs `struct SourceWrapper<absl::Cord>`, a specialization the index
holds without its name, whose template is only declared. A method of
another template's specialization is no constructor: fmt's
`fmt::scan<int>(…)` is fmt's `scan` function, not `scanner<num>::scan`.
The check runs only where one of those strategies would return.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 05:56:24 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 009e4377ff
commit 81fd007850
3 changed files with 442 additions and 10 deletions
+68 -10
View File
@@ -16163,13 +16163,55 @@ function isUndeclaredTypeName(typeName: string, ref: UnresolvedRef, context: Res
* (cppNamesBroughtIn).
*/
function isCppUndeclaredQualifier(qualifier: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
return isUndeclaredTypeName(qualifier, ref, context) &&
cppTypesNamed(qualifier, context).length === 0 &&
!context.getNodesByName(qualifier).some((n) => (n.language === 'cpp' || n.language === 'c') && (n.kind === 'namespace' || n.kind === 'module')) &&
!cppNamesBroughtIn(ref.filePath, context).has(qualifier);
if (!isUndeclaredTypeName(qualifier, ref, context) || cppTypesNamed(qualifier, context).length > 0 ||
context.getNodesByName(qualifier).some((n) => (n.language === 'cpp' || n.language === 'c') && (n.kind === 'namespace' || n.kind === 'module'))) return false;
const names = cppNamesBroughtIn(ref.filePath, context);
return !names.types.has(qualifier) && !names.namespaces.has(qualifier);
}
const CPP_FILE_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
* Is the qualifier of a C++ `x::m()` call a namespace, `absl` in
* `absl::StripPrefix(…)` or `fmt` in `fmt::ptr(p)`, and not a type? A
* lowercase name that no C or C++ type or alias node has, and that the
* calling file doesn't declare as a template parameter or an alias
* (cppNamesBroughtIn). A namespace holds functions and types, never a
* class's method.
*/
function isCppNamespaceQualifier(qualifier: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
return /^[a-z_]/.test(qualifier) && cppTypesNamed(qualifier, context).length === 0 &&
!cppNamesBroughtIn(ref.filePath, context).types.has(qualifier);
}
/**
* Is `method` the constructor of the class a C++ call `ns::m(…)` names
* through its namespace? Its owner, right inside `ns`, is `m` itself or a
* specialization of the class template `m`, which the index holds without
* a name: protobuf's `internal::SourceWrapper<absl::Cord>(&data)` constructs
* the `struct SourceWrapper<absl::Cord>` in message_lite.cc, whose template
* is only declared. fmt's `template <> struct scanner<num>` is no `scan`.
*/
function isCppConstructorThrough(namespace: string, method: Node, context: ResolutionContext): boolean {
const parts = method.qualifiedName.split('::');
if (parts.length < 3 || parts[parts.length - 3] !== namespace) return false;
const owner = parts[parts.length - 2]!;
if (owner === method.name) return true;
if (owner !== '<anonymous>') return false;
const holder = context.getNodesByQualifiedName(parts.slice(0, -1).join('::')).find((n) =>
n.filePath === method.filePath && n.startLine <= method.startLine && (n.endLine ?? n.startLine) >= method.startLine);
const lines = holder ? context.getFileLines?.(holder.filePath) ?? context.readFile(holder.filePath)?.split(/\r?\n/) : undefined;
const head = holder && lines ? lines.slice(holder.startLine - 1, holder.startLine + 1).join(' ') : '';
return new RegExp(String.raw`\b(?:class|struct|union)\s+(?:[A-Za-z_]\w*\s+)*${method.name}\s*<`).test(head);
}
/** The names a C++ file makes something of itself, which the index holds no node for (see cppNamesBroughtIn). */
interface CppFileNames {
/** Template parameters and aliases. */
types: ReadonlySet<string>;
/** The namespaces it opens, aliases or uses. */
namespaces: ReadonlySet<string>;
}
const CPP_FILE_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, CppFileNames>>();
const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
@@ -16180,20 +16222,22 @@ const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, Readonly
* names: opened, aliased or used (`using namespace …;`). googletest's typed
* tests declare `TypeParam` and `TestFixture`.
*/
function cppNamesBroughtIn(file: string, context: ResolutionContext): ReadonlySet<string> {
function cppNamesBroughtIn(file: string, context: ResolutionContext): CppFileNames {
let memo = CPP_FILE_TYPE_NAMES.get(context);
if (!memo) CPP_FILE_TYPE_NAMES.set(context, (memo = new Map()));
const hit = memo.get(file);
if (hit) return hit;
const text = stripCommentsForRegex((context.readFile(file) ?? '').replace(/\\\r?\n/g, ' '), 'cpp');
const names = cppTypeNamesDeclared(text);
const types = cppTypeNamesDeclared(text);
const namespaces = new Set<string>();
for (const m of text.matchAll(/\bnamespace\s+([^;{}()]{1,200}?)\s*[;{]/g)) {
for (const name of m[1]!.match(/[A-Za-z_]\w*/g) ?? []) names.add(name);
for (const name of m[1]!.match(/[A-Za-z_]\w*/g) ?? []) namespaces.add(name);
}
for (const [macro, declared] of cppMacroTypeNames(context)) {
if (hasCppWord(text, macro)) for (const name of declared) names.add(name);
if (hasCppWord(text, macro)) for (const name of declared) types.add(name);
}
if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) names.add('TypeParam').add('TestFixture');
if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) types.add('TypeParam').add('TestFixture');
const names = { types, namespaces };
memo.set(file, names);
return names;
}
@@ -19165,6 +19209,17 @@ export function matchMethodCall(
return null;
}
// A C++ name through a namespace, `absl::StripPrefix(…)` or `fmt::ptr(p)`,
// is a function or type of that namespace, never a class's method, which
// is all the strategies below find: protobuf's `absl::StripPrefix` went to
// `rust::MultiCasePrefixStripper::StripPrefix`, and fmt's `fmt::ptr` to a
// scanner's `ptr` method, past fmt's own `ptr`, which a namespace macro
// declares and is read after them. The one exception is the constructor of
// a class the call names in that namespace (isCppConstructorThrough).
// Asked only of a guess about to be made.
const throughCppNamespace = (): boolean =>
ref.language === 'cpp' && match === colonMatch && isCppNamespaceQualifier(objectOrClass!, ref, context);
// Strategy 2: Instance variable receiver - try capitalized form to find class
// e.g., "permissionEngine" → look for a class named "PermissionEngine"
const capitalizedReceiver = objectOrClass!.charAt(0).toUpperCase() + objectOrClass!.slice(1);
@@ -19186,6 +19241,7 @@ export function matchMethodCall(
const methodNode = classStrategyMethod(classNode, methodName!, context);
if (methodNode) {
if (throughCppNamespace()) return null;
return {
original: ref,
targetNodeId: methodNode.id,
@@ -19299,6 +19355,7 @@ export function matchMethodCall(
!sharesReceiverWord(objectOrClass!, targetMethods[0]!) &&
!(ref.language === 'objc' && objcReceiverReaches(objectOrClass!, targetMethods[0]!, context)) &&
!(ref.language === 'php' && phpReceiverReaches(objectOrClass!, targetMethods[0]!, context)))) {
if (throughCppNamespace() && !isCppConstructorThrough(objectOrClass!, targetMethods[0]!, context)) return null;
return {
original: ref,
targetNodeId: targetMethods[0]!.id,
@@ -19382,6 +19439,7 @@ export function matchMethodCall(
// `JavaFeatures_NestInFileClassFeature::descriptor`, its Python
// extension's `OneofOptions::default_instance()` to `CppFileOptions`'.
if (ref.language === 'cpp' && match === colonMatch && isCppUndeclaredQualifier(objectOrClass!, ref, context)) return null;
if (throughCppNamespace() && !isCppConstructorThrough(objectOrClass!, bestMatch, context)) return null;
return {
original: ref,
targetNodeId: bestMatch.id,