From d0f82e0069854c42541ea2dd1d89e712256cb05e Mon Sep 17 00:00:00 2001 From: maxvon Date: Sun, 5 Jul 2026 15:17:33 +0800 Subject: [PATCH] Detect export-macro class/struct .h headers as C++, not C (#1159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `.h` whose only C++ tell is an all-caps export macro between the keyword and the type name — the ubiquitous UE / library form `class ENGINE_API Foo : Base` — was misclassified as C by looksLikeCpp, so the file fell back to the C grammar and its whole class definition (node, members, base clause) vanished from the graph. The existing #1061 macro-blanking never got a chance to run because detection had already routed the header to C, one step upstream. - looksLikeCpp: the class/struct signal now tolerates an optional all-caps macro between the keyword and the name. `struct` is matched only in its base-clause form (`struct X : Base`) — that `:` is C++-exclusive, whereas plain `struct X { … }` is valid C and stays C. - Tests: detection for macro class (with/without base), macro struct, plus regression guards (plain class:base stays cpp, plain C struct{} stays c); and an end-to-end case proving a macro-only header now recovers the class node and its `extends` edge. Verified: full vitest suite green (124 files, 2121 passed). --- __tests__/extraction.test.ts | 36 ++++++++++++++++++++++++++++++++++++ src/extraction/grammars.ts | 11 ++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 61ffa64a0..581372f92 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -102,6 +102,42 @@ describe('Language Detection', () => { expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c'); }); + it('should detect a .h with an export-macro class/struct header as C++ (#1159)', () => { + // `class MODULE_API Foo : Base` — the ubiquitous Unreal Engine / library + // export-macro form. The macro between `class` and the name previously + // defeated the C++ signal, so the header fell back to C and the class was + // dropped. Case is stripped of any other C++ tell (no `public:`) so only the + // class/struct signal itself is under test. + expect(detectLanguage('Foo.h', 'class ENGINE_API Foo : public Bar\n{\n void Tick();\n};\n')).toBe('cpp'); + expect(detectLanguage('Foo.h', 'class CORE_API Widget\n{\n void draw();\n};\n')).toBe('cpp'); + expect(detectLanguage('Widget.h', 'struct MODULE_API Widget : Base\n{\n int n;\n};\n')).toBe('cpp'); + // Regressions guards: the plain forms must keep working, and a plain C + // `struct X { … }` (valid C, base-clause absent) must stay C. + expect(detectLanguage('Foo.h', 'class Foo : public Bar\n{\n};\n')).toBe('cpp'); + expect(detectLanguage('Point.h', 'struct Point {\n int x;\n int y;\n};\n')).toBe('c'); + }); + + it('recovers the class from an export-macro-only .h header end-to-end (#1159)', () => { + // The issue's impact: with detection fixed, the header now reaches the C++ + // grammar + #1061 macro-blanking, so the class, its member, and its base + // edge are all recovered. The header carries NO other C++ tell (no `public:`, + // no `#define`) — the macro class is the sole signal, which is exactly the + // case that regressed to C before. + const header = `class ENGINE_API Foo : public Bar +{ + void Tick(); +}; +`; + expect(detectLanguage('Foo.h', header)).toBe('cpp'); + const result = extractFromSource('Foo.h', header); + expect(result.nodes.find((n) => n.name === 'Foo')?.kind).toBe('class'); + expect( + result.unresolvedReferences.find( + (r) => r.referenceKind === 'extends' && r.referenceName === 'Bar' + ) + ).toBeTruthy(); + }); + it('should detect Metal shader files as C++ (#1121)', () => { expect(detectLanguage('Shaders.metal')).toBe('cpp'); expect(isSourceFile('Renderer/Shaders.metal')).toBe(true); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index aef4cac22..f269edf52 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -373,10 +373,19 @@ export function detectLanguage(filePath: string, source?: string, overrides?: Re /** * Heuristic: does a .h file contain C++ constructs? * Checks the first ~8KB for patterns that are unique to C++ and never valid C. + * + * The `class`/`struct` signals tolerate an optional all-caps export/visibility + * macro between the keyword and the type name — the ubiquitous Unreal Engine / + * library form `class ENGINE_API Foo : Base` (#1159). Without this, such a + * header (when it carries no other C++ tell like `public:` in the first 8KB) + * falls back to the C grammar and its whole class definition is dropped. + * `struct` is matched only in its base-clause form (`struct X : Base`) — that + * `:` is C++-exclusive, whereas a plain `struct X { … }` is valid C and must + * stay `c`. */ function looksLikeCpp(source: string): boolean { const sample = source.substring(0, 8192); - return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample); + return /\bnamespace\b|\bclass\s+(?:[A-Z_][A-Z0-9_]*\s+)?\w+\s*[:{]|\bstruct\s+(?:[A-Z_][A-Z0-9_]*\s+)?\w+\s*:|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample); } /**