Files
clice/src/feature/folding_ranges.cpp
ykiko 6d3b6acc82 feat: initial CompileGraph integration into MasterServer (#376)
## Summary

Initial integration of `CompileGraph` (#375) into `MasterServer`,
enabling basic end-to-end C++20 module support: on-demand PCM building,
dependency-ordered compilation, cascade invalidation on save, and
diagnostic integration.

This is a **first-pass implementation** — the core pipeline works, but
there are known areas for follow-up:

- PCM files go to system temp dir instead of `.clice/cache/`; no disk
cleanup on invalidation
- `run_build_drain` scans imports itself rather than delegating fully to
CompileGraph
- No incremental/partial rebuild (full PCM rebuild on any change)
- Cycle detection is tested at unit level but integration-level coverage
is minimal

## Changes

### Module dependency compilation (`master_server.cpp`)

Before sending a file to the stateful worker, `run_build_drain` now:

1. Scans imports via `scan_precise()` to discover module dependencies
2. Compiles each dep through `compile_graph->compile()`, which
recursively builds transitive PCMs
3. Handles implementation units — `module M;` implicitly needs the
interface PCM
4. Passes all built PCMs to the stateful worker, excluding the file's
own PCM
5. Skips compile on dep failure and resets `build_running` /
`drain_scheduled`
6. Re-lookups iterators after `co_await` to avoid use-after-invalidation

### Cascade invalidation (`didSave` / `didClose`)

- `didSave`: calls `compile_graph->update()` to mark transitive
dependents dirty, removes stale PCM paths, schedules rebuilds for open
dirtied files
- `didClose`: cancels in-flight compilations for the closed file

### Other fixes in this PR

- Debounce timers switched to `shared_ptr` to prevent use-after-free
when `didClose` destroys the timer mid-wait
- `fill_compile_args` returns `bool`; callers handle empty CDB
gracefully
- Adapt all `PositionMapper` call sites to the new `optional` return API
from eventide

## Test plan

- [x] 25 C++ unit tests for CompileGraph (cycles, partial failure,
cancel, update, empty graph)
- [x] 24 C++ integration tests with real clang PCM compilation
- [x] 3 worker-level module tests (BuildPCM, PCM-dependent compile,
multi-module)
- [x] 26 Python LSP integration tests (single module through circular
deps, hover, error diagnostics)
- [x] 371 unit tests + 54 integration tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 20:05:58 +08:00

385 lines
12 KiB
C++

#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "feature/feature.h"
#include "semantic/filtered_ast_visitor.h"
#include "clang/AST/ExprCXX.h"
namespace clice::feature {
namespace {
enum class FoldingKind : std::uint8_t {
Namespace,
Class,
Enum,
Struct,
Union,
LambdaCapture,
FunctionParams,
FunctionBody,
FunctionCall,
CompoundStmt,
AccessSpecifier,
ConditionDirective,
Initializer,
Region,
};
auto to_kind(FoldingKind kind) -> protocol::FoldingRangeKind {
switch(kind) {
case FoldingKind::Namespace: return "namespace";
case FoldingKind::Class: return "class";
case FoldingKind::Enum: return "enum";
case FoldingKind::Struct: return "struct";
case FoldingKind::Union: return "union";
case FoldingKind::LambdaCapture: return "lambdaCapture";
case FoldingKind::FunctionParams: return "functionParams";
case FoldingKind::FunctionBody: return "functionBody";
case FoldingKind::FunctionCall: return "functionCall";
case FoldingKind::CompoundStmt: return "compoundStmt";
case FoldingKind::AccessSpecifier: return "accessSpecifier";
case FoldingKind::ConditionDirective: return "conditionDirective";
case FoldingKind::Initializer: return "initializer";
case FoldingKind::Region:
return protocol::FoldingRangeKind(protocol::FoldingRangeKind::region);
}
return protocol::FoldingRangeKind(protocol::FoldingRangeKind::region);
}
struct RawFoldingRange {
LocalSourceRange range;
std::optional<protocol::FoldingRangeKind> kind;
std::string collapsed_text;
};
class FoldingRangeCollector : public FilteredASTVisitor<FoldingRangeCollector> {
public:
explicit FoldingRangeCollector(CompilationUnitRef unit) : FilteredASTVisitor(unit, true) {}
bool VisitNamespaceDecl(const clang::NamespaceDecl* decl) {
auto tokens = unit.expanded_tokens(decl->getSourceRange())
.drop_until([](const clang::syntax::Token& token) {
return token.kind() == clang::tok::l_brace;
});
if(tokens.empty()) {
return true;
}
add_range(clang::SourceRange(tokens.front().location(), decl->getRBraceLoc()),
to_kind(FoldingKind::Namespace),
"{...}");
return true;
}
bool VisitTagDecl(const clang::TagDecl* decl) {
if(!decl->isThisDeclarationADefinition()) {
return true;
}
auto kind = decl->isStruct() ? FoldingKind::Struct
: decl->isClass() ? FoldingKind::Class
: decl->isUnion() ? FoldingKind::Union
: FoldingKind::Enum;
add_range(decl->getBraceRange(), to_kind(kind), "{...}");
auto* record = llvm::dyn_cast<clang::CXXRecordDecl>(decl);
if(!record || record->isLambda() || record->isImplicit()) {
return true;
}
clang::AccessSpecDecl* previous = nullptr;
for(auto* member: record->decls()) {
auto* access = llvm::dyn_cast<clang::AccessSpecDecl>(member);
if(!access) {
continue;
}
if(previous) {
add_range(
clang::SourceRange(previous->getColonLoc(), access->getAccessSpecifierLoc()),
to_kind(FoldingKind::AccessSpecifier),
"");
}
previous = access;
}
if(previous) {
add_range(clang::SourceRange(previous->getColonLoc(), record->getBraceRange().getEnd()),
to_kind(FoldingKind::AccessSpecifier),
"");
}
return true;
}
bool VisitFunctionDecl(const clang::FunctionDecl* decl) {
if(!decl->doesThisDeclarationHaveABody()) {
collect_parameter_list(decl->getSourceRange());
return true;
}
collect_parameter_list(decl->getBeginLoc(), decl->getBody()->getBeginLoc());
add_range(decl->getBody()->getSourceRange(), to_kind(FoldingKind::FunctionBody), "{...}");
return true;
}
bool VisitLambdaExpr(const clang::LambdaExpr* lambda) {
add_range(lambda->getIntroducerRange(), to_kind(FoldingKind::LambdaCapture), "[...]");
if(lambda->hasExplicitParameters()) {
collect_parameter_list(lambda->getIntroducerRange().getEnd(),
lambda->getCompoundStmtBody()->getBeginLoc());
}
collect_compound_stmt(lambda->getBody());
return true;
}
bool VisitCallExpr(const clang::CallExpr* call) {
auto tokens = unit.expanded_tokens(call->getSourceRange());
if(tokens.empty() || tokens.back().kind() != clang::tok::r_paren) {
return true;
}
auto right_paren = tokens.back().location();
std::size_t depth = 0;
while(!tokens.empty()) {
auto kind = tokens.back().kind();
if(kind == clang::tok::r_paren) {
depth += 1;
} else if(kind == clang::tok::l_paren && --depth == 0) {
add_range(clang::SourceRange(tokens.back().location(), right_paren),
to_kind(FoldingKind::FunctionCall),
"(...)");
break;
}
tokens = tokens.drop_back();
}
return true;
}
bool VisitCXXConstructExpr(const clang::CXXConstructExpr* expr) {
if(auto paren_or_brace = expr->getParenOrBraceRange(); paren_or_brace.isValid()) {
add_range(clang::SourceRange(paren_or_brace.getBegin().getLocWithOffset(1),
paren_or_brace.getEnd()),
to_kind(FoldingKind::FunctionCall),
"(...)");
}
return true;
}
bool VisitInitListExpr(const clang::InitListExpr* expr) {
add_range(clang::SourceRange(expr->getLBraceLoc(), expr->getRBraceLoc()),
to_kind(FoldingKind::Initializer),
"{...}");
return true;
}
auto collect() -> std::vector<RawFoldingRange> {
TraverseDecl(unit.tu());
auto directives_it = unit.directives().find(unit.interested_file());
if(directives_it != unit.directives().end()) {
collect_directives(directives_it->second);
}
std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
return lhs.range.end < rhs.range.end;
});
return std::move(ranges);
}
private:
void add_range(clang::SourceRange range,
std::optional<protocol::FoldingRangeKind> kind,
std::string collapsed_text) {
if(range.isInvalid()) {
return;
}
auto [begin, end] = range;
begin = unit.expansion_location(begin);
end = unit.expansion_location(end);
if(begin == end) {
return;
}
auto [fid, local] = unit.decompose_range(clang::SourceRange(begin, end));
if(fid != unit.interested_file() || !local.valid() || local.end <= local.begin) {
return;
}
auto content = unit.file_content(fid);
bool single_line = true;
for(std::uint32_t offset = local.begin; offset < local.end; ++offset) {
if(content[offset] == '\n') {
single_line = false;
break;
}
}
if(single_line) {
return;
}
ranges.push_back({
.range = local,
.kind = std::move(kind),
.collapsed_text = std::move(collapsed_text),
});
}
void collect_parameter_list(clang::SourceLocation left, clang::SourceLocation right) {
collect_parameter_list(clang::SourceRange(left, right));
}
void collect_parameter_list(clang::SourceRange bounds) {
auto tokens = unit.expanded_tokens(bounds);
auto left_paren = tokens.drop_until(
[](const clang::syntax::Token& token) { return token.kind() == clang::tok::l_paren; });
if(left_paren.empty()) {
return;
}
auto right_paren = std::find_if(
left_paren.rbegin(),
left_paren.rend(),
[](const clang::syntax::Token& token) { return token.kind() == clang::tok::r_paren; });
if(right_paren == left_paren.rend()) {
return;
}
add_range(clang::SourceRange(left_paren.front().location(), right_paren->location()),
to_kind(FoldingKind::FunctionParams),
"(...)");
}
void collect_compound_stmt(const clang::Stmt* stmt) {
auto* compound = llvm::dyn_cast_or_null<clang::CompoundStmt>(stmt);
if(!compound) {
return;
}
add_range(clang::SourceRange(compound->getLBracLoc(), compound->getRBracLoc()),
to_kind(FoldingKind::CompoundStmt),
"{...}");
for(const auto* child: stmt->children()) {
collect_compound_stmt(child);
}
}
void collect_directives(const Directive& directives) {
collect_condition_directives(directives.conditions);
collect_pragma_region(directives.pragmas);
}
void collect_condition_directives(const std::vector<Condition>& conditions) {
llvm::SmallVector<const Condition*> stack;
for(const auto& condition: conditions) {
switch(condition.kind) {
case Condition::BranchKind::If:
case Condition::BranchKind::Ifdef:
case Condition::BranchKind::Ifndef:
case Condition::BranchKind::Elif:
case Condition::BranchKind::Elifndef: stack.push_back(&condition); break;
case Condition::BranchKind::Else: {
if(!stack.empty()) {
auto* previous = stack.pop_back_val();
add_range(
clang::SourceRange(previous->condition_range.getEnd(), condition.loc),
to_kind(FoldingKind::ConditionDirective),
"");
}
stack.push_back(&condition);
break;
}
case Condition::BranchKind::EndIf:
if(!stack.empty()) {
(void)stack.pop_back_val();
}
break;
default: break;
}
}
}
void collect_pragma_region(const std::vector<Pragma>& pragmas) {
llvm::SmallVector<const Pragma*> stack;
for(const auto& pragma: pragmas) {
if(pragma.kind == Pragma::Kind::Region) {
stack.push_back(&pragma);
continue;
}
if(pragma.kind != Pragma::Kind::EndRegion || stack.empty()) {
continue;
}
auto* previous = stack.pop_back_val();
add_range(clang::SourceRange(previous->loc, pragma.loc),
to_kind(FoldingKind::Region),
"");
}
}
private:
std::vector<RawFoldingRange> ranges;
};
} // namespace
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::FoldingRange> {
auto collected = FoldingRangeCollector(unit).collect();
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::FoldingRange> result;
result.reserve(collected.size());
for(const auto& item: collected) {
auto start = *converter.to_position(item.range.begin);
auto end = *converter.to_position(item.range.end);
protocol::FoldingRange range{
.start_line = start.line,
.start_character = start.character,
.end_line = end.line,
.end_character = end.character,
};
if(item.kind.has_value()) {
range.kind = *item.kind;
}
if(!item.collapsed_text.empty()) {
range.collapsed_text = item.collapsed_text;
}
result.push_back(std::move(range));
}
return result;
}
} // namespace clice::feature