mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-29 06:46:49 +08:00
fix(cpp): a nested class defined out of line is indexed under its name, and a construction makes the class C++ finds (#2542)
A class, struct or union whose declaration writes its name in a scope —
leveldb's `struct SkipList<Key, Comparator>::Node { … }`, rocksdb's
`struct BlockBasedTable::Rep { … }` — was indexed as `<anonymous>`, so its
members belonged to no class anyone names and a call on a `Node*` or `Rep*`
was guessed by the method's name. Such a class now takes its own name,
qualified under the written scope the way an out-of-line method is, and its
members are qualified under it, in the TS extractor and the kernel alike. A
specialization (`struct fmt::formatter<Answer>`) stays unnamed: named after
its template, it stood in for the template and every other specialization.
Constructions are recorded by the type's last name, so with `DBImpl::Writer`
named, leveldb's `new log::Writer(…)` in `DBImpl` methods went to it. A C++
construction now makes the class C++ lookup finds for the type as written at
the construction (matchCppConstruction): read with its template arguments
(`new Outer<int>::Inner(…)` makes an `Outer::Inner`), at 0.9, or 0.7 when it
is only the class of that name the file can see; an enum lookup finds; through
an alias the calling function declares, the class it names; and nothing for a
template parameter, a member of a type from outside the project or a local
alias of one. Of several classes or aliases the file declares under one
qualified name (the local classes of separate gtest TEST_F bodies, the members
of unnamed specializations), the one declared around the construction. A `<`
or `>` in parentheses inside template arguments is now read as a comparison.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a6ceb0472c
commit
34dd3d2fb4
@@ -192,6 +192,31 @@ function recoverCppMacroDefinedName(node: SyntaxNode, source: string): string |
|
||||
return name;
|
||||
}
|
||||
|
||||
const CPP_CLASS_SPECIFIERS: ReadonlySet<string> = new Set(['class_specifier', 'struct_specifier', 'union_specifier']);
|
||||
|
||||
/**
|
||||
* The `::` segments of a C++ class, struct or union name written with a
|
||||
* scope, the scope's template arguments dropped: `['SkipList', 'Node']` for
|
||||
* an out-of-line nested class `struct SkipList<Key, Comparator>::Node`. Such
|
||||
* a class was indexed `<anonymous>`, so its members were no class's.
|
||||
* Undefined for a plain name, and for a specialization (`class
|
||||
* FunctionMocker<R(Args...)>`, `struct fmt::formatter<Foo>`): named after its
|
||||
* template, it would stand in for the template and every other
|
||||
* specialization of it (fmt's `formatter<weekday, Char> : formatter<std::tm,
|
||||
* Char>` extended `formatter<day, Char>`). Mirrored in the kernel
|
||||
* (ccpp/mod.rs).
|
||||
*/
|
||||
function cppWrittenClassParts(node: SyntaxNode, source: string): string[] | undefined {
|
||||
if (!CPP_CLASS_SPECIFIERS.has(node.type)) return undefined;
|
||||
const written = getChildByField(node, 'name');
|
||||
if (written?.type !== 'qualified_identifier') return undefined;
|
||||
let innermost: SyntaxNode | null = written;
|
||||
while (innermost?.type === 'qualified_identifier') innermost = getChildByField(innermost, 'name');
|
||||
if (innermost?.type !== 'type_identifier') return undefined;
|
||||
const parts = stripCppTemplateArgs(getNodeText(written, source)).split('::').map((p) => p.trim()).filter(Boolean);
|
||||
return parts.length > 0 && parts.every((p) => /^[A-Za-z_]\w*$/.test(p)) ? parts : undefined;
|
||||
}
|
||||
|
||||
function extractCppQualifiedMethodName(node: SyntaxNode, source: string): string | undefined {
|
||||
const macroDefined = recoverSingleArgMacroDefinedName(node, source) ?? recoverCppMacroDefinedName(node, source);
|
||||
if (macroDefined) return macroDefined;
|
||||
@@ -2400,9 +2425,10 @@ export const cppExtractor: LanguageExtractor = {
|
||||
bodyField: 'body',
|
||||
paramsField: 'parameters',
|
||||
resolveName: (node, source) =>
|
||||
extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source),
|
||||
extractCppQualifiedMethodName(node, source) ?? unnamedTypeVariableName(node, source) ?? cppWrittenClassParts(node, source)?.at(-1),
|
||||
getDeclarationWrapper: (node) => definingDeclaration(node) ?? undefined,
|
||||
getReceiverType: extractCppReceiverType,
|
||||
getClassScope: (node, source) => cppWrittenClassParts(node, source)?.slice(0, -1).join('::') || undefined,
|
||||
getReturnType: extractCppReturnType,
|
||||
// Constructors (definitions and class-body declarations) carry their
|
||||
// parameter list as the signature; a trailing semicolon marks a prototype, so a local `T obj(args)` can be matched
|
||||
|
||||
@@ -314,6 +314,14 @@ export interface LanguageExtractor {
|
||||
*/
|
||||
getReceiverType?: (node: SyntaxNode, source: string) => string | undefined;
|
||||
|
||||
/**
|
||||
* The scope a class, struct or union declaration writes its name in, when
|
||||
* it does: C++'s `SkipList` for an out-of-line nested class `struct
|
||||
* SkipList<Key, Comparator>::Node`. The class is qualified under it the way
|
||||
* an out-of-line method is under its receiver, and so are its members.
|
||||
*/
|
||||
getClassScope?: (node: SyntaxNode, source: string) => string | undefined;
|
||||
|
||||
/**
|
||||
* Extract a function/method's normalized return type name (bare class name,
|
||||
* smart-pointer pointee unwrapped), stored on the node as `returnType`. Used
|
||||
|
||||
@@ -1819,6 +1819,18 @@ export class TreeSitterExtractor {
|
||||
return prefix.length > 0 ? `${prefix.join('::')}::${base}` : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* The qualified name of a class, struct or union whose declaration writes
|
||||
* its name in a scope (getClassScope): C++'s out-of-line nested `struct
|
||||
* SkipList<Key, Comparator>::Node` is `leveldb::SkipList::Node`, composed
|
||||
* like an out-of-line method's. Its members are qualified under it
|
||||
* (objectPathOwners). Undefined for any other.
|
||||
*/
|
||||
private writtenScopeQualifiedName(node: SyntaxNode, name: string): string | undefined {
|
||||
const scope = this.extractor?.getClassScope?.(node, this.source);
|
||||
return scope ? this.composeReceiverQualifiedName(scope, name) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build qualified name from node stack
|
||||
*/
|
||||
@@ -2140,14 +2152,17 @@ export class TreeSitterExtractor {
|
||||
const docstring = this.docstringFor(node);
|
||||
const visibility = this.extractor.getVisibility?.(node);
|
||||
const isExported = this.extractor.isExported?.(node, this.source);
|
||||
const scoped = this.writtenScopeQualifiedName(node, name);
|
||||
|
||||
const classNode = this.createNode(kind, name, node, {
|
||||
docstring,
|
||||
visibility,
|
||||
isExported,
|
||||
...this.cppBodyEnd(resolvedBody),
|
||||
...(scoped !== undefined ? { qualifiedName: scoped } : {}),
|
||||
});
|
||||
if (!classNode) return;
|
||||
if (scoped !== undefined) this.objectPathOwners.set(classNode.id, scoped);
|
||||
this.openCppClassScope(classNode, resolvedBody);
|
||||
|
||||
// Extract extends/implements
|
||||
@@ -2356,14 +2371,17 @@ export class TreeSitterExtractor {
|
||||
const docstring = this.docstringFor(node);
|
||||
const visibility = this.extractor.getVisibility?.(node);
|
||||
const isExported = this.extractor.isExported?.(node, this.source);
|
||||
const scoped = this.writtenScopeQualifiedName(node, name);
|
||||
|
||||
const aggregateNode = this.createNode(kind, name, node, {
|
||||
docstring,
|
||||
visibility,
|
||||
isExported,
|
||||
...this.cppBodyEnd(body),
|
||||
...(scoped !== undefined ? { qualifiedName: scoped } : {}),
|
||||
});
|
||||
if (!aggregateNode) return;
|
||||
if (scoped !== undefined) this.objectPathOwners.set(aggregateNode.id, scoped);
|
||||
this.openCppClassScope(aggregateNode, body);
|
||||
|
||||
// Extract inheritance (e.g. Swift: struct HTTPMethod: RawRepresentable)
|
||||
|
||||
@@ -81,12 +81,16 @@ function cppTypeName(raw: string): TypeName | null {
|
||||
const parts: string[] = [];
|
||||
const templated: boolean[] = [];
|
||||
let depth = 0;
|
||||
let parens = 0;
|
||||
let part = '';
|
||||
let args = false;
|
||||
const text = raw.replace(/\b(?:const|volatile|mutable|typename|template|class|struct|union|enum)\b/g, ' ').replace(/[&*]+/g, ' ');
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const c = text[i]!;
|
||||
if (c === '<') {
|
||||
// (In template arguments, a `<` or `>` in parentheses compares: `Bits<(N > 8)>`.)
|
||||
if (depth > 0 && (c === '(' || c === ')')) parens = Math.max(0, parens + (c === '(' ? 1 : -1));
|
||||
else if (parens > 0) continue;
|
||||
else if (c === '<') {
|
||||
depth++;
|
||||
args = true;
|
||||
} else if (c === '>') depth = Math.max(0, depth - 1);
|
||||
@@ -254,6 +258,17 @@ function localAliasLines(caller: Node, lines: readonly string[], context: Resolu
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the function making `ref` declare, before it, an alias of the name a
|
||||
* written type starts with — `using Map = std::map<…>;` for `Map`? Such an
|
||||
* alias isn't indexed, so a lookup goes past it; resolveCppAliasedType reads it.
|
||||
*/
|
||||
export function cppDeclaresLocalAlias(written: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const name = cppTypeName(written);
|
||||
return !!name && !written.trimStart().startsWith('::') &&
|
||||
localAliasedType(name.names[0]!, context.getNodeById?.(ref.fromNodeId), ref, context) !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type an alias declared earlier in the calling function names, as
|
||||
* written. Those aliases are not indexed, so the function's own lines are
|
||||
|
||||
@@ -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, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases';
|
||||
import { cppAliasedClass, cppAliasedTypeName, cppAliasedTypeWritten, cppDeclaresLocalAlias, cppParentScope, cppScopesWithin, cppTemplateArguments, cppTemplateParameters, cppTypeSegments, isCppPointerType, resolveCppAliasedType, stripCppTemplateArguments, type CppAliasedType } from './cpp-type-aliases';
|
||||
import { clearCppNamespaceMemos, cppMacroNamespaceFrames, cppMacroNamespaceHeads, cppNamespaceAliases } from './cpp-namespaces';
|
||||
import { clearCppRawStringMemos, cppLiteralsBlanked, cppRawStringLines } from './cpp-raw-strings';
|
||||
import { cppClassWritten, type CppNameSite } from './cpp-supertypes';
|
||||
@@ -11346,6 +11346,133 @@ function inferCppAutoInitializerType(
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A C++ construction — `new log::Writer(…)`, `Writer w(…)`, which the
|
||||
* extractor records by the type's last name — makes the class C++ lookup
|
||||
* finds for the type as written at the ref (cppClassWritten), as a base
|
||||
* class names one (matchCppSupertype): from the scopes around the call
|
||||
* outward, its qualifier followed, an alias to the class it names. leveldb's
|
||||
* db_impl.cc writes `new log::Writer(…)` in `DBImpl` methods, which also
|
||||
* construct `DBImpl::Writer`s, the struct it defines out of line, as `Writer
|
||||
* w(…)`; by its last name alone the two were one.
|
||||
* - At 0.7 when lookup doesn't place the type and the class is only the one
|
||||
* of that name the file can see.
|
||||
* - The enum lookup finds instead of a class: rocksdb's `const ValueType
|
||||
* types[] = {kTypeValue, …}`.
|
||||
* - Through an alias the calling function declares itself, which isn't
|
||||
* indexed, the class it names, if one of the project's.
|
||||
* - null (no edge) when the type names no particular class: a template
|
||||
* parameter, a member of a type from outside the project (rocksdb's
|
||||
* `stl_wrappers::KVMap::value_type`), a function's own alias of one —
|
||||
* protobuf's `using Map = std::map<…>; … new Map{}` is no
|
||||
* `google::protobuf::Map`.
|
||||
* - undefined, for the name strategies to decide as before, when nothing
|
||||
* the file sees is named so.
|
||||
* The type is read with its template arguments, so `new Outer<int>::Inner(…)`,
|
||||
* which the extractor records by the name before the first `<`, makes an
|
||||
* `Outer::Inner`. Where the file declares several classes or aliases of the
|
||||
* qualified name lookup found, which C++ tells apart and qualified names
|
||||
* don't, the type is the one declared around the construction
|
||||
* (cppNamesakeAround).
|
||||
*/
|
||||
function matchCppConstruction(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
|
||||
const caller = context.getNodeById?.(ref.fromNodeId);
|
||||
const lines = context.getFileLines?.(ref.filePath) ?? context.readFile(ref.filePath)?.split(/\r?\n/);
|
||||
const text = lines?.[ref.line - 1]?.slice(Math.max(0, ref.column));
|
||||
if (!caller || !text) return undefined;
|
||||
// (A declaration's column is where its specifiers start: `const Slice key(…)`.)
|
||||
const specifiers = /^(?:(?:new|const|volatile|static|thread_local|constexpr|constinit|mutable|struct|class|union|typename)\s+)*/.exec(text)![0];
|
||||
const written = cppTypeWrittenAt(text.slice(specifiers.length));
|
||||
if (!written) return undefined;
|
||||
const made = (node: Node, exact = true): ResolvedRef => exact
|
||||
? { original: ref, targetNodeId: node.id, confidence: 0.9, resolvedBy: 'qualified-name' }
|
||||
: { original: ref, targetNodeId: node.id, confidence: 0.7, resolvedBy: 'exact-match' };
|
||||
if (cppDeclaresLocalAlias(written, ref, context)) {
|
||||
const aliased = resolveCppAliasedType(written, ref, context);
|
||||
const named = aliased && !aliased.pointer ? cppAliasedClass(aliased, context) : undefined;
|
||||
const cls = named ? context.getNodesByQualifiedName(named).find((n) => CPP_CLASS_KINDS.has(n.kind)) : undefined;
|
||||
return cls ? made(cls) : null;
|
||||
}
|
||||
const site = cppCallSite(caller, ref, context);
|
||||
const found = cppClassWritten(written, site, ref, context);
|
||||
if (found === null) {
|
||||
const named = cppEnumWritten(written, site, context);
|
||||
return named ? made(named) : null;
|
||||
}
|
||||
if (!found) return undefined;
|
||||
const namesakes = context.getNodesByQualifiedName(found.node.qualifiedName)
|
||||
.filter((n) => n.filePath === ref.filePath && (CPP_CLASS_KINDS.has(n.kind) || n.kind === 'type_alias'));
|
||||
const type = namesakes.length > 1 ? cppNamesakeAround(namesakes, ref, context) : found.node;
|
||||
return type ? made(type, found.exact) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The enum a C++ type written at `site` names, which C++ lookup for a class
|
||||
* passes by: the first scope around the site, innermost first, that declares
|
||||
* one of that name the file can see. (A template parameter of that name is
|
||||
* the parameter, which the resolver's type-parameter gate sees to.)
|
||||
*/
|
||||
function cppEnumWritten(written: string, site: CppNameSite, context: ResolutionContext): Node | undefined {
|
||||
const global = written.trimStart().startsWith('::');
|
||||
const path = stripCppTemplateArguments(written).replace(/\s+/g, '').replace(/^::/, '');
|
||||
for (const scope of global ? [''] : cppScopesWithin(site.scope)) {
|
||||
const enums = context.getNodesByQualifiedName(scope ? `${scope}::${path}` : path)
|
||||
.filter((n) => n.kind === 'enum' && (n.filePath === site.file || !C_SOURCE_EXT.test(n.filePath)));
|
||||
if (enums.length > 0) return enums.find((n) => n.filePath === site.file) ?? enums[0];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const CPP_HOLDER_KINDS: ReadonlySet<string> = new Set(['function', 'method', 'class', 'struct', 'union']);
|
||||
|
||||
/**
|
||||
* Of several classes or aliases the ref's file declares under one qualified
|
||||
* name — the local classes of rocksdb's gtest `TEST_F` bodies, each a
|
||||
* `FakeWF`, or the `Impl`s and `ArgumentTuple`s of googlemock's unnamed
|
||||
* specializations — the one whose holder (the function or class it is
|
||||
* declared in, else the file) holds the ref. Undefined unless exactly one is.
|
||||
*/
|
||||
function cppNamesakeAround(namesakes: Node[], ref: UnresolvedRef, context: ResolutionContext): Node | undefined {
|
||||
const holders = context.getNodesInFile(ref.filePath).filter((n) => CPP_HOLDER_KINDS.has(n.kind));
|
||||
const around = namesakes.filter((cls) => {
|
||||
let holder: Node | undefined;
|
||||
for (const n of holders) {
|
||||
if (n.id === cls.id || n.startLine > cls.startLine || n.endLine < cls.endLine) continue;
|
||||
if (!holder || n.endLine - n.startLine < holder.endLine - holder.startLine) holder = n;
|
||||
}
|
||||
return !holder || (holder.startLine <= ref.line && ref.line <= holder.endLine);
|
||||
});
|
||||
return around.length === 1 ? around[0] : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type name `text` starts with, template arguments and all: `log::Writer`
|
||||
* of `log::Writer(…)`, `Outer<int>::Inner` of `Outer<int>::Inner x(…)`.
|
||||
* Undefined when it starts with none, or a template argument list goes on to
|
||||
* the next line.
|
||||
*/
|
||||
function cppTypeWrittenAt(text: string): string | undefined {
|
||||
let end = 0;
|
||||
for (let segment = /^(?:::\s*)?[A-Za-z_]\w*/; ; segment = /^\s*::\s*[A-Za-z_]\w*/) {
|
||||
const name = segment.exec(text.slice(end));
|
||||
if (!name) return end > 0 ? text.slice(0, end) : undefined;
|
||||
end += name[0].length;
|
||||
const open = /^\s*</.exec(text.slice(end));
|
||||
if (!open) continue;
|
||||
end += open[0].length;
|
||||
// (`>` inside parentheses compares: `Bits<(N > 8)>`.)
|
||||
for (let angles = 1, parens = 0; angles > 0; end++) {
|
||||
const c = text[end];
|
||||
if (c === undefined) return undefined;
|
||||
if (c === '(') parens++;
|
||||
else if (c === ')') parens--;
|
||||
else if (parens > 0) continue;
|
||||
else if (c === '<') angles++;
|
||||
else if (c === '>') angles--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a C++ chained call whose receiver is itself a call — encoded by the
|
||||
* extractor as `<innerCallee>().<method>` (#645). The receiver's type is what
|
||||
@@ -15849,6 +15976,13 @@ function matchReferenceInner(
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
// 1b'. A C++ construction — `new log::Writer(…)`, `Writer w(…)` — makes the
|
||||
// class C++ lookup finds for the type as written there, or nothing.
|
||||
if (ref.language === 'cpp' && ref.referenceKind === 'instantiates') {
|
||||
const constructed = nmTimedT('cppConstruct', ref, () => matchCppConstruction(ref, context));
|
||||
if (constructed !== undefined) return constructed;
|
||||
}
|
||||
|
||||
// 1c. `::`-scoped factory chain — PHP `Cls::for($x)->method()` (#608) or Rust
|
||||
// `Foo::new().bar()`, both encoded as `Cls::factory().method`. The receiver's
|
||||
// type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
|
||||
|
||||
Reference in New Issue
Block a user