Skip to content

refactor(filtered ast visitor): refactor DocumentSymbolCollector to fit the CRTP design of RecursiveASTVisitor#427

Open
Perdixky wants to merge 4 commits intoclice-io:mainfrom
Perdixky:refactor/remove-redundant-ast-filters
Open

refactor(filtered ast visitor): refactor DocumentSymbolCollector to fit the CRTP design of RecursiveASTVisitor#427
Perdixky wants to merge 4 commits intoclice-io:mainfrom
Perdixky:refactor/remove-redundant-ast-filters

Conversation

@Perdixky
Copy link
Copy Markdown
Contributor

@Perdixky Perdixky commented Apr 17, 2026

  • Refactor
    • Refactor the logic of DocumentSymbolCollector to fit the CRTP design of RecursiveASTVisitor, and simplify some bloat code at the same time.

Copilot AI review requested due to automatic review settings April 17, 2026 11:33
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Traversal logic was simplified across the AST visitors: explicit suppression of implicit declarations and implicit template instantiations was removed, DocumentSymbol collection now uses an explicit push_symbol pattern with per-decl Traverse overrides, and full-AST traversal is always invoked in SemanticVisitor::run() regardless of prior interest gating.

Changes

Cohort / File(s) Summary
Filtered AST Visitor
src/semantic/filtered_ast_visitor.h
Removed special-case suppression for decl->isImplicit() and implicit template-instantiation checks; TraverseDecl now delegates directly to Base::TraverseDecl(decl) except for the existing TranslationUnitDecl handling when interested_only is enabled.
Document Symbols Collector
src/feature/document_symbols.cpp
Replaced on_traverse_decl gating with a push_symbol(NamedDecl*) helper and RAII cursor management. Added explicit Traverse*Decl overrides for many decl kinds that call push_symbol then Base::Traverse*Decl. Removed is_interested filtering and dynamic dispatch used previously.
Semantic Visitor
src/semantic/semantic_visitor.h
Always calls Base::TraverseAST(unit.context()) (removed conditional top-level-only traversal when interested_only was set). Simplified macro-kind mapping in directive processing to a single conditional expression. Added compile/directive.h include.

Sequence Diagram(s)

