You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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>_API empty on the compiler command line (/D "MODULE_API=") — every UE plugin header uses it
CMakegenerate_export_header() emits <lib>_EXPORT used identically
Self-contained dummy snippet mirroring UE5 plugin conventions (reflection macros are harmless here — the breaking token is the DUMMYPLUGIN_API export macro):
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)
Command: codebase-memory-mcp cli index_repository '{"repo_path":"<dir>"}' then cli 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 { ... }
⚠️ 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:
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.
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.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.
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 Fooyields a class definition namedFoo.This pattern is generated by every symbol-exporting build system, not a niche convention:
<MODULE>_APIempty on the compiler command line (/D "MODULE_API=") — every UE plugin header uses itgenerate_export_header()emits<lib>_EXPORTused identicallyspdlog/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 LazyDescriptorReproduction
Self-contained dummy snippet mirroring UE5 plugin conventions (reflection macros are harmless here — the breaking token is the
DUMMYPLUGIN_APIexport macro):ue_style.hin an empty dir; theUCLASS/GENERATED_BODYtokens can be left as-is — they do not affect the result)codebase-memory-mcp cli index_repository '{"repo_path":"<dir>"}'thencli search_graph '{"project":"...","name_pattern":".*Dummy.*"}'grammar_cpp.c):class Foo { ... }(no macro)FooUCLASS()thenclass Foo : ...Foo(theUCLASS()call itself is a harmless ERROR)class DUMMYPLUGIN_API UDummyPoolSubsystem : ...DUMMYPLUGIN_API; real name swallowed into an ERROR regionstruct DUMMYPLUGIN_API FDummyCfg { ... }DUMMYPLUGIN_API— silent wrong name, no error flagenum class EDummyMode : uint8 { A, B };(isolated)EDummyModeDUMMYPLUGIN_API void DoThing();AST evidence for the class case:
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/ADummySpawnerall vanish into onefield_declarationfragment).In the real CBM graph (indexed a large UE5 plugin, v0.10.8) this surfaces as:
UObjectPoolSubsystemappears only as aFunction-label node named after the class, while theClasslabel has no entry for it — soquery_graph("MATCH (c:Class) WHERE c.name CONTAINS 'Subsystem'")returns 0 rows andtrace_pathinbound misses real callers (adjacent open issues #694/#763).Scale
Real-world UE5 plugin repo (731 C++ files): 230/364 headers carry
<MOD>_APIon a class/struct/enum declaration; 365 files flaggedparse_partial. The same shape exists in spdlog/fmt/protobuf and any CMakegenerate_export_headerproject, 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++ withextra_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_specifieritself parses "successfully"), so nothing is corrected.Proposed fix (direction — will implement per your preference)
The machinery already exists; this is assembly, not new infrastructure:
^[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.CBM_EXTRA_DEFINES) or project metadata (UE*.Build.csPublicDefinitions, CMake export headers) — complements (1), generally useful, zero heuristics.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
^[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)?tests/test_pipeline.c/tests/test_extraction.cand a spdlog-derived fixture as the shareable test bed.Confirmations