fix(extraction): skip bodiless C++ forward declarations (#1093) (#1095)

A `class Foo;` forward declaration parses as a bodiless class_specifier.
extractStruct (#831) and extractEnum already skip their bodiless forms,
but extractClass did not — so every forward decl across dozens of headers
minted a phantom bodiless `class` node that competed with, and could be
picked as the blast-radius representative over, the single real definition.

Add an opt-in `skipBodilessClass` extractor flag (set only on cppExtractor)
and skip a bodiless class node when it's set, mirroring the struct/enum
skip. The flag keeps this C/C++-scoped: languages where a bodiless class is
a complete definition (Kotlin `class Empty`, Scala `case object`/`trait`)
leave it unset and are unaffected. The body is now resolved once at the top
of extractClass and reused for the member walk.

Regression tests cover the collapse to a single definition, elaborated-type
references creating no phantom, and Kotlin/Scala staying indexed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-01 06:23:50 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent ad03d24fb9
commit f856f7ae49
5 changed files with 81 additions and 3 deletions
+6
View File
@@ -245,6 +245,12 @@ export const cppExtractor: LanguageExtractor = {
preParse: blankCppExportMacros,
functionTypes: ['function_definition'],
classTypes: ['class_specifier'],
// A bodiless `class_specifier` is a forward declaration (`class Foo;`) or an
// elaborated type reference, not a definition. Skip it so dozens of forward
// decls across headers don't mint phantom `class` nodes that crowd out — and
// get picked as the blast-radius representative over — the single real
// definition, exactly as bodiless struct/enum specifiers are already skipped. (#1093)
skipBodilessClass: true,
methodTypes: ['function_definition'],
interfaceTypes: [],
structTypes: ['struct_specifier'],
+8
View File
@@ -163,6 +163,14 @@ export interface LanguageExtractor {
extraClassNodeTypes?: string[];
/** Whether methods can be top-level without enclosing class (Go: true) */
methodsAreTopLevel?: boolean;
/**
* Skip a bodiless class node as a forward declaration / elaborated type,
* mirroring the bodiless-struct/enum skip. Set only for languages where a
* bodiless `class` specifier is NOT a complete definition — C/C++
* (`class Foo;` is a forward decl). Leave unset for languages where a
* bodiless class IS complete (Kotlin `class Empty`, Scala `case object`). (#1093)
*/
skipBodilessClass?: boolean;
/** NodeKind to use for interface-like declarations (Rust: 'trait'). Default: 'interface' */
interfaceKind?: NodeKind;
+10 -3
View File
@@ -1528,6 +1528,15 @@ export class TreeSitterExtractor {
private extractClass(node: SyntaxNode, kind: NodeKind = 'class'): void {
if (!this.extractor) return;
// Skip forward declarations / elaborated type references (`class Foo;`) in
// languages that opt in — bodiless there means "not a definition", so it
// would otherwise mint a phantom node competing with the real definition
// (#1093). Languages where a bodiless class is complete (Kotlin, Scala)
// leave the flag unset. Resolved once here and reused for the body walk.
const resolvedBody = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (this.extractor.skipBodilessClass && !resolvedBody) return;
const name = extractName(node, this.source, this.extractor);
const docstring = getPrecedingDocstring(node, this.source);
const visibility = this.extractor.getVisibility?.(node);
@@ -1551,9 +1560,7 @@ export class TreeSitterExtractor {
// Push to stack and visit body
this.nodeStack.push(classNode.id);
let body = this.extractor.resolveBody?.(node, this.extractor.bodyField)
?? getChildByField(node, this.extractor.bodyField);
if (!body) body = node;
const body = resolvedBody ?? node;
// Visit all children for methods and properties
for (let i = 0; i < body.namedChildCount; i++) {