sequenceDiagram
    participant AST as clang::ASTContext
    participant Filtered as FilteredASTVisitor (Derived)
    participant Base as Base::TraverseDecl
    participant Symbols as DocumentSymbolCollector
    participant Semantic as SemanticVisitor

    AST->>Filtered: start TraverseAST
    Filtered->>Base: TraverseDecl(decl)  -- direct delegation (no implicit suppression)
    Base->>Filtered: visit child decls / callbacks
    alt NamedDecl encountered (DocumentSymbols)
        Symbols->>Symbols: push_symbol(NamedDecl)
        Symbols->>Base: Base::Traverse*Decl(child)
    end
    Semantic->>AST: run() always calls Base::TraverseAST(unit.context())
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through nodes both shy and slight,
No hidden leaves to hide from sight,
I pushed each symbol, nested neat,
The AST and I now gladly meet,
A rabbit's joy — traversal done right. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The PR title focuses on refactoring DocumentSymbolCollector to fit CRTP design, which is addressed in the changes, but the PR objectives emphasize removing redundant AST filters. The title is partially related but doesn't capture the main objective. Update the title to reflect the primary objective: 'refactor: remove redundant AST filters' or 'refactor(visitor): remove redundant AST declaration filters to simplify traversal logic'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/setup-llvm.py`:
- Around line 205-215: The config parsing loop in scripts/setup-llvm.py is too
strict: change the loop that reads cfg.read_text(...) so it normalizes each line
(strip leading/trailing whitespace), accepts both "--sysroot=PATH" and
"--sysroot PATH" forms, strips surrounding quotes from PATH, resolves and
validates the Path, and continue scanning all lines but keep the last valid
sysroot found (instead of returning the first); ensure you still handle OSError
as before and return None if no valid sysroot is found. Use the existing
variables/functions (cfg.read_text, sysroot, Path.resolve(), sysroot.exists())
to locate and update the parsing logic.
- Around line 341-358: The code currently sets needs_install=True when a
provided candidate exists but fails llvm_install_matches_compiler, but leaves
install_path unset so the bundled LLVM is extracted into install_root instead of
the user-specified path; update the branch that checks
llvm_install_matches_compiler(candidate) to explicitly honor the user's
candidate by setting install_path = candidate (and leave needs_install = True so
installation proceeds), and change the log message to clearly state "will
install bundled LLVM to provided path {candidate} (outside active compiler
sysroot)"; reference the symbols args.install_path, candidate,
llvm_install_matches_compiler, install_path, needs_install and install_root to
locate and modify the logic and message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f72d2ccd-4edb-4fa8-b426-72644940a542

📥 Commits

Reviewing files that changed from the base of the PR and between d42d9d5 and 0769540.

📒 Files selected for processing (3)
  • cmake/llvm.cmake
  • scripts/setup-llvm.py
  • src/semantic/filtered_ast_visitor.h

Comment thread scripts/setup-llvm.py Outdated
Comment thread scripts/setup-llvm.py Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors AST traversal to remove redundant implicit-declaration/template-instantiation filtering, and hardens LLVM dependency setup to avoid linking against LLVM installs that are ABI-incompatible with sandboxed toolchains (e.g., pixi/NixOS).

Changes:

  • Simplifies FilteredASTVisitor by dropping explicit filtering of implicit decls and implicit template instantiations.
  • Enhances setup-llvm.py to detect the active compiler sysroot and reject LLVM installs outside it.
  • Updates LLVM CMake integration to also include ${binary_dir}/include so downloaded “private” Clang headers are found.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/semantic/filtered_ast_visitor.h Removes implicit/implicit-instantiation skip logic from decl traversal.
scripts/setup-llvm.py Adds compiler sysroot detection and filters out LLVM installs that fall outside the sysroot.
cmake/llvm.cmake Adds build-tree include directory for fetched private Clang headers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/setup-llvm.py Outdated
@Perdixky Perdixky force-pushed the refactor/remove-redundant-ast-filters branch from 0769540 to 4fa9fe6 Compare April 17, 2026 11:39
@Perdixky Perdixky changed the title Refactor/remove redundant ast filters refactor(filtered ast visitor): remove redundant ast filters Apr 18, 2026
Perdixky and others added 2 commits April 19, 2026 16:26
RecursiveASTVisitor already skips implicit decls and implicit
template instantiations by default via shouldVisitImplicitCode()
and shouldVisitTemplateInstantiations().

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Perdixky Perdixky force-pushed the refactor/remove-redundant-ast-filters branch from 352fcbf to 40bb6ed Compare April 19, 2026 08:52
Move DocumentSymbolCollector off the generic decl hook.
This keeps document-symbol logic local and lets RecursiveASTVisitor retain its normal filtering and child traversal.
@Perdixky Perdixky force-pushed the refactor/remove-redundant-ast-filters branch from 40bb6ed to 0d606b7 Compare April 19, 2026 09:30
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/semantic/filtered_ast_visitor.h (1)

36-52: Stale comment and redundant branch after the simplification.

Two small cleanups are worth doing here:

  1. The comment on lines 36–37 ("Clang doesn't visit implicit declarations and implicit template instantiations by default") used to justify the explicit skip logic that has now been removed. In its current position it sits above the TranslationUnitDecl branch, which is unrelated — it reads as explaining the TU handling. Either move it back to where the implicit-decl filtering reasoning applies (or drop it).
  2. Once interested_only is false, both branches do exactly return Base::TraverseDecl(decl);, so the outer isa<TranslationUnitDecl> block can collapse into a single early return guarded by interested_only.
♻️ Suggested tightening
-        /// Clang doesn't visit implicit declarations and
-        /// implicit template instantiations by default.
-        if(llvm::isa<clang::TranslationUnitDecl>(decl)) {
-            if(interested_only) {
-                for(auto top: unit.top_level_decls()) {
-                    if(!TraverseDecl(top)) {
-                        return false;
-                    }
-                }
-                return true;
-            }
-
-            return Base::TraverseDecl(decl);
-        }
-
-        return Base::TraverseDecl(decl);
+        /// When `interested_only` is set, only walk the top-level decls of the
+        /// interested file instead of the full translation unit. Implicit decls
+        /// and implicit template instantiations are skipped by
+        /// `RecursiveASTVisitor` by default, so no extra filtering is needed.
+        if(interested_only && llvm::isa<clang::TranslationUnitDecl>(decl)) {
+            for(auto top: unit.top_level_decls()) {
+                if(!TraverseDecl(top)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        return Base::TraverseDecl(decl);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/semantic/filtered_ast_visitor.h` around lines 36 - 52, The comment about
implicit declarations is stale and the TranslationUnitDecl branch has redundant
returns; remove or relocate the comment (it belongs with the implicit-decl
filtering logic, not above the TranslationUnitDecl handling), then simplify the
control flow: if interested_only is true, iterate unit.top_level_decls() and
call TraverseDecl(top) (return false on failure and true after the loop),
otherwise just call and return Base::TraverseDecl(decl); eliminate the
unnecessary isa<clang::TranslationUnitDecl> outer branch so TranslationUnitDecl
handling only appears as the interested_only early-path and otherwise falls
through to Base::TraverseDecl.
src/feature/document_symbols.cpp (1)

