fix(cpp): recover export-macro-annotated classes instead of dropping them (#1061) (#1070)

A C++ class annotated with an export/visibility macro between `class`/`struct`
and the type name — `class MYMODULE_API UMyComponent : public UActorComponent`
(the standard Unreal-Engine `*_API` pattern), or the equivalent `*_EXPORT`/
`*_ABI` macros in Qt, Boost, LLVM, etc. — makes tree-sitter read `class MACRO`
as an elaborated type and the whole declaration as a function. #946 dropped the
resulting phantom function, but that also discarded the recoverable class name,
members, and base-class edge, so the class never entered the graph and
"find subclasses" / type-hierarchy / impact-through-inheritance returned
nothing for effectively every gameplay class in a UE project.

Add `blankCppExportMacros` as `cppExtractor.preParse`: it blanks the macro with
equal-length spaces before parsing (offset-preserving, like C#'s
`blankCsharpPreprocessorDirectives`/#237), so the declaration parses as a normal
class_specifier and existing extraction emits the node, members, and `extends`
edge. Generalized past UE `*_API` to any all-caps export macro, with two
false-positive guards: the trailing `[:{]` definition-guard (leaves elaborated
var decls like `struct FOO var;` alone) and requiring the macro to be followed
by the real name (leaves an all-caps class NAME such as `class FOO : public Base`
alone). C++-only, so C's heavier `struct TAG var;` never reaches it. The #946
drop stays as the fallback for any residual misparse the blanking doesn't catch.

Validated on google/leveldb (LEVELDB_EXPORT, 134 files): class/struct nodes
266→293, extends edges 292→359, phantom functions 588→513; every export-macro
real definition flips function→class and `EnvWrapper extends Env` goes
absent→present.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-29 21:03:23 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 2b256b93e5
commit e596c968ab
3 changed files with 139 additions and 14 deletions
+36 -1
View File
@@ -209,7 +209,40 @@ function isMacroMisparsedTypeDecl(node: SyntaxNode): boolean {
return true;
}
/**
* Blank an export/visibility macro in a `class/struct EXPORT_MACRO Name …`
* *definition* header before parsing. Not knowing the macro, tree-sitter reads
* `class EXPORT_MACRO` as an elaborated type specifier and the rest as a
* function, so the whole class — its name, base clause, and members — drops out
* of the index (#946 catches the resulting phantom function but can't recover
* the class), which silently breaks type-hierarchy / inheritance-impact queries
* for effectively every Unreal-Engine (`*_API`), Qt/Boost (`*_EXPORT`), LLVM
* (`*_ABI`), … class. Replacing the macro with equal-length spaces preserves
* every byte offset (and thus line/column), so the declaration then parses as a
* normal class_specifier and the existing extraction emits the node, members,
* and `extends` edge. (#1061, follow-up to #946.)
*
* Matched tightly so it can't touch the same macro used as an ordinary value
* elsewhere (`int x = SOME_API;`): the macro is the ALL-CAPS token sitting
* *between* `class`/`struct` and the type name, and the trailing `[:{]`
* definition-guard fires only when a base clause or body follows — the only
* shape that misparses. That guard also leaves elaborated-type variable
* declarations (`struct FOO var;`, `class FOO obj = …`) untouched, since those
* end in `;` / `=` / `[`, never `:` / `{`. C++-only (wired into cppExtractor),
* so C's heavier use of `struct TAG var;` never reaches it.
*/
export function blankCppExportMacros(source: string): string {
if (source.indexOf('class') === -1 && source.indexOf('struct') === -1) return source;
return source.replace(
/\b(class|struct)(\s+)([A-Z][A-Z0-9_]+)(?=\s+[A-Za-z_]\w*(?:\s+final)?\s*[:{])/g,
(_m, kw, ws, macro) => kw + ws + ' '.repeat(macro.length)
);
}
export const cppExtractor: LanguageExtractor = {
// Recover macro-annotated class/struct definitions (`class MYMODULE_API Foo : Base`)
// that tree-sitter otherwise misparses into a phantom function (#1061/#946).
preParse: blankCppExportMacros,
functionTypes: ['function_definition'],
classTypes: ['class_specifier'],
methodTypes: ['function_definition'],
@@ -262,7 +295,9 @@ export const cppExtractor: LanguageExtractor = {
const cppKeywords = ['switch', 'if', 'for', 'while', 'do', 'case', 'return'];
if (cppKeywords.includes(name)) return true;
// `class MACRO Name : public Base { … }` misparses to a function_definition
// named after the class — drop that phantom (#946).
// named after the class. `blankCppExportMacros` (preParse) recovers the
// common ALL-CAPS export-macro shape; this drop is the fallback for any
// residual misparse it doesn't blank — still no phantom function (#1061/#946).
return isMacroMisparsedTypeDecl(node);
},
extractImport: (node, source) => {