mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 12:00:02 +08:00
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:
co-authored by
Claude Opus 5.5
parent
accf600e47
commit
009e4377ff
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user