Parcourir la source

fix(extraction): detect export-macro-annotated class in .h language check (#1159) (#1207)

Carries #1133 forward onto current main (rebased for conflicts). A lean Unreal-Engine-style `.h` whose only C++ signal is `class ENGINE_API Foo : public Bar` (no public:/virtual/namespace/template) was misdetected as C and its class + inheritance edge silently dropped; looksLikeCpp now recognizes the export-macro-annotated class/struct shape, matching what blankCppExportMacros already recovers.

Fixes #1159.

Co-Authored-By: robertyluo <robertyluo@tencent.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry il y a 2 mois
Parent
commit
c049d9eb0d
3 fichiers modifiés avec 36 ajouts et 1 suppressions
  1. 1 0
      CHANGELOG.md
  2. 25 0
      __tests__/extraction.test.ts
  3. 10 1
      src/extraction/grammars.ts

+ 1 - 0
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 `<keyword> <MACRO> <Name>` 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
 

+ 25 - 0
__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');

+ 10 - 1
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 (`<KW> <MACRO> <Name>`
+  // 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);
 }
 
 /**