Skip to content

fix(cpp): extract templated and include-guarded declarations from C/C++ headers#535

Open
vitorvalecsw wants to merge 2 commits into
Egonex-AI:mainfrom
vitorvalecsw:fix/cpp-header-template-extraction
Open

fix(cpp): extract templated and include-guarded declarations from C/C++ headers#535
vitorvalecsw wants to merge 2 commits into
Egonex-AI:mainfrom
vitorvalecsw:fix/cpp-header-template-extraction

Conversation

@vitorvalecsw

Copy link
Copy Markdown

Problem

The C/C++ extractor produced empty structural output for most real headers. Two root causes in walkTopLevel (packages/core/src/plugins/extractors/cpp-extractor.ts), which only descended into namespace bodies:

  1. Include guards. A conventional header wraps its contents in
    #ifndef __X_H__ / #define __X_H__ / … / #endif, which tree-sitter parses as a
    preproc_ifdef node. The walker never entered it, so every header using an
    include guard yielded zero symbols
    (classes, functions, includes — all lost).
  2. Templates. template <…> class/struct/function parses as a
    template_declaration wrapping the entity — at file/namespace scope and as a
    class member. None of these were unwrapped, so all templated classes, structs,
    free functions, and templated member methods were dropped.

Because these two patterns are near-universal in real C++ headers (include guards especially), the extractor’s deterministic output for a typical header-heavy C++ project was effectively empty, forcing the downstream LLM pass to reconstruct everything from raw source.

Fix

  • Recurse into preproc_ifdef / preproc_if / preproc_else / preproc_elif blocks in walkTopLevel, so guard-wrapped declarations (and their #includes) are traversed.
  • Unwrap template_declaration to its inner class_specifier / struct_specifier / function_definition, both at top level and inside class member lists.

A useful side effect: because resolveImports shares the same walker, header→header #include edges (previously hidden inside guards) now resolve too.

Companion change (agents/file-analyzer.md)

The extraction script already emits a deterministic callGraph ({caller, callee, lineNumber}), but the calls-edge instruction only said “infer from imports + function names”, so the call graph went unused. The instruction now makes callGraph the primary source for calls edges (resolving each callee against imported/neighbor files, skipping std/external symbols).

Before / After (real C++ headers)

Header Before After
Queue.h (template<class T> class Queue) (no symbols) Queue + enqueue/dequeue/size/clear/…
SequentialUnorderedMap.h (template) (no symbols) class + 12 methods
Uuid.h (guarded, non-template) (no symbols) Uuid + 12 functions
Logger.h (guarded; variadic template methods) (no symbols) Logger + log/logf methods

Tests

Adds 6 regression tests to cpp-extractor.test.ts:

  • declarations inside an #ifndef include guard,
  • top-level templated class,
  • templated class nested in a namespace,
  • templated member method (definition inside a class),
  • top-level templated free function,
  • combined real-header shape (guard + namespace + templates).

Full core suite: 759/759 passing (pnpm --filter @understand-anything/core test), including the new tests.

Known limitation (out of scope)

Free (non-member) function prototypes at file/namespace scope — e.g. void freeFn(); declared in a header with the body in a .cpp — are still not emitted as nodes from the header (only the .cpp definition creates the node). Class method declarations are captured. This is a pre-existing, separate limitation from the guard/template bugs fixed here and is comparatively rare in class-centric C++ codebases; left for a follow-up to keep this change focused.

Notes

  • No changes to the tree-sitter grammar; grammars ship prebuilt binaries.
  • .h→language mapping was intentionally left unchanged (.h is ambiguous C vs C++, and CppExtractor already handles both language ids with the same grammar, so it does not affect extraction).

Vitor Vale and others added 2 commits July 2, 2026 17:49
The C/C++ structural walker (walkTopLevel) only descended into namespace
bodies, so two very common header constructs were silently skipped:

- Include guards: `#ifndef __X_H__ ... #endif` wraps a header's contents in a
  `preproc_ifdef` node that was never traversed, so every header using an
  include guard yielded zero symbols.
- Templates: `template <...> class/struct/function` parses as a
  `template_declaration` wrapping the entity — at file/namespace scope and as
  class members. These were skipped, dropping all templated classes, structs,
  free functions, and templated member methods.

Recurse into preproc_ifdef/if/else/elif blocks, and unwrap template_declaration
nodes to the inner class/struct/function (both top-level and inside class
bodies).

Adds regression tests: include guards, top-level and namespaced template
classes, templated member methods, templated free functions, and a combined
real-header shape (guard + namespace + templates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The extraction script already emits a deterministic callGraph
({caller, callee, lineNumber}) for code files, but the calls-edge instruction
only told the agent to "infer from imports + function names", so the callGraph
went unused and cross-file calls were rarely emitted.

Make the callGraph the primary source for calls edges: for each entry whose
callee resolves to a function/method of an imported/neighbor file, emit a
calls edge; skip standard-library/external callees.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant