Repository navigation
Description
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:
- Unreal Engine UBT defines
<MODULE>_APIempty on the compiler command line (/D "MODULE_API=") — every UE plugin header uses it - CMake
generate_export_header()emits<lib>_EXPORTused identically - Verified public code:
spdlog/include/spdlog/logger.h:50class SPDLOG_API logger;fmt/include/fmt/os.h:222class FMT_API file;protobuf/src/google/protobuf/descriptor.hclass PROTOBUF_EXPORT LazyDescriptor
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 };- Code: the snippet above (save as
ue_style.hin an empty dir; theUCLASS/GENERATED_BODYtokens can be left as-is — they do not affect the result) - Command:
codebase-memory-mcp cli index_repository '{"repo_path":"<dir>"}'thencli search_graph '{"project":"...","name_pattern":".*Dummy.*"}' - 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 { ... } |
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:
- 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 emptyNAME=entries toextra_definesforcbm_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 viaoriginal_line_by_expanded_linealready exists). Locally verified with tree-sitter-cpp: expanding-DDUMMYPLUGIN_API= -DMYMODULE_API=flips extraction toUDummyPoolSubsystem/FDummyCfgcorrectly. - Plumb user/project defines: accept defines from config (e.g.
CBM_EXTRA_DEFINES) or project metadata (UE*.Build.csPublicDefinitions, CMake export headers) — complements (1), generally useful, zero heuristics. - Extractor-level fallback (cheapest): when
class_specifier.namematches the export-macro shape and an ERROR region holds the followingidentifier, 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.cand a spdlog-derived fixture as the shareable test bed.
Confirmations
- I searched existing issues and this is not a duplicate. Closest: task: Parsing & extraction quality — language/format coverage gaps #592 (tracking), C/C++ CALLS edges are attributed to the File/Module, not the enclosing function — callers are file-level, callees are empty (Python is correct) #694/CALLS edges fall back to File/Module node when enclosing_func_qn is unset — trace_path returns empty (root cause for #694/#480/#686/#678/#523) #763 (C/C++ CALLS attribution — adjacent, distinct root cause), bug for parse c++ #946 (generic C++ parse bug).
- My reproduction uses shareable code (self-contained dummy snippet + public OSS repos spdlog / fmt / protobuf).
Activity
DeusData commentedon Sep 1, 2026
Thank you for asking before opening the implementation. Please open a focused PR for option 1 only: collect a bounded set of conventional export-macro candidates and feed empty definitions into the existing C/C++ preprocessed second pass. Keep the candidate count capped and include negative controls for ordinary all-caps identifiers so the heuristic cannot silently rename legitimate code. Please keep user-configured define plumbing out of that PR; it creates a separate public configuration and precedence surface that we should review independently. The public fixtures and the class, struct, enum, and free-function cases you listed are the right test boundary. Thank you for doing the design check before investing in a larger combined change.
fix(parsing): recover export-macro-hidden type definitions (DeusData#…
fix(parsing): recover export-macro-hidden type definitions (DeusData#…
Merge pull request #2005 from XIYBHK/fix/1989-export-macro-parsing
DeusData commentedon Sep 21, 2026
The zero-config shape rule (option 1) is on
mainwith #2005, merge commit c7488b0: types, enums, free functions and inline methods hidden behind conventional export macros are now recovered without configuration, and explicitly configured defines keep priority. It ships in the next patch release; a build frommainhas it now.An explicit override for macros the shape rule cannot see (macros with arguments, unconventional names) and for excluding a name that is a genuine symbol — option 2 — is the open follow-up, so I am leaving this issue open for it. Thank you @XIYBHK for the implementation.
fix(parsing): recover export-macro-hidden type definitions (DeusData#…
fix(parsing): recover export-macro-hidden type definitions (DeusData#…
fix(extract): one definition per site when the raw and preprocessed p…