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
+53
View File
@@ -2811,6 +2811,59 @@ class MYGAME_API UMyComponent : public UActorComponent { };
});
});
describe('C++ forward declarations do not mint phantom class nodes (#1093)', () => {
// `class Foo;` parses as a bodiless class_specifier. Repeated across headers,
// each forward decl minted a phantom bodiless `class` node that crowded out —
// and could be picked as the blast-radius representative over — the single
// real definition. Bodiless struct/enum specifiers were already skipped;
// classes now are too, but ONLY for C/C++ (opt-in flag), never for languages
// where a bodiless class is a complete definition.
it('keeps only the real definition, dropping repeated forward decls', () => {
const code = `
class APXCharacter; // forward decl (header 1)
class APXCharacter; // forward decl (header 2)
friend class APXCharacter; // elaborated / friend forward reference
class APXCharacter { // the one real definition
int hp;
void takeDamage(int amount) { hp -= amount; }
};
`;
const result = extractFromSource('character.cpp', code);
const classes = result.nodes.filter(
(n) => n.kind === 'class' && n.name === 'APXCharacter'
);
// Exactly one class node, and it's the definition (carries the member).
expect(classes).toHaveLength(1);
expect(classes[0].startLine).toBe(6);
expect(
result.nodes.some((n) => n.kind === 'method' && n.name === 'takeDamage')
).toBe(true);
});
it('elaborated type references in declarations create no phantom class', () => {
// `class Foo obj;` is a variable declaration using an elaborated type, not
// a class definition — it must not mint a `Foo` class node.
const result = extractFromSource('use.cpp', 'class Foo;\nvoid f() { class Foo *p = nullptr; (void)p; }\n');
expect(result.nodes.filter((n) => n.kind === 'class' && n.name === 'Foo')).toHaveLength(0);
});
it('does NOT affect languages where a bodiless class is complete', () => {
// Kotlin `class Empty` and Scala `trait`/`case object`/`class` with no body
// are complete definitions — the C/C++-only skip must leave them indexed.
const kt = extractFromSource('Empty.kt', 'class Empty\nclass Full { val x = 1 }\n');
const ktClasses = kt.nodes.filter((n) => n.kind === 'class').map((n) => n.name);
expect(ktClasses).toContain('Empty');
expect(ktClasses).toContain('Full');
const scala = extractFromSource('M.scala', 'trait Marker\ncase object Red\nclass Foo\n');
const scalaNames = scala.nodes
.filter((n) => ['class', 'trait', 'interface'].includes(n.kind))
.map((n) => n.name);
expect(scalaNames).toEqual(expect.arrayContaining(['Marker', 'Red', 'Foo']));
});
});
describe('C++ templated base-class inheritance (#1043)', () => {
// Inheriting from a template (`class D : public Base<int>`) recorded the base
// ref as the full instantiation `Base<int>`, which never name-matched the