115-145: push_symbol relies on guaranteed copy elision of Guard — worth a quick defensive tweak.

The pattern returns a locally-defined Guard by value from push_symbol and captures it with auto guard = this->push_symbol(decl);. C++17 mandatory prvalue copy elision makes this sound today, and the user-provided destructor suppresses the implicit move so any accidental future refactor that forces a copy (e.g., binding through a function-like wrapper, storing in a variant) would invoke the implicit copy constructor and run the source Guard’s destructor prematurely, restoring result.cursor before traversal of children completes.

Consider either marking Guard as move-only (and defaulting the move constructor while deleting copy), or clearing previous in the moved-from guard. Low priority — current code is correct.

♻️ Defensive hardening
         struct Guard {
             std::vector<InternalSymbol>* previous;
             std::vector<InternalSymbol>** cursor;
 
+            Guard(const Guard&) = delete;
+            Guard& operator=(const Guard&) = delete;
+            Guard(Guard&& other) noexcept
+                : previous(std::exchange(other.previous, nullptr)),
+                  cursor(other.cursor) {}
+            Guard& operator=(Guard&&) = delete;
+
             ~Guard() {
                 if(previous) {
                     *cursor = previous;
                 }
             }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/feature/document_symbols.cpp` around lines 115 - 145, push_symbol returns
a local Guard with a non-trivial destructor which can be accidentally copied and
prematurely restore result.cursor; make Guard move-only to prevent accidental
copies by deleting its copy constructor/assignment and defaulting the move
ctor/assign (or alternatively ensure moved-from Guard clears previous so its
destructor is no-op). Update the inner struct Guard in push_symbol: delete copy
ctor/assign and default move ctor/assign (or clear previous on move) and keep
the destructor logic to restore result.cursor from previous.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/feature/document_symbols.cpp`:
- Around line 148-170: The TRAVERSE_SYMBOL_DECL macro currently omits
typedef/type alias nodes so TypedefNameDecl handling in symbol_detail is dead:
update the macro invocations to also include Typedef, TypeAlias, and
TypeAliasTemplate (i.e., add TRAVERSE_SYMBOL_DECL(Typedef);
TRAVERSE_SYMBOL_DECL(TypeAlias); TRAVERSE_SYMBOL_DECL(TypeAliasTemplate);) so
push_symbol is called for those decls and symbol_detail's TypedefNameDecl branch
is reachable; alternatively, if you prefer removing dead code, delete the
TypedefNameDecl/type alias branch from symbol_detail. After making the change,
add unit/integration tests that assert typedef and using/type-alias-template
declarations appear in the document-symbols output.

---

Nitpick comments:
In `@src/feature/document_symbols.cpp`:
- Around line 115-145: push_symbol returns a local Guard with a non-trivial
destructor which can be accidentally copied and prematurely restore
result.cursor; make Guard move-only to prevent accidental copies by deleting its
copy constructor/assignment and defaulting the move ctor/assign (or
alternatively ensure moved-from Guard clears previous so its destructor is
no-op). Update the inner struct Guard in push_symbol: delete copy ctor/assign
and default move ctor/assign (or clear previous on move) and keep the destructor
logic to restore result.cursor from previous.

In `@src/semantic/filtered_ast_visitor.h`:
- Around line 36-52: The comment about implicit declarations is stale and the
TranslationUnitDecl branch has redundant returns; remove or relocate the comment
(it belongs with the implicit-decl filtering logic, not above the
TranslationUnitDecl handling), then simplify the control flow: if
interested_only is true, iterate unit.top_level_decls() and call
TraverseDecl(top) (return false on failure and true after the loop), otherwise
just call and return Base::TraverseDecl(decl); eliminate the unnecessary
isa<clang::TranslationUnitDecl> outer branch so TranslationUnitDecl handling
only appears as the interested_only early-path and otherwise falls through to
Base::TraverseDecl.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 328f5477-a2d0-4aa3-994a-8927f3e9ea1c

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa9fe6 and 0d606b7.

📒 Files selected for processing (3)
  • src/feature/document_symbols.cpp
  • src/semantic/filtered_ast_visitor.h
  • src/semantic/semantic_visitor.h

Comment on lines +148 to +170
#define TRAVERSE_SYMBOL_DECL(type) \
bool Traverse##type##Decl(clang::type##Decl* decl) { \
auto guard = this->push_symbol(decl); \
return Base::Traverse##type##Decl(decl); \
}

