Skip to content

C/C++: export macros (<MODULE>_API / <lib>_EXPORT) between class-key and name extract the macro as the type name; enums and free functions are lost #1989

Description

@XIYBHK

Version

codebase-memory-mcp 0.10.8

Platform

Windows (x64)

Install channel

GitHub release archive / install.ps1

Variant

standard

What happened, and what did you expect?

Indexing C/C++ headers that place a build-system export macro (<MODULE>_API, <lib>_EXPORT, …) between the class-key and the type name extracts the macro token as the type name instead of the real class name — and in several forms (enums, free functions) the declaration is lost entirely (ERROR region → parse_partial).

Expected: class MYMODULE_API Foo yields a class definition named Foo.

This pattern is generated by every symbol-exporting build system, not a niche convention:

Reproduction

Self-contained dummy snippet mirroring UE5 plugin conventions (reflection macros are harmless here — the breaking token is the DUMMYPLUGIN_API export macro):

// ue_style.h — minimal UE-style header
#pragma once
#include "CoreMinimal.h"

UCLASS(BlueprintType)
class DUMMYPLUGIN_API UDummyPoolSubsystem : public UTickableWorldSubsystem
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category = "Dummy|Pool")
    AActor* SpawnActorFromPool(UClass* ActorClass, const FTransform& SpawnTransform);
private:
    UPROPERTY()
    TMap<UClass*, TArray<TObjectPtr<AActor>>> Pools;
};

UINTERFACE(MinimalAPI, BlueprintType)
class UDummyPoolable : public UInterface
{
    GENERATED_BODY()
};

class IDummyPoolable
{
    GENERATED_BODY()
public:
    virtual void OnAcquiredFromPool(AActor* Actor) = 0;
};

UCLASS()
class ADummySpawner : public AActor
{
    GENERATED_BODY()
public:
    ADummySpawner();
    UPROPERTY(EditAnywhere, Category = "Dummy")
    int32 PoolSize = 16;
};

struct DUMMYPLUGIN_API FDummyCfg { int32 X; };
enum class EDummyMode : uint8 { A, B };
  1. Code: the snippet above (save as ue_style.h in an empty dir; the UCLASS/GENERATED_BODY tokens can be left as-is — they do not affect the result)
  2. Command: codebase-memory-mcp cli index_repository '{"repo_path":"<dir>"}' then cli search_graph '{"project":"...","name_pattern":".*Dummy.*"}'
  3. Actual extraction of declarations (probed directly against tree-sitter-cpp, same grammar family as the vendored grammar_cpp.c):
Declaration Extracted
class Foo { ... } (no macro) ✅ Class Foo
UCLASS() then class Foo : ... ✅ Class Foo (the UCLASS() call itself is a harmless ERROR)
class DUMMYPLUGIN_API UDummyPoolSubsystem : ... ❌ Class named DUMMYPLUGIN_API; real name swallowed into an ERROR region
struct DUMMYPLUGIN_API FDummyCfg { ... } ⚠️ Struct named DUMMYPLUGIN_API — silent wrong name, no error flag
enum class EDummyMode : uint8 { A, B }; (isolated) ✅ Enum EDummyMode
DUMMYPLUGIN_API void DoThing(); ❌ Function lost (ERROR)

AST evidence for the class case:

function_definition
  class_specifier
    class
    type_identifier    # 'DUMMYPLUGIN_API'   <- macro taken as the name
  ERROR                <- real name 'UDummyPoolSubsystem' lands here
    identifier
    :  public

UINTERFACE-style blocks cascade worse: one parse error makes a single ERROR region swallow every following top-level declaration in the header (verified: UDummyPoolable/IDummyPoolable/ADummySpawner all vanish into one field_declaration fragment).

In the real CBM graph (indexed a large UE5 plugin, v0.10.8) this surfaces as: UObjectPoolSubsystem appears only as a Function-label node named after the class, while the Class label has no entry for it — so query_graph("MATCH (c:Class) WHERE c.name CONTAINS 'Subsystem'") returns 0 rows and trace_path inbound misses real callers (adjacent open issues #694/#763).

Scale

Real-world UE5 plugin repo (731 C++ files): 230/364 headers carry <MOD>_API on a class/struct/enum declaration; 365 files flagged parse_partial. The same shape exists in spdlog/fmt/protobuf and any CMake generate_export_header project, so this is a general C/C++ extraction gap.

Root cause

tree-sitter parses without preprocessor state, so MODULE_API (empty on the real compile command line) is an ordinary identifier → class_specifier.name = MODULE_API. The existing simplecpp second pass (cbm.c:~1397) already re-parses C/C++ with extra_defines, but its defs rescue (#961) only adopts defs that intersect raw ERROR regions and whose name line exists in raw source — the raw pass already supplied the wrong name (class_specifier itself parses "successfully"), so nothing is corrected.

Proposed fix (direction — will implement per your preference)

The machinery already exists; this is assembly, not new infrastructure:

  1. Synthesized empty defines (recommended): for C/C++, collect candidate export macros from the raw source (identifier shape ^[A-Z][A-Z0-9_]*_(API|EXPORT|IMPORT|DLLEXPORT|DEPRECATED)$, capped), add them as empty NAME= entries to extra_defines for cbm_preprocess_with_map, then let the existing second pass + C extractor: functions with #ifdef-split braces are dropped from the graph (no Function node) #961 rescue adopt corrected defs (line remap via original_line_by_expanded_line already exists). Locally verified with tree-sitter-cpp: expanding -DDUMMYPLUGIN_API= -DMYMODULE_API= flips extraction to UDummyPoolSubsystem / FDummyCfg correctly.
  2. Plumb user/project defines: accept defines from config (e.g. CBM_EXTRA_DEFINES) or project metadata (UE *.Build.cs PublicDefinitions, CMake export headers) — complements (1), generally useful, zero heuristics.
  3. Extractor-level fallback (cheapest): when class_specifier.name matches the export-macro shape and an ERROR region holds the following identifier, adopt that identifier as the name. Doesn't fix enums/functions.

(1) fixes class/struct/enum/function loss in one place and reuses battle-tested code; (2) is the deterministic path for compile_commands-less projects; (3) is a minimal stopgap. No vendored-grammar changes.

Questions

  • Is the ^[A-Z0-9_]*_(API|EXPORT|IMPORT|DLLEXPORT|DEPRECATED)$ shape acceptable as the candidate-macro filter, or would you prefer an explicit config-driven list only (option 2)?
  • Since this changes what gets extracted (per CONTRIBUTING), may I open a focused PR for (1)+(2)? Would keep it <500 lines with tests in tests/test_pipeline.c / tests/test_extraction.c and a spdlog-derived fixture as the shareable test bed.

Confirmations

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions