fix(cpp): a call through a class the index doesn't hold gets no guess by a shared word (#2729)

A C++ call written through a class, `X::m()`, whose class has no node in the
index fell through to the last strategy of matchMethodCall, which scores
every method named `m` by the words its class shares with `X` and picks one.
protocolbuffers/protobuf declares its descriptor messages in descriptor.pb.h,
which is over the indexer's size cap, so `FeatureSet::default_instance()`
linked to `UnknownFieldSet::default_instance`, `FeatureSetDefaults::GetDescriptor()`
to `JavaFeatures_NestInFileClassFeature::GetDescriptor`, and the options
messages' `default_instance()` to `pb::enumvalue::JsonEnumValueOptions`'. A
class whose name only shares a word with `X` is another class: one named `X`
was looked for first, by qualified name and by class name.

That guess is now dropped when `X` is a capitalized name (isUndeclaredTypeName)
that no C or C++ class, struct, union, enum, alias or namespace node has, and
that the calling file makes nothing of itself (cppNamesBroughtIn): not a
template parameter of any `template <…>` head in it, not an alias it declares
(a function-local `using FD = FieldDescriptor;`, a typedef, or one a macro it
expands declares, like rocksdb's IMPORT_RIBBON_IMPL_TYPES), not a namespace it
opens, aliases or uses, and not googletest's TypeParam or TestFixture in a
typed test. The one method of a name in the project is still taken as before.

The scans read the file once (cppTypeNamesDeclared in cpp-type-aliases.ts,
shared with the template-head reader) and the project's headers once for
macro bodies, only when a guess would otherwise be returned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-21 05:11:50 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent accf600e47
commit 009e4377ff
4 changed files with 656 additions and 6 deletions
+74 -5
View File
@@ -383,12 +383,81 @@ function collectTemplateHeader(lines: readonly string[], startLine: number, star
// Args>` / `PROTOBUF_NDEBUG_INLINE static T* Create(…)`.
const header = at < 0 ? null : /^template\s*<([\s\S]*)>(?:\s*(?:\[\[[^\]]*\]\]|(?:static|inline|constexpr|consteval|virtual|explicit|friend|extern)\b|[A-Z][A-Z0-9]*_[A-Z0-9_]*\b(?:\s*\([^()]*\))?))*\s*$/.exec(before.slice(at));
if (!header) return;
for (const item of splitTopLevel(header[1]!)) {
// `typename T = int`, `template <typename> class Policy`, `typename... Ts`, `int N`.
const declared = stripCppTemplateArguments(item).split('=')[0]!;
const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1];
if (name && name !== 'typename' && name !== 'class') names.add(name);
for (const item of splitTopLevel(header[1]!)) addTemplateParameter(item, names);
}
/** Add the name one parameter of a `template <…>` header declares. */
function addTemplateParameter(item: string, names: Set<string>): void {
// `typename T = int`, `template <typename> class Policy`, `typename... Ts`, `int N`.
const declared = stripCppTemplateArguments(item).split('=')[0]!;
const name = /([A-Za-z_]\w*)\s*$/.exec(declared)?.[1];
if (name && name !== 'typename' && name !== 'class') names.add(name);
}
/** How far a `template <…>` header may run; one that runs longer is not read. */
const MAX_TEMPLATE_HEADER = 1000;
/** The `>` closing the template parameter list opened at `open`, or -1 when a `;` or a brace comes first. */
function templateHeaderEnd(text: string, open: number): number {
let depth = 0;
let parens = 0;
for (let i = open; i < text.length && i < open + MAX_TEMPLATE_HEADER; i++) {
const c = text[i]!;
if (c === '(' || c === '[') parens++;
else if (c === ')' || c === ']') parens = Math.max(0, parens - 1);
// `N > 0` in a default argument is written in parentheses.
else if (parens > 0) continue;
else if (c === '<') depth++;
else if (c === '>') {
if (--depth === 0) return i;
} else if (c === ';' || c === '{' || c === '}') return -1;
}
return -1;
}
/**
* The declarators of the `typedef` whose keyword ends at `from`, up to its
* `;`: those after the body of a type it defines (`typedef struct {…} X;`).
*/
function typedefDeclarators(text: string, from: number): string {
let depth = 0;
let start = from;
for (let i = from; i < text.length && i < from + 4 * MAX_TEMPLATE_HEADER; i++) {
const c = text[i]!;
if (c === '{') depth++;
else if (c === '}') {
if (--depth < 0) return '';
if (depth === 0) start = i + 1;
} else if (c === ';' && depth === 0) return text.slice(start, i);
}
return '';
}
/**
* The names C++ source text declares as template parameters — every
* `template <…>` header's, `Traits` of `typename Traits = buffer_traits` —
* or as aliases: `using X = …;`, `typedef … X;`, `typedef struct {…} X;`,
* `typedef void (*X)(int);`. Function and macro bodies count, though the
* index has no node for what they declare.
*/
export function cppTypeNamesDeclared(text: string): Set<string> {
const names = new Set<string>();
for (const m of text.matchAll(/\btemplate\s*</g)) {
const open = m.index! + m[0].length - 1;
const close = templateHeaderEnd(text, open);
if (close > open) for (const item of splitTopLevel(text.slice(open + 1, close))) addTemplateParameter(item, names);
}
for (const m of text.matchAll(/\busing\s+([A-Za-z_]\w*)\s*(?:\[\[[^\]]*\]\]\s*)?=(?!=)/g)) names.add(m[1]!);
for (const m of text.matchAll(/\btypedef\b/g)) {
for (const declarator of splitTopLevel(typedefDeclarators(text, m.index! + m[0].length))) {
// `(*Handler)` of a pointer to a function or member; else the last name,
// past a function type's parameters and an array's bounds.
const name = /\(\s*(?:[A-Za-z_]\w*\s*::\s*)*[*&^]+\s*(?:const\s+)?([A-Za-z_]\w*)\s*\)/.exec(declarator)?.[1] ??
/([A-Za-z_]\w*)\s*(?:\[[^\]]*\]\s*)*$/.exec(declarator.replace(/\([^()]*\)\s*(?:const\s*)?$/, ''))?.[1];
if (name) names.add(name);
}
}
return names;
}
/**
+85 -1
View File
@@ -14,7 +14,7 @@ import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-vis
import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries';
import { isDartLocallyBound } from './dart-local-scope';
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases';
import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeNamesDeclared, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases';
import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNameMacros, cppNamespaceAliases } from './cpp-namespaces';
import { clearCppRawStringMemos, cppLiteralsBlanked, cppNonCodeLines } from './cpp-raw-strings';
import { cppClassWritten, type CppNameSite } from './cpp-supertypes';
@@ -16155,6 +16155,81 @@ function isUndeclaredTypeName(typeName: string, ref: UnresolvedRef, context: Res
!context.getNodesByName(typeName).some((n) => isMethodOwnerKind(n) && sameLanguageFamily(n.language, ref.language));
}
/**
* Is the qualifier of a C++ `X::m()` call a class the index doesn't hold, as
* protobuf's `FeatureSet` is (declared in `descriptor.pb.h`, which is over the
* indexer's size cap)? A capitalized name that no C or C++ type, alias or
* namespace node has, and that the calling file makes nothing of itself
* (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);
}
const CPP_FILE_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
const CPP_MACRO_TYPE_NAMES = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
/**
* The names a C++ file makes types or namespaces of itself, which the index
* holds no node for: its template parameters (fmt's `Traits` of `typename
* Traits = buffer_traits`), the aliases it declares — in a function body
* too, or by expanding a macro that declares them — and the namespaces it
* names: opened, aliased or used (`using namespace …;`). googletest's typed
* tests declare `TypeParam` and `TestFixture`.
*/
function cppNamesBroughtIn(file: string, context: ResolutionContext): ReadonlySet<string> {
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);
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 [macro, declared] of cppMacroTypeNames(context)) {
if (hasCppWord(text, macro)) for (const name of declared) names.add(name);
}
if (/\bTYPED_TEST(?:_P)?\s*\(/.test(text)) names.add('TypeParam').add('TestFixture');
memo.set(file, names);
return names;
}
/**
* The type names each of the project's macros declares where it is
* expanded: rocksdb's `IMPORT_RIBBON_IMPL_TYPES(T)` declares `using
* SimpleSoln = …;`, a vendored googletest's `TYPED_TEST` its `TestFixture`.
*/
function cppMacroTypeNames(context: ResolutionContext): Map<string, ReadonlySet<string>> {
const hit = CPP_MACRO_TYPE_NAMES.get(context);
if (hit) return hit;
const macros = new Map<string, ReadonlySet<string>>();
for (const file of context.getAllFiles()) {
if (!/\.(?:h|hh|hpp|hxx|h\+\+|inl|inc|ipp|tcc)$/i.test(file)) continue;
const raw = context.readFile(file);
if (!raw || !raw.includes('define')) continue;
// Each `#define` with the lines it continues; only its body is stripped of comments.
for (const m of raw.matchAll(/^[ \t]*#[ \t]*define[ \t]+([A-Za-z_]\w*)(?:\([^)\n]*\))?((?:[^\n\\]|\\\r?\n|\\(?!\r?\n))*)/gm)) {
if (macros.has(m[1]!) || !/\b(?:using|typedef|template)\b/.test(m[2]!)) continue;
const names = cppTypeNamesDeclared(stripCommentsForRegex(m[2]!.replace(/\\\r?\n/g, ' '), 'cpp'));
if (names.size > 0) macros.set(m[1]!, names);
}
}
CPP_MACRO_TYPE_NAMES.set(context, macros);
return macros;
}
/** Does `text` hold `word` as a whole identifier? */
function hasCppWord(text: string, word: string): boolean {
for (let at = text.indexOf(word); at >= 0; at = text.indexOf(word, at + 1)) {
if (!/\w/.test(text[at - 1] ?? '') && !/\w/.test(text[at + word.length] ?? '')) return true;
}
return false;
}
function inferCppReceiverType(
receiverName: string,
ref: UnresolvedRef,
@@ -17250,6 +17325,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
KOTLIN_FRAMES.delete(context);
clearCppNamespaceMemos(context);
clearCppRawStringMemos(context);
CPP_FILE_TYPE_NAMES.delete(context);
CPP_MACRO_TYPE_NAMES.delete(context);
CPP_CLASSES_IN_FILE.delete(context);
CPP_STEM_HEADERS.delete(context);
TEST_SUITE_FILES.delete(context);
@@ -19298,6 +19375,13 @@ export function matchMethodCall(
// `$comment->delete()` — names the caller's own class only by a shared
// word. The guess is the caller itself, so there is no guess.
if (bestMatch && bestScore >= 2 && bestMatch.id !== ref.fromNodeId) {
// Nor is there one for a C++ `X::m()` through a class the index
// doesn't hold: a class whose name only shares a word with `X` is
// another class, since one named `X` was looked for first.
// protobuf's `FeatureSet::descriptor()` went to
// `JavaFeatures_NestInFileClassFeature::descriptor`, its Python
// extension's `OneofOptions::default_instance()` to `CppFileOptions`'.
if (ref.language === 'cpp' && match === colonMatch && isCppUndeclaredQualifier(objectOrClass!, ref, context)) return null;
return {
original: ref,
targetNodeId: bestMatch.id,