Repository: colbymchenry/codegraph (2026-07-07T17:18:34Z)

Issue:
UE-style single-line export-macro class headers (class ENGINE_API Foo : public Bar) are misdetected as C and their class definitions are dropped
## Summary

In a `.h` header, a class declared with an all-caps export/visibility macro between the `class`/`struct` keyword and the type name — the ubiquitous Unreal Engine / library form `class ENGINE_API Foo : public Bar` — is misclassified by the header language heuristic as **C** instead of **C++**. Parsed with the C grammar, the class definition is never recovered and disappears from the graph.

## Reproduction

Header `Foo.h`:

```cpp
class ENGINE_API Foo : public Bar
{
public:
    void Tick();
};
```

Index the file → there is no `class` node for `Foo` and no `extends Bar` edge.

## Impact

Any `.h` using the `class MODULE_API Type : Base` form — standard across Unreal Engine and the many C/C++ libraries that use `*_API` / `*_EXPORT` export macros — loses its class definition, its base clause, and its members. That breaks type-hierarchy, callers, and blast-radius queries that flow through the type.

## Root cause

`looksLikeCpp` (`src/extraction/grammars.ts`) recognizes `class Foo : Base` but not `class MACRO Foo : Base`: the macro token between the keyword and the identifier defeats the C++ signal, so ambiguous `.h` files fall back to the C grammar.

## Fix

PR #1133 — extend the `looksLikeCpp` pattern to also accept an all-caps macro between `class`/`struct` and the identifier before the `:` / `{`.



Discussion:

Patch:
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 `<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
 
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 (`<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);
 }
 
 /**


Test patch:
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');