mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 20:12:11 +08:00
fix(cpp,rust): a qualified call like Status::OK() links the class it names, not one whose name ends the same way (#2723)
The partial qualified-name match took any declaration whose qualified name ended with the written text, with no scope boundary. In facebook/rocksdb about 4,100 calls to `Status::OK()`, `Status::InvalidArgument(…)` and their siblings went to `IOStatus`'s methods of those names; base-class calls from an override, like `return FileSystemWrapper::NewWritableFile(…)` or leveldb's `return DB::Put(…)`, went to a namesake subclass or to the calling method itself; and ripgrep's `Path::new(…)` on std's `Path` went to its own `PrinterPath::new`. - The written name must start a scope of the candidate's: the candidate's qualified name is the name, or ends with it right after `::` or `.`. - Among those candidates, the ones in the call's own language come first. protobuf's C++ `DescriptorPool::generated_pool()` met a Ruby namesake first, which the language gate then dropped. - A C++ name qualified by a template parameter, like `CacheShard::ComputeHash` in `template <class CacheShard> class ShardedCache`, gets no edge from any strategy, except to a member of the parameter's default (`typename Parser = TcParser`). The boundary alone moved rocksdb's call to the receiver-word guess, with the same wrong target. - The C++ template-head reader names each parameter by its last word, `int N`, `detail::fixed_string S`, `const PrefetchOpts& kOpts`, and keeps its default. A `<…>` after a C++ declaration's name is a specialization's arguments, not parameters. Before, `WireFormatLite:: FieldType kKeyFieldType` made `WireFormatLite` a type parameter. Measured on mainbd62d9c0and re-checked on17f1453a: rocksdb 4,219 retargeted, 83 added, 30 removed; protobuf 24, 47, 40; fmt 7, 0, 21; leveldb 2, 0, 0; ripgrep 0, 0, 38. Nine Ruby, Java, C#, C, JS, Python and Go controls are byte-identical. Only C++, Rust and Ruby reach this match; the file-path suffix match is already anchored and is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
17f1453a6c
commit
ac9f96f8a3
@@ -11626,16 +11626,23 @@ export function matchByQualifiedName(
|
||||
|
||||
// Try partial qualified name match — again preferring the call site's own
|
||||
// file when more than one symbol's qualifiedName ends with the reference.
|
||||
// The written name must start a scope of the candidate's: rocksdb's
|
||||
// `CacheShard::ComputeHash` (a template parameter's) is no
|
||||
// `clock_cache::ClockCacheShard::ComputeHash`. A namesake in the call's own
|
||||
// language comes first: protobuf's C++ `DescriptorPool::generated_pool()`
|
||||
// is never its Ruby `Google::Protobuf::DescriptorPool`'s.
|
||||
const parts = ref.referenceName.split(/[:.]/);
|
||||
const lastName = parts[parts.length - 1];
|
||||
if (lastName) {
|
||||
const partialCandidates = cppSpecializationPool(keepForRef(context.getNodesByName(lastName))
|
||||
.filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName)), ref, context);
|
||||
const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
|
||||
.filter((candidate) => endsAtScopeBoundary(candidate.qualifiedName, ref.referenceName)), ref, context);
|
||||
const ownLanguage = partialCandidates.filter((candidate) => sameLanguageFamily(candidate.language, ref.language));
|
||||
const pool = ownLanguage.length > 0 ? ownLanguage : partialCandidates;
|
||||
const chosen = preferCallSiteFile(pool, ref.filePath)[0];
|
||||
if (chosen) {
|
||||
return {
|
||||
original: ref,
|
||||
targetNodeId: cppFunctionOverload(chosen, partialCandidates, ref, context).id,
|
||||
targetNodeId: cppFunctionOverload(chosen, pool, ref, context).id,
|
||||
confidence: 0.85,
|
||||
resolvedBy: 'qualified-name',
|
||||
};
|
||||
@@ -11645,6 +11652,19 @@ export function matchByQualifiedName(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether qualified name `qn` is `name`, or ends with it right after a scope
|
||||
* separator: `::`, or the `.` of a dotted package, namespace, object path or
|
||||
* config key. A name written from the global scope (`::Foo::bar`) is never
|
||||
* the tail of a nested one.
|
||||
*/
|
||||
function endsAtScopeBoundary(qn: string, name: string): boolean {
|
||||
if (qn === name) return true;
|
||||
if (!qn.endsWith(name)) return false;
|
||||
const before = qn.slice(0, qn.length - name.length);
|
||||
return before.endsWith('::') || before.endsWith('.');
|
||||
}
|
||||
|
||||
/** A node a `Receiver.method()` call can name as the method's owning type. */
|
||||
function isMethodOwnerKind(n: Node): boolean {
|
||||
return n.kind === 'class' || n.kind === 'struct' || n.kind === 'union' || n.kind === 'interface' ||
|
||||
|
||||
@@ -32,7 +32,7 @@ const DECLARATION_KINDS: ReadonlySet<string> = new Set([
|
||||
/** How far past a declaration's first line its head can run: a parameter list may take a line per parameter. */
|
||||
const HEAD_LINES = 40;
|
||||
|
||||
const memos = new WeakMap<ResolutionContext, Map<string, ReadonlySet<string>>>();
|
||||
const memos = new WeakMap<ResolutionContext, Map<string, ReadonlyMap<string, string | undefined>>>();
|
||||
|
||||
/** Drop the memos (see ReferenceResolver.clearCaches). */
|
||||
export function clearTypeParameterMemos(context: ResolutionContext): void {
|
||||
@@ -69,6 +69,23 @@ export function gateTypeParameter(resolved: ResolvedRef | null, ref: UnresolvedR
|
||||
if (!chosen) return null;
|
||||
result = { ...result, targetNodeId: chosen.id };
|
||||
}
|
||||
// A C++ name qualified by a template parameter is a member of whatever type
|
||||
// the template is instantiated with: rocksdb's `CacheShard::ComputeHash(…)`
|
||||
// in `template <class CacheShard> class ShardedCache` is not the
|
||||
// `ClockCacheShard::ComputeHash` a guess by name reaches. Only the
|
||||
// parameter's default is known: protobuf's `Parser::ParseLoop(…)` under
|
||||
// `template <typename Parser = TcParser>` is TcParser's for every caller
|
||||
// that passes no parser.
|
||||
if (ref.language === 'cpp') {
|
||||
const qualifier = /^([A-Za-z_]\w*)::/.exec(ref.referenceName)?.[1];
|
||||
if (qualifier) {
|
||||
for (const declaration of enclosingDeclarations(ref, context)) {
|
||||
const parameters = declaredTypeParameters(declaration, context);
|
||||
if (!parameters.has(qualifier)) continue;
|
||||
return isMemberOfDefault(result, parameters.get(qualifier), context) ? result : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!GENERIC_LANGUAGES.has(ref.language) || !TYPE_REF_KINDS.has(ref.referenceKind)) return result;
|
||||
const name = ref.referenceName;
|
||||
if (!/^[A-Z][A-Za-z0-9_]*$/.test(name)) return result;
|
||||
@@ -78,6 +95,14 @@ export function gateTypeParameter(resolved: ResolvedRef | null, ref: UnresolvedR
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether the resolved target is a member of the class a C++ template parameter defaults to (`= TcParser`, `= std::allocator<T>`). */
|
||||
function isMemberOfDefault(result: ResolvedRef, fallback: string | undefined, context: ResolutionContext): boolean {
|
||||
const type = fallback === undefined ? undefined : /([A-Za-z_]\w*)\s*(?:<[\s\S]*>)?\s*$/.exec(fallback)?.[1];
|
||||
if (!type) return false;
|
||||
const segments = context.getNodeById?.(result.targetNodeId)?.qualifiedName.split('::') ?? [];
|
||||
return segments.length >= 2 && segments[segments.length - 2] === type;
|
||||
}
|
||||
|
||||
const SCALA_OWNER_KINDS: ReadonlySet<string> = new Set(['class', 'trait', 'module', 'interface', 'struct', 'enum']);
|
||||
const SCALA_TYPE_KINDS: ReadonlySet<string> = new Set(['class', 'trait', 'interface', 'struct', 'enum', 'type_alias']);
|
||||
|
||||
@@ -129,7 +154,8 @@ function enclosingDeclarations(ref: UnresolvedRef, context: ResolutionContext):
|
||||
.filter((n) => DECLARATION_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line);
|
||||
}
|
||||
|
||||
function declaredTypeParameters(node: Node, context: ResolutionContext): ReadonlySet<string> {
|
||||
/** The type parameters a declaration declares, each with the default a C++ head gives it (`typename Parser = TcParser`). */
|
||||
function declaredTypeParameters(node: Node, context: ResolutionContext): ReadonlyMap<string, string | undefined> {
|
||||
let memo = memos.get(context);
|
||||
if (!memo) {
|
||||
memo = new Map();
|
||||
@@ -137,12 +163,14 @@ function declaredTypeParameters(node: Node, context: ResolutionContext): Readonl
|
||||
}
|
||||
const hit = memo.get(node.id);
|
||||
if (hit) return hit;
|
||||
const names = new Set<string>();
|
||||
const names = new Map<string, string | undefined>();
|
||||
const lines = context.getFileLines?.(node.filePath) ?? context.readFile(node.filePath)?.split(/\r?\n/) ?? null;
|
||||
if (lines) {
|
||||
const head = lines.slice(node.startLine - 1, Math.min(node.endLine, node.startLine - 1 + HEAD_LINES)).join('\n');
|
||||
const at = nameIndex(head, node.name);
|
||||
if (at >= 0) {
|
||||
// (C++ declares them only in a `template <…>` head, below: the `<…>` after
|
||||
// a C++ class's name is a specialization's arguments.)
|
||||
if (at >= 0 && node.language !== 'cpp') {
|
||||
// After the declared name: `Foo<T>`, `f[A]`.
|
||||
let open = at + node.name.length;
|
||||
while (open < head.length && /[ \t]/.test(head[open]!)) open++;
|
||||
@@ -159,11 +187,13 @@ function declaredTypeParameters(node: Node, context: ResolutionContext): Readonl
|
||||
const receiver = /^\s*func\s*\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*[A-Za-z_]\w*\s*\[/.exec(head);
|
||||
if (receiver) collect(head, receiver[0].length - 1, names);
|
||||
}
|
||||
// C++: `template <typename T, class U>` on the lines just above.
|
||||
// C++: `template <typename T, class U>` on the lines just above. Each
|
||||
// parameter is named last: `int N = 0`, `detail::fixed_string S` and
|
||||
// `std::integral T` declare N, S and T.
|
||||
if (node.language === 'cpp') {
|
||||
const above = lines.slice(Math.max(0, node.startLine - 3), node.startLine).join('\n');
|
||||
const template = /\btemplate\s*</g;
|
||||
for (let m = template.exec(above); m; m = template.exec(above)) collect(above, m.index + m[0].length - 1, names);
|
||||
for (let m = template.exec(above); m; m = template.exec(above)) collect(above, m.index + m[0].length - 1, names, true);
|
||||
}
|
||||
}
|
||||
memo.set(node.id, names);
|
||||
@@ -231,15 +261,26 @@ function nameIndex(head: string, name: string): number {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Add the parameter names of the bracket group opening at `open`. */
|
||||
function collect(text: string, open: number, names: Set<string>): void {
|
||||
/**
|
||||
* Add the parameter names of the bracket group opening at `open`. With
|
||||
* `lastWord` (C++), each is the last word before its default, which is kept.
|
||||
*/
|
||||
function collect(text: string, open: number, names: Map<string, string | undefined>, lastWord = false): void {
|
||||
const close = matchingBracket(text, open);
|
||||
if (close < 0) return;
|
||||
for (const item of splitTopLevel(text.slice(open + 1, close), text[open] === '<')) {
|
||||
if (lastWord) {
|
||||
const at = item.indexOf('=');
|
||||
const param = /([A-Za-z_]\w*)\s*$/.exec(at < 0 ? item : item.slice(0, at))?.[1];
|
||||
if (param && param !== 'typename' && param !== 'class' && param !== 'struct') {
|
||||
names.set(param, at < 0 ? undefined : item.slice(at + 1).trim());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Past annotations (`@sp(Int, Long) A`, `@NonNull T`), variance and keywords.
|
||||
const bare = item.replace(/^\s*(?:@[\w.]+(?:\([^()]*\))?\s*)+/, '');
|
||||
const param = /^\s*(?:(?:in|out|reified|const|typename|class|struct)\s+|[+\-']\s*)*([A-Za-z_]\w*)/.exec(bare)?.[1];
|
||||
if (param && param !== 'typename' && param !== 'class') names.add(param);
|
||||
if (param && param !== 'typename' && param !== 'class' && !names.has(param)) names.set(param, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user