TRAVERSE_SYMBOL_DECL(Namespace);
TRAVERSE_SYMBOL_DECL(Enum);
TRAVERSE_SYMBOL_DECL(EnumConstant);
TRAVERSE_SYMBOL_DECL(Function);
TRAVERSE_SYMBOL_DECL(CXXMethod);
TRAVERSE_SYMBOL_DECL(CXXConstructor);
TRAVERSE_SYMBOL_DECL(CXXDestructor);
TRAVERSE_SYMBOL_DECL(CXXConversion);
TRAVERSE_SYMBOL_DECL(CXXDeductionGuide);
TRAVERSE_SYMBOL_DECL(Record);
TRAVERSE_SYMBOL_DECL(CXXRecord);
TRAVERSE_SYMBOL_DECL(Field);
TRAVERSE_SYMBOL_DECL(Var);
TRAVERSE_SYMBOL_DECL(Binding);
TRAVERSE_SYMBOL_DECL(Concept);

#undef TRAVERSE_SYMBOL_DECL
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any tests asserting presence/absence of typedef/type-alias entries in document symbols
rg -nP --type=cpp -C3 'document_symbols|DocumentSymbol' -g '*test*'
rg -nP --type=cpp -C2 '\b(typedef|using .* =)' -g '*document*symbol*test*'

Repository: clice-io/clice

Length of output: 5174


🏁 Script executed:

# Find and read the document_symbols.cpp file to verify the TRAVERSE_SYMBOL_DECL macro
fd -t f 'document_symbols\.cpp' -x cat -n {}

Repository: clice-io/clice

Length of output: 9555


🏁 Script executed:

# Check what's in symbol_detail function around lines 87-90
fd -t f 'document_symbols\.cpp' -x sed -n '80,100p' {}

Repository: clice-io/clice

Length of output: 723


🏁 Script executed:

# Look at the test file for any typedef/type alias test cases
cat -n tests/unit/feature/document_symbol_tests.cpp | head -200

Repository: clice-io/clice

Length of output: 4423


🏁 Script executed:

# Search for TypedefDecl, TypeAliasDecl, TypeAliasTemplateDecl in the entire codebase
rg -n 'TypedefDecl|TypeAliasDecl|TypeAliasTemplateDecl' --type=cpp

Repository: clice-io/clice

Length of output: 1442


🏁 Script executed:

# Check what other Traverse methods exist in DocumentSymbolCollector or similar visitor classes
rg -A2 'Traverse.*Decl\(' src/feature/document_symbols.cpp

# Search for all type alias and typedef related declaration types in Clang
rg -n 'TypeAlias|TypedefDecl|TemplateDecl' --type=cpp src/ | grep -E '(Typedef|TypeAlias|Template.*Decl)' | head -20

Repository: clice-io/clice

Length of output: 2549


🏁 Script executed:

# Check if there are template specialization or other template-related types we might be missing
rg -n 'ClassTemplateDecl|FunctionTemplateDecl|VarTemplateDecl' --type=cpp | head -20

Repository: clice-io/clice

Length of output: 2166


🏁 Script executed:

