diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ecf8f2e3..4137492a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Heavily-reflected Unreal Engine C++ classes are no longer dropped from the index. Reflection markup that decorates members — `UPROPERTY(...)`, `UFUNCTION(...)`, `UCLASS(...)`, `GENERATED_BODY()`, `UE_DEPRECATED_*(...)`, `DECLARE_DELEGATE_*(...)` — are no-semicolon macro calls that tree-sitter doesn't recognize, so each drops into error recovery; in a big class the errors pile up until the whole `class_specifier` collapses and the class, its base clause, and its members vanish (`UCharacterMovementComponent`, with ~240 such macros, disappeared entirely, breaking every subclass/type-hierarchy and blast-radius query that went through it). These line-leading annotation macros are now blanked (offset-preserving) before parsing so the class survives. Thanks @luoyxy for the report and root-cause analysis. (#1093 follow-up) - Unreal Engine members and methods prefixed by an export/visibility macro are no longer lost. The `*_API` macro doesn't only sit on the class header — it prefixes almost every exported member of a large UE class (`ENGINE_API virtual void Tick(...)`, `static ENGINE_API void AddReferencedObjects(...)`); the parser read the macro as an extra type token and each such declaration fell into error recovery, so on headers like `Actor.h` and `World.h` hundreds of return types piled up as orphan errors and could still tip the class into collapse. Member/method-level `*_API` / `*_EXPORT` / `*_ABI` macros (Unreal, Qt/Boost, LLVM) are now blanked before parsing, mirroring the existing class-header recovery. (#1093 follow-up) - Unreal Engine annotation macros that appear mid-line — an enum value's `UMETA(DisplayName=...)`, a parameter's `UPARAM(ref)`, or a deprecation tag wedged into a `using` alias (`using FOnNetTick UE_DEPRECATED(5.5, "...") = ...;`, which alone collapsed `UWorld` in `World.h`) — are now stripped too. These sit in positions the line-leading recovery structurally can't reach, and a single one could take down the surrounding enum or class. They are matched by an Unreal-only name list (`UMETA`, `UPARAM`, `UE_DEPRECATED*`) so no standard-C++ or other-library code is affected. Together these three fixes recover the main class of every large Unreal Engine header tested (`Actor`, `ActorComponent`, `SkeletalMeshComponent`, `World`, `LightComponent`, `CharacterMovementComponent`). (#1093 follow-up) +- A C++ header whose only C++ signal is an export-macro-annotated class is no longer misdetected as C — which had silently dropped the class. A `.h` file defaults to C and is only reclassified as C++ when it shows a C++-specific construct, but that check couldn't see through an export/visibility macro: `class ENGINE_API UFoo : public UObject` didn't match the macro-blind `class Name :` pattern, so a lean Unreal-Engine-style header carrying just `GENERATED_BODY()` and no explicit `public:` / `virtual` / `namespace` / `template` was parsed as C. The C extractor emits no class nodes, so the class — and its inheritance link — vanished from the graph, quietly undoing the macro-class recovery added in #1061. The C++ detection heuristic now recognizes an export-macro-annotated `class`/`struct` declaration (the same shape #1061 blanks before parsing), matching how `class Foo : Bar` was already detected; the two-token ` ` before a `[:{]` never occurs in valid C, so genuine C headers are unaffected. Thanks @luoyxy for the report and root-cause analysis. (#1133) ## [1.2.0] - 2026-07-02 diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 309bf2d30..8619d12d7 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -150,6 +150,31 @@ describe('Language Detection', () => { expect(isSourceFile('default.nix')).toBe(true); }); + it('should detect a .h whose only C++ signal is an export-macro class as cpp', () => { + // Lean Unreal-Engine style header: the class is annotated with an export + // macro and carries no explicit `public:`/`virtual`/`namespace`/`template`, + // so the macro-blind `class\s+\w+\s*[:{]` branch alone can't see it. It must + // still detect as C++ — otherwise the C extractor (classTypes: []) drops the + // class definition entirely. (#1093 follow-up) + const macroClassHeader = `#pragma once +#include "CoreMinimal.h" + +UCLASS() +class ENGINE_API UNetConnectionRepControl : public UObject +{ +\tGENERATED_BODY() +\tbool IsRepControlEnable() const; +}; +`; + expect(detectLanguage('NetConnectionRepControl.h', macroClassHeader)).toBe('cpp'); + // Macro class with no base clause, brace on the next line, still C++. + expect(detectLanguage('Foo.h', 'MYMODULE_API_DECL\nclass MYMODULE_API FFoo\n{\n\tint X;\n};\n')).toBe('cpp'); + // Export-macro struct with inheritance is likewise C++-only. + expect(detectLanguage('Bar.h', 'struct ENGINE_API FBar : public FBase {};\n')).toBe('cpp'); + // Guard: a genuine C header must NOT be dragged to C++ by the new branch. + expect(detectLanguage('cfoo.h', '#ifndef CFOO_H\nstruct Point { int x; int y; };\nvoid f(struct Point p);\n#endif\n')).toBe('c'); + }); + it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 2565e02a3..5a1b9ed42 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -394,7 +394,16 @@ export function detectLanguage(filePath: string, source?: string, overrides?: Re */ 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); + // The `class MACRO Name : Base` / `class MACRO Name { … }` branch mirrors what + // `blankCppExportMacros` recovers: an ALL-CAPS export/visibility macro + // (`ENGINE_API`, `MYMODULE_API`, `*_EXPORT`, …) sitting between `class`/`struct` + // and the type name. Without it, a header whose ONLY C++ signal is such a + // macro-annotated class — common for lean Unreal-Engine types that carry just + // `GENERATED_BODY()` and no explicit `public:`/`virtual` — is misdetected as C, + // routed through the C extractor (which extracts no classes), and its class + // definition silently vanishes. The two-token shape (` ` + // before a `[:{]`) never occurs in valid C, so this can't misclassify C headers. + return /\bnamespace\b|\bclass\s+\w+\s*[:{]|\b(?:class|struct)\s+[A-Z][A-Z0-9_]+\s+\w+\s*(?:final\s*)?[:{]|\btemplate\s*<|\b(?:public|private|protected)\s*:|\bvirtual\b|\busing\s+(?:namespace\b|\w+\s*=)/.test(sample); } /**