mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 13:14:03 +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)
|
||||
|
||||
Reference in New Issue
Block a user