# Verify that Typedef, TypeAlias, TypeAliasTemplate exist as Clang declaration types
# and check if there are any usages in the current codebase that would be missed
rg -n 'TypedefDecl|TypeAliasDecl|TypeAliasTemplateDecl' --type=cpp src/feature/document_symbols.cpp

Repository: clice-io/clice

Length of output: 40


🏁 Script executed:

# Check if there's a FilteredASTVisitor base class that might have these Traverse methods
fd -t f 'filtered_ast_visitor' -x cat -n {} | head -50

Repository: clice-io/clice

Length of output: 1942


🏁 Script executed:

# Check if RecursiveASTVisitor has Traverse methods for Typedef, TypeAlias, TypeAliasTemplate
# by searching Clang documentation or code patterns
rg -n 'TraverseTypedef|TraverseTypeAlias|Typedef.*Decl' --type=cpp src/ | head -10

Repository: clice-io/clice

Length of output: 934


🏁 Script executed:

# Look at git history to see if these were removed recently
git log --oneline --all -S 'TRAVERSE_SYMBOL_DECL(Typedef' -- src/feature/document_symbols.cpp 2>/dev/null | head -5

Repository: clice-io/clice

Length of output: 40


🏁 Script executed:

# Check if there's any pattern that shows how template declarations should be handled
rg -B5 -A5 'ClassTemplateDecl|FunctionTemplateDecl' src/feature/document_symbols.cpp

Repository: clice-io/clice

Length of output: 40


🏁 Script executed:

# Check symbol_kind.cpp to see how it recognizes TypedefNameDecl
sed -n '1,30p' src/semantic/symbol_kind.cpp

Repository: clice-io/clice

Length of output: 1457


🏁 Script executed:

# Check if code_completion handles TypedefDecl in TRAVERSE or similar
rg -B5 -A5 'TypedefNameDecl' src/feature/code_completion.cpp | head -30

Repository: clice-io/clice

Length of output: 515


Typedefs and type aliases are excluded from document symbols, leaving dead code in symbol_detail.

The TRAVERSE_SYMBOL_DECL macro (lines 154–168) explicitly lists only these declaration types, but excludes Typedef, TypeAlias, and TypeAliasTemplate. Meanwhile, symbol_detail (lines 87–88) contains a branch that prints "type alias" for TypedefNameDecl—code that will never execute because these nodes don't reach push_symbol.

Since symbol_kind.cpp already recognizes TypedefNameDecl as SymbolKind::Type, and other features like code_completion.cpp handle both TypedefNameDecl and TypeAliasTemplateDecl, users will incorrectly stop seeing typedef/using declarations in the document-symbols outline (and potentially template type aliases if they are also missing).

Either remove the stale TypedefNameDecl handling from symbol_detail, or extend the macro to include these types:

Suggested fix
     TRAVERSE_SYMBOL_DECL(Binding);
     TRAVERSE_SYMBOL_DECL(Concept);
+    TRAVERSE_SYMBOL_DECL(Typedef);
+    TRAVERSE_SYMBOL_DECL(TypeAlias);
+    TRAVERSE_SYMBOL_DECL(TypeAliasTemplate);

Also add test coverage for typedef and type alias declarations to prevent regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/feature/document_symbols.cpp` around lines 148 - 170, The
TRAVERSE_SYMBOL_DECL macro currently omits typedef/type alias nodes so
TypedefNameDecl handling in symbol_detail is dead: update the macro invocations
to also include Typedef, TypeAlias, and TypeAliasTemplate (i.e., add
TRAVERSE_SYMBOL_DECL(Typedef); TRAVERSE_SYMBOL_DECL(TypeAlias);
TRAVERSE_SYMBOL_DECL(TypeAliasTemplate);) so push_symbol is called for those
decls and symbol_detail's TypedefNameDecl branch is reachable; alternatively, if
you prefer removing dead code, delete the TypedefNameDecl/type alias branch from
symbol_detail. After making the change, add unit/integration tests that assert
typedef and using/type-alias-template declarations appear in the
document-symbols output.

@Perdixky Perdixky changed the title refactor(filtered ast visitor): remove redundant ast filters refactor(filtered ast visitor): refactor DocumentSymbolCollector to fit the CRTP design of RecursiveASTVisitor Apr 19, 2026
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.

2 participants