3 Commits

Author SHA1 Message Date
ykiko
42a3d20971 fix(server): make shutdown sanitizer-clean 2026-06-01 10:09:26 +08:00
ykiko
dfeda4dc6f fix(server): make shutdown sanitizer-clean 2026-06-01 00:08:12 +08:00
ykiko
cc5b25d5c3 refactor: public feature types and snapshot testing infrastructure (#442)
## Summary

- **Public feature types**: Move `SemanticToken`, `FoldingRange`,
`DocumentSymbol`, `InlayHint`, and `HintCategory` from internal `.cpp`
files to `feature.h` as public API types. Each feature now exposes two
overloads: a raw overload returning offset-based types and a protocol
overload that converts to LSP wire-format with explicit
`PositionEncoding`.
- **Snapshot testing**: Add corpus-driven snapshot tests using
`ASSERT_SNAPSHOT_GLOB` for semantic tokens, folding ranges, inlay hints,
document symbols, and TU index. Tests compile real C++ corpus files,
format output as YAML flow mappings, and diff against `.snap.yml`
baselines.
- **Test infrastructure**: Add `compile_file()` to `Tester`,
`yaml_str()` utility, `--corpus-dir` / `--snapshot-dir` CLI options, and
`--verbose` flag for unit tests. Migrate to kotatsu's unified
`kota::zest::Options` API.
- **Toolchain robustness**: Filter unknown cc1 args via
`clang::driver::getDriverOptTable()` to handle system compilers newer
than embedded LLVM.
- **Dependency bump**: Update kotatsu to 7381404 (unified zest Options,
out-param `from_json` API).

## Details

### Feature type changes
All five feature modules (`semantic_tokens`, `folding_ranges`,
`document_symbols`, `inlay_hints`, `document_links`) now follow the same
two-overload pattern. The raw overload returns offset-based structs
suitable for indexing and testing; the protocol overload adds
`PositionEncoding` conversion for LSP responses. `stateful_worker.cpp`
explicitly passes `PositionEncoding::UTF16` at every call site.

### Snapshot tests
Corpus files live in `tests/corpus/` (organized by language construct).
Snapshot baselines live in `tests/snapshots/<feature>/`. Format lambdas
are inlined directly in test bodies — no separate format functions for
single-use formatters. YAML output uses flow mappings (`- { key: value
}`) for compact, diffable baselines.

### cc1 arg filtering
`src/command/toolchain.cpp` now parses the cc1 argument list through
LLVM's driver option table and drops any args classified as
`UnknownClass`. This prevents compilation failures when the system
compiler emits flags that the embedded LLVM version doesn't recognize.

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-24 19:36:27 +08:00
32 changed files with 852 additions and 224 deletions

View File

@@ -41,7 +41,7 @@ set(FLATBUFFERS_BUILD_FLATHASH OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
kotatsu
GIT_REPOSITORY https://github.com/clice-io/kotatsu
GIT_TAG e024f3b427a554502c4aa015952800a03ca4384b
GIT_TAG 73814044ce8142f4438a3028f44668675fc09fff
)
set(KOTA_ENABLE_ZEST ON)

View File

@@ -161,7 +161,7 @@ depends-on = [{ task = "lint-cpp", args = ["{{ type }}"] }]
[feature.test.tasks.unit-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data"'
cmd = './build/{{ type }}/bin/unit_tests --test-dir="./tests/data" --snapshot-dir="./tests/snapshots" --corpus-dir="./tests/corpus" --verbose'
[feature.test.tasks.integration-test]
args = [{ arg = "type", default = "RelWithDebInfo" }]
@@ -257,7 +257,7 @@ format-markdown = "fd -H -e md -x prettier --write"
format-json = "fd -H -e json -E package-lock.json -x prettier --write"
format-toml = "fd -H -e toml -x tombi format"
format-yaml = """
fd -H -e yaml -e yml -E pnpm-lock.yaml -x prettier --write && \
fd -H -e yaml -e yml -E pnpm-lock.yaml -E '*.snap.yml' -x prettier --write && \
fd -H "^\\.clang-(format|tidy)$" -x prettier --write --parser yaml
"""
format = { depends-on = [

View File

@@ -9,6 +9,7 @@
#include "support/logging.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/DenseSet.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
@@ -17,6 +18,7 @@
#include "llvm/TargetParser/Host.h"
#include "clang/Driver/Compilation.h"
#include "clang/Driver/Driver.h"
#include "clang/Driver/Options.h"
#include "clang/Driver/Tool.h"
#ifndef _WIN32
@@ -470,11 +472,32 @@ std::vector<const char*> query_clang_toolchain(const QueryParams& params) {
continue;
}
for(auto arg: args) {
if(arg == "-###"sv) {
// FIXME: the system compiler may be newer than our embedded LLVM,
// producing cc1 flags we don't recognize. Filter them out here.
// Long-term we should unify the command pipeline so the driver
// version always matches the embedded LLVM.
auto& table = clang::driver::getDriverOptTable();
auto cc1_args = llvm::ArrayRef(args).drop_front(2);
unsigned missing_index = 0, missing_count = 0;
auto parsed = table.ParseArgs(cc1_args, missing_index, missing_count);
llvm::DenseSet<unsigned> unknown_indices;
for(auto* a: parsed) {
if(a->getOption().getKind() == llvm::opt::Option::UnknownClass) {
unknown_indices.insert(a->getIndex());
}
}
result.emplace_back(params.callback(args[0]));
result.emplace_back(params.callback(args[1]));
for(unsigned i = 0; i < cc1_args.size(); ++i) {
if(unknown_indices.contains(i)) {
continue;
}
result.emplace_back(params.callback(arg));
if(cc1_args[i] == "-###"sv) {
continue;
}
result.emplace_back(params.callback(cc1_args[i]));
}
}
}

View File

@@ -93,18 +93,9 @@ auto symbol_detail(clang::ASTContext& context, const clang::NamedDecl& decl) ->
return detail;
}
struct InternalSymbol {
std::string name;
std::string detail;
SymbolKind kind = SymbolKind::Invalid;
LocalSourceRange range;
LocalSourceRange selection_range;
std::vector<InternalSymbol> children;
};
struct SymbolFrame {
std::vector<InternalSymbol> symbols;
std::vector<InternalSymbol>* cursor = &symbols;
std::vector<DocumentSymbol> symbols;
std::vector<DocumentSymbol>* cursor = &symbols;
};
class DocumentSymbolCollector : public FilteredASTVisitor<DocumentSymbolCollector> {
@@ -143,7 +134,7 @@ public:
return ok;
}
auto collect() -> std::vector<InternalSymbol> {
auto collect() -> std::vector<DocumentSymbol> {
TraverseDecl(unit.tu());
return std::move(result.symbols);
}
@@ -174,8 +165,8 @@ private:
SymbolFrame result;
};
void sort_symbols(std::vector<InternalSymbol>& symbols) {
std::ranges::sort(symbols, [](const InternalSymbol& lhs, const InternalSymbol& rhs) {
void sort_symbols(std::vector<DocumentSymbol>& symbols) {
std::ranges::sort(symbols, [](const DocumentSymbol& lhs, const DocumentSymbol& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
@@ -187,7 +178,7 @@ void sort_symbols(std::vector<InternalSymbol>& symbols) {
}
}
auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& converter)
auto to_protocol_symbol(const DocumentSymbol& symbol, const PositionMapper& converter)
-> protocol::DocumentSymbol {
protocol::DocumentSymbol result{
.name = symbol.name,
@@ -215,10 +206,15 @@ auto to_protocol_symbol(const InternalSymbol& symbol, const PositionMapper& conv
} // namespace
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol> {
auto result = DocumentSymbolCollector(unit).collect();
sort_symbols(result);
return result;
}
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentSymbol> {
auto internal = DocumentSymbolCollector(unit).collect();
sort_symbols(internal);
auto internal = document_symbols(unit);
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::DocumentSymbol> symbols;

View File

@@ -7,6 +7,7 @@
#include "compile/compilation.h"
#include "compile/compilation_unit.h"
#include "semantic/symbol_kind.h"
#include "kota/ipc/lsp/position.h"
#include "kota/ipc/lsp/protocol.h"
@@ -59,18 +60,66 @@ struct InlayHintsOptions {
struct SignatureHelpOptions {};
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
struct SemanticToken {
LocalSourceRange range;
SymbolKind kind = SymbolKind::Invalid;
std::uint32_t modifiers = 0;
};
struct FoldingRange {
LocalSourceRange range;
std::optional<protocol::FoldingRangeKind> kind;
std::string collapsed_text;
};
struct DocumentSymbol {
std::string name;
std::string detail;
SymbolKind kind = SymbolKind::Invalid;
LocalSourceRange range;
LocalSourceRange selection_range;
std::vector<DocumentSymbol> children;
};
enum class HintCategory : std::uint8_t {
Parameter,
DefaultArgument,
Type,
Designator,
BlockEnd,
};
struct InlayHint {
std::uint32_t offset = 0;
HintCategory kind = HintCategory::Type;
std::string label;
bool padding_left = false;
bool padding_right = false;
};
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken>;
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
-> protocol::SemanticTokens;
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange>;
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::FoldingRange>;
auto document_symbols(CompilationUnitRef unit) -> std::vector<DocumentSymbol>;
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::DocumentSymbol>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options = {}) -> std::vector<InlayHint>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint>;
auto document_links(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::DocumentLink>;
auto document_symbols(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::DocumentSymbol>;
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::FoldingRange>;
auto diagnostics(CompilationUnitRef unit, PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::Diagnostic>;
@@ -89,12 +138,6 @@ auto hover(CompilationUnitRef unit,
const HoverOptions& options = {},
PositionEncoding encoding = PositionEncoding::UTF16) -> std::optional<protocol::Hover>;
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options = {},
PositionEncoding encoding = PositionEncoding::UTF16)
-> std::vector<protocol::InlayHint>;
auto signature_help(CompilationParams& params, const SignatureHelpOptions& options = {})
-> protocol::SignatureHelp;

View File

@@ -53,12 +53,6 @@ auto to_kind(FoldingKind kind) -> protocol::FoldingRangeKind {
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) {}
@@ -185,7 +179,7 @@ public:
return true;
}
auto collect() -> std::vector<RawFoldingRange> {
auto collect() -> std::vector<FoldingRange> {
TraverseDecl(unit.tu());
auto directives_it = unit.directives().find(unit.interested_file());
@@ -193,7 +187,7 @@ public:
collect_directives(directives_it->second);
}
std::ranges::sort(ranges, [](const RawFoldingRange& lhs, const RawFoldingRange& rhs) {
std::ranges::sort(ranges, [](const FoldingRange& lhs, const FoldingRange& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
@@ -343,14 +337,18 @@ private:
}
private:
std::vector<RawFoldingRange> ranges;
std::vector<FoldingRange> ranges;
};
} // namespace
auto folding_ranges(CompilationUnitRef unit) -> std::vector<FoldingRange> {
return FoldingRangeCollector(unit).collect();
}
auto folding_ranges(CompilationUnitRef unit, PositionEncoding encoding)
-> std::vector<protocol::FoldingRange> {
auto collected = FoldingRangeCollector(unit).collect();
auto collected = folding_ranges(unit);
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::FoldingRange> result;

View File

@@ -26,22 +26,6 @@ using llvm::dyn_cast_or_null;
// For now, inlay hints are always anchored at the left or right of their range.
enum class HintSide { Left, Right };
enum class HintCategory : std::uint8_t {
Parameter,
DefaultArgument,
Type,
Designator,
BlockEnd,
};
struct RawInlayHint {
std::uint32_t offset = 0;
HintCategory kind = HintCategory::Type;
std::string label;
bool padding_left = false;
bool padding_right = false;
};
bool is_expanded_from_param_pack(const clang::ParmVarDecl* param) {
return ast::underlying_pack_type(param) != nullptr;
}
@@ -123,7 +107,7 @@ struct Callee {
class Builder {
public:
Builder(std::vector<RawInlayHint>& result,
Builder(std::vector<InlayHint>& result,
CompilationUnitRef unit,
LocalSourceRange restrict_range,
const InlayHintsOptions& options) :
@@ -499,7 +483,7 @@ public:
bool pad_left = prefix.consume_front(" ");
bool pad_right = suffix.consume_back(" ");
RawInlayHint hint{
InlayHint hint{
.offset = offset,
.kind = kind,
.label = (prefix + label + suffix).str(),
@@ -554,7 +538,7 @@ public:
}
private:
std::vector<RawInlayHint>& result;
std::vector<InlayHint>& result;
CompilationUnitRef unit;
LocalSourceRange restrict_range;
const InlayHintsOptions& options;
@@ -913,36 +897,43 @@ private:
} // namespace
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
auto inlay_hints(CompilationUnitRef unit, LocalSourceRange target, const InlayHintsOptions& options)
-> std::vector<InlayHint> {
if(!options.enabled) {
return {};
}
std::vector<RawInlayHint> raw_hints;
std::vector<InlayHint> raw_hints;
Builder builder(raw_hints, unit, target, options);
Visitor visitor(builder, unit, target, options);
visitor.TraverseDecl(unit.tu());
std::ranges::sort(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
std::ranges::sort(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
return std::tie(lhs.offset, lhs.label, lhs.kind, lhs.padding_left, lhs.padding_right) <
std::tie(rhs.offset, rhs.label, rhs.kind, rhs.padding_left, rhs.padding_right);
});
auto unique_begin =
std::ranges::unique(raw_hints, [](const RawInlayHint& lhs, const RawInlayHint& rhs) {
std::ranges::unique(raw_hints, [](const InlayHint& lhs, const InlayHint& rhs) {
return lhs.offset == rhs.offset && lhs.kind == rhs.kind && lhs.label == rhs.label &&
lhs.padding_left == rhs.padding_left && lhs.padding_right == rhs.padding_right;
});
raw_hints.erase(unique_begin.begin(), unique_begin.end());
return raw_hints;
}
auto inlay_hints(CompilationUnitRef unit,
LocalSourceRange target,
const InlayHintsOptions& options,
PositionEncoding encoding) -> std::vector<protocol::InlayHint> {
auto collected = inlay_hints(unit, target, options);
PositionMapper converter(unit.interested_content(), encoding);
std::vector<protocol::InlayHint> hints;
hints.reserve(raw_hints.size());
hints.reserve(collected.size());
for(const auto& hint: raw_hints) {
for(const auto& hint: collected) {
protocol::InlayHint out{
.position = *converter.to_position(hint.offset),
.label = hint.label,

View File

@@ -18,12 +18,6 @@ namespace clice::feature {
namespace {
struct RawToken {
LocalSourceRange range;
SymbolKind kind = SymbolKind::Invalid;
std::uint32_t modifiers = 0;
};
void add_modifier(std::uint32_t& modifiers, SymbolModifiers::Kind kind) {
modifiers |= SymbolModifiers::to_mask(kind);
}
@@ -166,7 +160,7 @@ class SemanticTokensCollector : public SemanticVisitor<SemanticTokensCollector>
public:
explicit SemanticTokensCollector(CompilationUnitRef unit) : SemanticVisitor(unit, true) {}
auto collect() -> std::vector<RawToken> {
auto collect() -> std::vector<SemanticToken> {
highlight_lexical(unit.interested_file());
run();
highlight_modules();
@@ -398,7 +392,7 @@ private:
}
}
static void resolve_conflict(RawToken& last, const RawToken& current) {
static void resolve_conflict(SemanticToken& last, const SemanticToken& current) {
if(last.kind == SymbolKind::Conflict) {
return;
}
@@ -414,14 +408,14 @@ private:
}
void merge_tokens() {
std::ranges::sort(tokens, [](const RawToken& lhs, const RawToken& rhs) {
std::ranges::sort(tokens, [](const SemanticToken& lhs, const SemanticToken& rhs) {
if(lhs.range.begin != rhs.range.begin) {
return lhs.range.begin < rhs.range.begin;
}
return lhs.range.end < rhs.range.end;
});
std::vector<RawToken> merged;
std::vector<SemanticToken> merged;
merged.reserve(tokens.size());
for(const auto& token: tokens) {
@@ -448,7 +442,7 @@ private:
}
public:
std::vector<RawToken> tokens;
std::vector<SemanticToken> tokens;
};
class SemanticTokenEncoder {
@@ -458,7 +452,7 @@ public:
protocol::SemanticTokens& output) :
content(content), converter(content, encoding), output(output) {}
void append(const RawToken& token) {
void append(const SemanticToken& token) {
if(!token.range.valid() || token.range.end <= token.range.begin ||
token.range.end > content.size()) {
return;
@@ -542,10 +536,14 @@ private:
} // namespace
auto semantic_tokens(CompilationUnitRef unit) -> std::vector<SemanticToken> {
SemanticTokensCollector collector(unit);
return collector.collect();
}
auto semantic_tokens(CompilationUnitRef unit, PositionEncoding encoding)
-> protocol::SemanticTokens {
SemanticTokensCollector collector(unit);
auto tokens = collector.collect();
auto tokens = semantic_tokens(unit);
protocol::SemanticTokens result;
result.data.reserve(tokens.size() * 5);

View File

@@ -778,9 +778,10 @@ AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) :
co_return result;
});
peer.on_notification([&srv](const ShutdownParams&) {
peer.on_notification([this, &srv](const ShutdownParams&) {
LOG_INFO("agentic/shutdown received, shutting down");
srv.schedule_shutdown();
this->peer.close();
});
}

View File

@@ -156,6 +156,7 @@ LSPClient::LSPClient(MasterServer& server, kota::ipc::JsonPeer& peer) : server(s
peer.on_notification([this]([[maybe_unused]] const protocol::ExitParams& params) {
LOG_INFO("Exit notification received");
this->server.schedule_shutdown();
this->peer.close();
});
peer.on_notification([this](const protocol::DidOpenTextDocumentParams& params) {

View File

@@ -110,14 +110,14 @@ void MasterServer::start_file_watcher() {
if(workspace_root.empty())
return;
loop.schedule([this]() -> kota::task<> {
auto watcher = kota::fs_event::create(workspace_root, {}, loop);
loop.schedule([](MasterServer& server) -> kota::task<> {
auto watcher = kota::fs_event::create(server.workspace_root, {}, server.loop);
if(!watcher) {
LOG_WARN("Failed to start file watcher for {}", workspace_root);
LOG_WARN("Failed to start file watcher for {}", server.workspace_root);
co_return;
}
LOG_INFO("File watcher started for {}", workspace_root);
LOG_INFO("File watcher started for {}", server.workspace_root);
while(true) {
auto changes = co_await watcher->next();
@@ -132,19 +132,19 @@ void MasterServer::start_file_watcher() {
llvm::StringRef file(change.path);
if(file.ends_with("compile_commands.json")) {
LOG_INFO("CDB changed, reloading workspace");
load_workspace();
server.load_workspace();
continue;
}
if(file.ends_with(".cpp") || file.ends_with(".cc") || file.ends_with(".cxx") ||
file.ends_with(".c") || file.ends_with(".h") || file.ends_with(".hpp") ||
file.ends_with(".hxx") || file.ends_with(".cppm") || file.ends_with(".ixx")) {
auto path_id = workspace.path_pool.intern(file);
on_file_saved(path_id);
auto path_id = server.workspace.path_pool.intern(file);
server.on_file_saved(path_id);
}
}
}
}());
}(*this));
}
Session* MasterServer::find_session(std::uint32_t path_id) {
@@ -212,10 +212,10 @@ void MasterServer::schedule_shutdown() {
workspace.save_cache();
shutdown_event.set();
loop.schedule([this]() -> kota::task<> {
co_await kota::when_all(indexer.stop(), compiler.stop(), pool.stop());
loop.stop();
}());
loop.schedule([](MasterServer& server) -> kota::task<> {
co_await kota::when_all(server.indexer.stop(), server.compiler.stop(), server.pool.stop());
server.loop.stop();
}(*this));
}
void MasterServer::load_workspace() {
@@ -355,35 +355,53 @@ static kota::task<> accept_connections(MasterServer& server,
std::list<Connection>& connections) {
auto& loop = kota::event_loop::current();
kota::task_group<> connection_group(loop);
bool lsp_registered = false;
while(true) {
auto conn = co_await acceptor.accept();
if(!conn.has_value())
break;
co_await kota::when_all(
[](MasterServer& server,
kota::tcp::acceptor& acceptor,
bool register_lsp,
std::list<Connection>& connections,
kota::task_group<>& connection_group) -> kota::task<> {
auto& loop = kota::event_loop::current();
bool lsp_registered = false;
LOG_INFO("Client connected");
while(true) {
auto conn = co_await acceptor.accept();
if(!conn.has_value())
break;
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
LOG_INFO("Client connected");
std::unique_ptr<LSPClient> lsp;
if(register_lsp && !lsp_registered) {
lsp = std::make_unique<LSPClient>(server, *peer);
lsp_registered = true;
}
auto agent = std::make_unique<AgentClient>(server, *peer);
auto transport = std::make_unique<kota::ipc::StreamTransport>(std::move(*conn));
auto peer = std::make_unique<kota::ipc::JsonPeer>(loop, std::move(transport));
auto* peer_ptr = peer.get();
auto it = connections.emplace(connections.end(),
Connection{
.peer = std::move(peer),
.lsp_client = std::move(lsp),
.agent_client = std::move(agent),
});
std::unique_ptr<LSPClient> lsp;
if(register_lsp && !lsp_registered) {
lsp = std::make_unique<LSPClient>(server, *peer);
lsp_registered = true;
}
auto agent = std::make_unique<AgentClient>(server, *peer);
connection_group.spawn(run_connection(peer_ptr, connections, it));
}
auto* peer_ptr = peer.get();
auto it = connections.emplace(connections.end(),
Connection{
.peer = std::move(peer),
.lsp_client = std::move(lsp),
.agent_client = std::move(agent),
});
connection_group.spawn(run_connection(peer_ptr, connections, it));
}
}(server, acceptor, register_lsp, connections, connection_group),
[](MasterServer& server,
kota::tcp::acceptor& acceptor,
std::list<Connection>& connections) -> kota::task<> {
co_await server.get_shutdown_event().wait();
acceptor.stop();
for(auto& conn: connections) {
conn.peer->close();
}
}(server, acceptor, connections));
co_await connection_group.join();
}
@@ -411,18 +429,50 @@ int run_server_mode(const ServerOptions& opts) {
kota::ipc::JsonPeer lsp_peer(loop, std::move(final_transport));
LSPClient lsp_client(server, lsp_peer);
loop.schedule([](MasterServer& server, kota::ipc::JsonPeer& peer) -> kota::task<> {
co_await server.get_shutdown_event().wait();
peer.close();
}(server, lsp_peer));
kota::tcp::acceptor agent_acceptor;
bool has_agent_acceptor = false;
if(opts.port > 0) {
auto acceptor = kota::tcp::listen(opts.host, opts.port, {}, loop);
if(acceptor) {
LOG_INFO("Agentic protocol listening on {}:{}", opts.host, opts.port);
loop.schedule(accept_connections(server, std::move(*acceptor), false, connections));
agent_acceptor = std::move(*acceptor);
has_agent_acceptor = true;
} else {
LOG_WARN("Failed to start agentic listener on {}:{}", opts.host, opts.port);
}
}
loop.schedule(lsp_peer.run());
loop.schedule([](MasterServer& server,
kota::ipc::JsonPeer& peer,
std::list<Connection>& connections,
kota::tcp::acceptor acceptor,
bool has_acceptor) -> kota::task<> {
auto run_peer = [](MasterServer& server, kota::ipc::JsonPeer& peer) -> kota::task<> {
co_await peer.run();
server.schedule_shutdown();
};
auto close_peer_on_shutdown = [](MasterServer& server,
kota::ipc::JsonPeer& peer) -> kota::task<> {
co_await server.get_shutdown_event().wait();
peer.close();
};
if(has_acceptor) {
co_await kota::when_all(
run_peer(server, peer),
close_peer_on_shutdown(server, peer),
accept_connections(server, std::move(acceptor), false, connections));
} else {
co_await kota::when_all(run_peer(server, peer),
close_peer_on_shutdown(server, peer));
}
}(server, lsp_peer, connections, std::move(agent_acceptor), has_agent_acceptor));
loop.run();
return 0;
}
@@ -463,7 +513,12 @@ static kota::task<> daemon_main(MasterServer& server, kota::pipe::acceptor accep
kota::task_group<> connection_group(loop);
co_await kota::when_all(
[&]() -> kota::task<> {
[](MasterServer& server,
kota::pipe::acceptor& acceptor,
std::list<DaemonConnection>& connections,
kota::task_group<>& connection_group) -> kota::task<> {
auto& loop = kota::event_loop::current();
while(true) {
auto conn = co_await acceptor.accept();
if(!conn.has_value())
@@ -484,14 +539,16 @@ static kota::task<> daemon_main(MasterServer& server, kota::pipe::acceptor accep
connection_group.spawn(run_daemon_connection(peer_ptr, connections, it));
}
}(),
[&]() -> kota::task<> {
}(server, acceptor, connections, connection_group),
[](MasterServer& server,
kota::pipe::acceptor& acceptor,
std::list<DaemonConnection>& connections) -> kota::task<> {
co_await server.get_shutdown_event().wait();
acceptor.stop();
for(auto& conn: connections) {
conn.peer->close();
}
}());
}(server, acceptor, connections));
co_await connection_group.join();
}

View File

@@ -245,26 +245,33 @@ void StatefulWorker::register_handlers() {
co_return kota::codec::RawValue{"[]"};
case K::SemanticTokens:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::semantic_tokens(doc.unit));
return to_raw(
feature::semantic_tokens(doc.unit, feature::PositionEncoding::UTF16));
});
case K::InlayHints:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
auto range = params.range;
if(range.begin == static_cast<uint32_t>(-1))
range = LocalSourceRange{0, static_cast<uint32_t>(doc.text.size())};
return to_raw(feature::inlay_hints(doc.unit, range));
return to_raw(feature::inlay_hints(doc.unit,
range,
{},
feature::PositionEncoding::UTF16));
});
case K::FoldingRange:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::folding_ranges(doc.unit));
return to_raw(
feature::folding_ranges(doc.unit, feature::PositionEncoding::UTF16));
});
case K::DocumentSymbol:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::document_symbols(doc.unit));
return to_raw(
feature::document_symbols(doc.unit, feature::PositionEncoding::UTF16));
});
case K::DocumentLink:
co_return co_await with_ast(params.path, [&](DocumentEntry& doc) {
return to_raw(feature::document_links(doc.unit));
return to_raw(
feature::document_links(doc.unit, feature::PositionEncoding::UTF16));
});
case K::CodeAction:
// TODO: Implement code actions

View File

@@ -116,6 +116,9 @@ bool WorkerPool::start(const WorkerPoolOptions& options) {
options_ = options;
log_dir_ = options.log_dir;
stateless_workers.reserve(options.stateless_count);
stateful_workers.reserve(options.stateful_count);
for(std::uint32_t i = 0; i < options.stateless_count; ++i) {
if(!spawn_worker(options.self_path, false, 0)) {
return false;
@@ -229,10 +232,10 @@ void WorkerPool::clear_owner(std::size_t worker_index) {
kota::task<> WorkerPool::monitor_worker(std::size_t index, bool stateful) {
auto& workers = stateful ? stateful_workers : stateless_workers;
auto& w = workers[index];
auto name = std::string(stateful ? "SF-" : "SL-") + std::to_string(index);
auto result = co_await w.proc.wait();
auto result = co_await workers[index].proc.wait();
auto& w = workers[index];
w.alive = false;
if(shutting_down_)

View File

@@ -156,13 +156,13 @@ std::optional<Config> Config::load(llvm::StringRef path, llvm::StringRef workspa
}
std::optional<Config> Config::load_from_json(llvm::StringRef json, llvm::StringRef workspace_root) {
auto result = kota::codec::json::from_json<Config>(json);
Config config{};
auto result = kota::codec::json::from_json(json, config);
if(!result) {
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message());
LOG_WARN("Failed to parse initializationOptions JSON: {}", result.error().message);
return std::nullopt;
}
auto config = std::move(*result);
config.apply_defaults(workspace_root);
LOG_INFO("Loaded config from initializationOptions");
return config;

View File

@@ -185,44 +185,94 @@ async def make_client(executable: Path, workspace: Path) -> CliceClient:
return c
SANITIZER_MARKERS = (
"AddressSanitizer",
"LeakSanitizer",
"MemorySanitizer",
"ThreadSanitizer",
"UndefinedBehaviorSanitizer",
"==ERROR:",
"runtime error:",
)
def _server_stderr_excerpt(stderr_text: str) -> str:
interesting = [
line
for line in stderr_text.splitlines()
if "[warn]" in line
or "[error]" in line
or "Sanitizer" in line
or "==ERROR:" in line
or "runtime error:" in line
]
return "\n".join(interesting[-80:])
async def assert_server_exited_cleanly(server, timeout: float = 3.0) -> None:
failures: list[str] = []
if server is None:
return
if server.returncode is None:
try:
await asyncio.wait_for(server.wait(), timeout=timeout)
except asyncio.TimeoutError:
server.kill()
await server.wait()
failures.append(f"server did not exit within {timeout:g}s after shutdown")
print(f"[server] exit code: {server.returncode}", flush=True)
stderr_text = ""
if server.stderr:
try:
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
stderr_text = stderr_data.decode("utf-8", errors="replace")
except Exception as exc:
failures.append(f"failed to collect server stderr: {exc!r}")
for line in _server_stderr_excerpt(stderr_text).splitlines():
print(f"[server] {line}", flush=True)
if server.returncode != 0:
failures.append(f"server exited with code {server.returncode}")
if any(marker in stderr_text for marker in SANITIZER_MARKERS):
failures.append("server stderr contains sanitizer/runtime error output")
if failures:
excerpt = _server_stderr_excerpt(stderr_text)
if excerpt:
failures.append("server stderr excerpt:\n" + excerpt)
pytest.fail("\n".join(failures))
async def _shutdown_client(c: CliceClient) -> None:
"""Gracefully shut down a client, force-kill if needed."""
server = getattr(c, "_server", None)
try:
await asyncio.wait_for(c.shutdown_async(None), timeout=3.0)
except Exception:
pass
try:
c.exit(None)
except Exception:
pass
await asyncio.sleep(0.3)
if hasattr(c, "_server") and c._server is not None and c._server.returncode is None:
c._server.kill()
try:
server = getattr(c, "_server", None)
if server:
if server.returncode is not None:
print(f"[server] exit code: {server.returncode}", flush=True)
if server.stderr:
stderr_data = await asyncio.wait_for(server.stderr.read(), timeout=2.0)
if stderr_data:
for line in stderr_data.decode(
"utf-8", errors="replace"
).splitlines():
if "[warn]" in line or "[error]" in line or "Sanitizer" in line:
print(f"[server] {line}", flush=True)
except Exception:
pass
try:
c._stop_event.set()
for task in c._async_tasks:
task.cancel()
await asyncio.sleep(0.1)
except Exception:
pass
await assert_server_exited_cleanly(server)
finally:
try:
c._stop_event.set()
for task in c._async_tasks:
task.cancel()
await asyncio.sleep(0.1)
except Exception:
pass
shutdown_client = _shutdown_client # Public alias for multi-session tests

View File

@@ -0,0 +1,36 @@
// basic if and if-else
namespace basic_if {
int abs_val(int x) {
if(x < 0)
return -x;
return x;
}
const char* sign(int x) {
if(x > 0) {
return "positive";
} else if(x < 0) {
return "negative";
} else {
return "zero";
}
}
// dangling else: else binds to nearest if
int nested_if(int a, int b) {
if(a > 0)
if(b > 0)
return 1;
else
return 2;
return 0;
}
void test() {
[[maybe_unused]] int r1 = abs_val(-3);
[[maybe_unused]] auto r2 = sign(5);
[[maybe_unused]] int r3 = nested_if(1, -1);
}
} // namespace basic_if

View File

@@ -530,7 +530,7 @@ async def test_rpc_impact_analysis_unknown(indexed_agentic, workspace):
async def test_shutdown_during_indexing(executable, tmp_path):
"""Shutdown during active background indexing must exit cleanly."""
from tests.integration.utils.client import CliceClient
from tests.conftest import _find_free_port
from tests.conftest import _find_free_port, assert_server_exited_cleanly
workspace = tmp_path / "ws"
workspace.mkdir()
@@ -560,33 +560,38 @@ async def test_shutdown_during_indexing(executable, tmp_path):
c = CliceClient()
await c.start_io(*cmd)
init_options = {
"project": {
"cache_dir": str(workspace / ".clice"),
"idle_timeout_ms": 0,
}
}
await c.initialize(workspace, initialization_options=init_options)
# Give indexing a moment to start, then send shutdown
await asyncio.sleep(0.5)
rpc = AgenticRpcClient(host, port)
body = json.dumps({"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}})
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
rpc.sock.settimeout(5)
try:
rpc.sock.recv(4096)
except (socket.timeout, OSError):
pass
rpc.sock.close()
init_options = {
"project": {
"cache_dir": str(workspace / ".clice"),
"idle_timeout_ms": 0,
}
}
try:
await c.initialize(workspace, initialization_options=init_options)
except Exception:
if c._server.returncode is not None:
await assert_server_exited_cleanly(c._server, timeout=15.0)
raise
for _ in range(30):
if c._server.returncode is not None:
break
# Give indexing a moment to start, then send shutdown
await asyncio.sleep(0.5)
assert c._server.returncode is not None, "Server did not exit after shutdown"
assert c._server.returncode >= 0, (
f"Server crashed with signal {-c._server.returncode}"
)
rpc = AgenticRpcClient(host, port)
body = json.dumps(
{"jsonrpc": "2.0", "method": "agentic/shutdown", "params": {}}
)
rpc.sock.sendall(f"Content-Length: {len(body)}\r\n\r\n{body}".encode())
rpc.sock.settimeout(5)
try:
rpc.sock.recv(4096)
except (socket.timeout, OSError):
pass
rpc.sock.close()
await assert_server_exited_cleanly(c._server, timeout=15.0)
finally:
c._stop_event.set()
for task in c._async_tasks:
task.cancel()
await asyncio.sleep(0.1)

View File

@@ -0,0 +1,14 @@
---
source: document_symbol_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { name: "basic_if", kind: Namespace, range: "1:0-35:1", selection_range: "1:10-1:18" }
- { name: "abs_val", kind: Function, range: "3:0-7:1", selection_range: "3:4-3:11", detail: "int (int)" }
- { name: "sign", kind: Function, range: "9:0-17:1", selection_range: "9:12-9:16", detail: "const char *(int)" }
- { name: "nested_if", kind: Function, range: "20:0-27:1", selection_range: "20:4-20:13", detail: "int (int, int)" }
- { name: "test", kind: Function, range: "29:0-33:1", selection_range: "29:5-29:9", detail: "void ()" }
- { name: "r1", kind: Variable, range: "30:21-30:41", selection_range: "30:25-30:27", detail: "int" }
- { name: "r2", kind: Variable, range: "31:21-31:38", selection_range: "31:26-31:28", detail: "const char *" }
- { name: "r3", kind: Variable, range: "32:21-32:46", selection_range: "32:25-32:27", detail: "int" }

View File

@@ -0,0 +1,11 @@
---
source: folding_range_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { range: "1:19-35:1", kind: namespace, collapsed_text: "{...}" }
- { range: "3:19-7:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "9:24-17:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "20:28-27:1", kind: functionBody, collapsed_text: "{...}" }
- { range: "29:12-33:1", kind: functionBody, collapsed_text: "{...}" }

View File

@@ -0,0 +1,11 @@
---
source: inlay_hint_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { pos: "30:38", kind: Parameter, label: "x:", padding_right: true }
- { pos: "31:28", kind: Type, label: ": const char *" }
- { pos: "31:36", kind: Parameter, label: "x:", padding_right: true }
- { pos: "32:40", kind: Parameter, label: "a:", padding_right: true }
- { pos: "32:43", kind: Parameter, label: "b:", padding_right: true }

View File

@@ -0,0 +1,75 @@
---
source: semantic_tokens_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { loc: "0:0", text: "// basic if and if-else", kind: Comment }
- { loc: "1:0", text: "namespace", kind: Keyword }
- { loc: "1:10", text: "basic_if", kind: Namespace, modifiers: [Definition] }
- { loc: "3:0", text: "int", kind: Keyword }
- { loc: "3:4", text: "abs_val", kind: Function, modifiers: [Definition] }
- { loc: "3:12", text: "int", kind: Keyword }
- { loc: "3:16", text: "x", kind: Parameter, modifiers: [Definition] }
- { loc: "4:4", text: "if", kind: Keyword }
- { loc: "4:7", text: "x", kind: Parameter }
- { loc: "4:11", text: "0", kind: Number }
- { loc: "5:8", text: "return", kind: Keyword }
- { loc: "5:16", text: "x", kind: Parameter }
- { loc: "6:4", text: "return", kind: Keyword }
- { loc: "6:11", text: "x", kind: Parameter }
- { loc: "9:0", text: "const", kind: Keyword }
- { loc: "9:6", text: "char", kind: Keyword }
- { loc: "9:12", text: "sign", kind: Function, modifiers: [Definition, Readonly] }
- { loc: "9:17", text: "int", kind: Keyword }
- { loc: "9:21", text: "x", kind: Parameter, modifiers: [Definition] }
- { loc: "10:4", text: "if", kind: Keyword }
- { loc: "10:7", text: "x", kind: Parameter }
- { loc: "10:11", text: "0", kind: Number }
- { loc: "11:8", text: "return", kind: Keyword }
- { loc: "11:15", text: "\"positive\"", kind: String }
- { loc: "12:6", text: "else", kind: Keyword }
- { loc: "12:11", text: "if", kind: Keyword }
- { loc: "12:14", text: "x", kind: Parameter }
- { loc: "12:18", text: "0", kind: Number }
- { loc: "13:8", text: "return", kind: Keyword }
- { loc: "13:15", text: "\"negative\"", kind: String }
- { loc: "14:6", text: "else", kind: Keyword }
- { loc: "15:8", text: "return", kind: Keyword }
- { loc: "15:15", text: "\"zero\"", kind: String }
- { loc: "19:0", text: "// dangling else: else binds to nearest if", kind: Comment }
- { loc: "20:0", text: "int", kind: Keyword }
- { loc: "20:4", text: "nested_if", kind: Function, modifiers: [Definition] }
- { loc: "20:14", text: "int", kind: Keyword }
- { loc: "20:18", text: "a", kind: Parameter, modifiers: [Definition] }
- { loc: "20:21", text: "int", kind: Keyword }
- { loc: "20:25", text: "b", kind: Parameter, modifiers: [Definition] }
- { loc: "21:4", text: "if", kind: Keyword }
- { loc: "21:7", text: "a", kind: Parameter }
- { loc: "21:11", text: "0", kind: Number }
- { loc: "22:8", text: "if", kind: Keyword }
- { loc: "22:11", text: "b", kind: Parameter }
- { loc: "22:15", text: "0", kind: Number }
- { loc: "23:12", text: "return", kind: Keyword }
- { loc: "23:19", text: "1", kind: Number }
- { loc: "24:8", text: "else", kind: Keyword }
- { loc: "25:12", text: "return", kind: Keyword }
- { loc: "25:19", text: "2", kind: Number }
- { loc: "26:4", text: "return", kind: Keyword }
- { loc: "26:11", text: "0", kind: Number }
- { loc: "29:0", text: "void", kind: Keyword }
- { loc: "29:5", text: "test", kind: Function, modifiers: [Definition] }
- { loc: "30:21", text: "int", kind: Keyword }
- { loc: "30:25", text: "r1", kind: Variable, modifiers: [Definition] }
- { loc: "30:30", text: "abs_val", kind: Function }
- { loc: "30:39", text: "3", kind: Number }
- { loc: "31:21", text: "auto", kind: Keyword }
- { loc: "31:26", text: "r2", kind: Variable, modifiers: [Definition, Readonly] }
- { loc: "31:31", text: "sign", kind: Function, modifiers: [Readonly] }
- { loc: "31:36", text: "5", kind: Number }
- { loc: "32:21", text: "int", kind: Keyword }
- { loc: "32:25", text: "r3", kind: Variable, modifiers: [Definition] }
- { loc: "32:30", text: "nested_if", kind: Function }
- { loc: "32:40", text: "1", kind: Number }
- { loc: "32:44", text: "1", kind: Number }
- { loc: "35:3", text: "// namespace basic_if", kind: Comment }

View File

@@ -0,0 +1,28 @@
---
source: tu_index_tests.cpp
created_at: 2026-05-20
input_file: statements/if/basic_if.cpp
---
- { loc: "1:10", kind: Namespace, text: "basic_if", relations: [Definition] }
- { loc: "3:4", kind: Function, text: "abs_val", relations: [Definition] }
- { loc: "3:16", kind: Parameter, text: "x", relations: [Definition] }
- { loc: "4:7", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "5:16", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "6:11", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "9:12", kind: Function, text: "sign", relations: [Definition] }
- { loc: "9:21", kind: Parameter, text: "x", relations: [Definition] }
- { loc: "10:7", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "12:14", kind: Parameter, text: "x", relations: [Reference] }
- { loc: "20:4", kind: Function, text: "nested_if", relations: [Definition] }
- { loc: "20:18", kind: Parameter, text: "a", relations: [Definition] }
- { loc: "20:25", kind: Parameter, text: "b", relations: [Definition] }
- { loc: "21:7", kind: Parameter, text: "a", relations: [Reference] }
- { loc: "22:11", kind: Parameter, text: "b", relations: [Reference] }
- { loc: "29:5", kind: Function, text: "test", relations: [Definition] }
- { loc: "30:25", kind: Variable, text: "r1", relations: [Definition] }
- { loc: "30:30", kind: Function, text: "abs_val", relations: [Reference] }
- { loc: "31:26", kind: Variable, text: "r2", relations: [Definition] }
- { loc: "31:31", kind: Function, text: "sign", relations: [Reference] }
- { loc: "32:25", kind: Variable, text: "r3", relations: [Definition] }
- { loc: "32:30", kind: Function, text: "nested_if", relations: [Reference] }

View File

@@ -11,7 +11,7 @@ namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(DocumentLink, Tester) {
TEST_SUITE(document_link, Tester) {
std::vector<protocol::DocumentLink> links;
@@ -136,7 +136,7 @@ ABCDE
EXPECT_LINK(0, "0", TestVFS::path("data.bin"));
}
}; // TEST_SUITE(DocumentLink)
}; // TEST_SUITE(document_link)
} // namespace

View File

@@ -1,4 +1,5 @@
#include <cstddef>
#include <format>
#include <functional>
#include <memory>
#include <vector>
@@ -7,13 +8,15 @@
#include "test/tester.h"
#include "feature/feature.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(DocumentSymbol, Tester) {
TEST_SUITE(document_symbol, Tester) {
std::vector<protocol::DocumentSymbol> symbols;
@@ -180,7 +183,57 @@ VAR(test)
ASSERT_EQ(total_size(symbols), 3U);
}
}; // TEST_SUITE(DocumentSymbol)
void format_document_symbols(std::string& out,
const feature::PositionMapper& mapper,
llvm::ArrayRef<feature::DocumentSymbol> nodes,
int depth) {
auto pad = std::string(depth * 2, ' ');
for(auto& node: nodes) {
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(node.kind), "Unknown");
auto start = mapper.to_position(node.range.begin);
auto end = mapper.to_position(node.range.end);
if(!start || !end)
continue;
auto sel_start = mapper.to_position(node.selection_range.begin);
auto sel_end = mapper.to_position(node.selection_range.end);
out += std::format("- {}{{ name: {}, kind: {}, range: \"{}:{}-{}:{}\"",
pad,
yaml_str(node.name),
kind,
start->line,
start->character,
end->line,
end->character);
if(sel_start && sel_end) {
out += std::format(", selection_range: \"{}:{}-{}:{}\"",
sel_start->line,
sel_start->character,
sel_end->line,
sel_end->character);
}
if(!node.detail.empty()) {
out += std::format(", detail: {}", yaml_str(node.detail));
}
out += " }\n";
if(!node.children.empty()) {
format_document_symbols(out, mapper, node.children, depth + 1);
}
}
}
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
format_document_symbols(result, mapper, feature::document_symbols(*unit), 0);
return result;
});
}
}; // TEST_SUITE(document_symbol)
} // namespace

View File

@@ -11,7 +11,7 @@ namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(FoldingRange, Tester) {
TEST_SUITE(folding_range, Tester) {
std::vector<protocol::FoldingRange> ranges;
@@ -429,7 +429,36 @@ $(1)#pragma region level1
)cpp");
}
}; // TEST_SUITE(FoldingRange)
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto ranges = feature::folding_ranges(*unit);
feature::PositionMapper mapper(unit->interested_content(), feature::PositionEncoding::UTF8);
std::string result;
for(auto& r: ranges) {
auto start = mapper.to_position(r.range.begin);
auto end = mapper.to_position(r.range.end);
if(!start || !end)
continue;
result += std::format("- {{ range: \"{}:{}-{}:{}\"",
start->line,
start->character,
end->line,
end->character);
if(r.kind.has_value()) {
result += std::format(", kind: {}", static_cast<const std::string&>(*r.kind));
}
if(!r.collapsed_text.empty()) {
result += std::format(", collapsed_text: {}", yaml_str(r.collapsed_text));
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(folding_range)
} // namespace

View File

@@ -1,16 +1,19 @@
#include <format>
#include <string>
#include "test/test.h"
#include "test/tester.h"
#include "feature/feature.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
namespace protocol = kota::ipc::protocol;
TEST_SUITE(InlayHint, Tester) {
TEST_SUITE(inlay_hint, Tester) {
std::vector<protocol::InlayHint> hints;
llvm::DenseMap<std::uint32_t, protocol::InlayHint> hints_map;
@@ -1529,6 +1532,38 @@ TEST_CASE(Dependent, skip = true) {
EXPECT_HINT("2", "par3:");
}
}; // TEST_SUITE(InlayHint)
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
LocalSourceRange range(0, content.size());
auto hints = feature::inlay_hints(*unit, range);
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
for(auto& hint: hints) {
auto pos = mapper.to_position(hint.offset);
if(!pos)
continue;
auto kind = kota::meta::enum_name(hint.kind, "Unknown");
result += std::format("- {{ pos: \"{}:{}\", kind: {}, label: {}",
pos->line,
pos->character,
kind,
yaml_str(hint.label));
if(hint.padding_left) {
result += ", padding_left: true";
}
if(hint.padding_right) {
result += ", padding_right: true";
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(inlay_hint)
} // namespace
} // namespace clice::testing

View File

@@ -9,6 +9,8 @@
#include "feature/feature.h"
#include "semantic/symbol_kind.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
@@ -99,7 +101,7 @@ auto decode_relative_tokens(const protocol::SemanticTokens& tokens) -> std::vect
return result;
}
TEST_SUITE(SemanticTokens, Tester) {
TEST_SUITE(semantic_tokens, Tester) {
protocol::SemanticTokens tokens;
std::vector<DecodedToken> decoded;
@@ -539,7 +541,53 @@ void f() {
EXPECT_TOKEN("v2", SymbolKind::Variable, definition);
}
}; // TEST_SUITE(SemanticTokens)
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto content = unit->interested_content();
auto tokens = feature::semantic_tokens(*unit);
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
for(auto& token: tokens) {
if(!token.range.valid() || token.range.end <= token.range.begin ||
token.range.end > content.size())
continue;
auto pos = mapper.to_position(token.range.begin);
if(!pos)
continue;
auto text = content.substr(token.range.begin, token.range.length());
auto kind = kota::meta::enum_name(static_cast<SymbolKind::Kind>(token.kind), "Unknown");
result += std::format("- {{ loc: \"{}:{}\", text: {}, kind: {}",
pos->line,
pos->character,
yaml_str(text),
kind);
std::string mods;
for(std::uint32_t i = 0; i < 32; ++i) {
if(token.modifiers & (1u << i)) {
auto name = kota::meta::enum_name(static_cast<SymbolModifiers::Kind>(i));
if(!name.empty()) {
if(!mods.empty())
mods += ", ";
mods += name;
}
}
}
if(!mods.empty()) {
result += std::format(", modifiers: [{}]", mods);
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(semantic_tokens)
} // namespace

View File

@@ -1,13 +1,18 @@
#include <algorithm>
#include <format>
#include <set>
#include "test/test.h"
#include "test/tester.h"
#include "feature/feature.h"
#include "index/tu_index.h"
#include "kota/meta/enum.h"
namespace clice::testing {
namespace {
TEST_SUITE(TUIndex, Tester) {
TEST_SUITE(tu_index, Tester) {
index::TUIndex tu_index;
@@ -500,6 +505,64 @@ TEST_CASE(SymbolKinds) {
check_kind("ns", SymbolKind::Namespace);
}
}; // TEST_SUITE(TUIndex)
TEST_CASE(snapshot) {
ASSERT_SNAPSHOT_GLOB(corpus_dir, "**/*.cpp", [&](std::string_view path) -> std::string {
if(!compile_file(path))
return "COMPILE_ERROR";
auto idx = index::TUIndex::build(*unit);
auto content = unit->interested_content();
feature::PositionMapper mapper(content, feature::PositionEncoding::UTF8);
std::string result;
auto sorted = idx.main_file_index.occurrences;
std::ranges::sort(sorted, [](auto& lhs, auto& rhs) {
return std::tuple(lhs.range.begin, lhs.range.end, lhs.target) <
std::tuple(rhs.range.begin, rhs.range.end, rhs.target);
});
for(auto& occ: sorted) {
auto text = content.substr(occ.range.begin, occ.range.end - occ.range.begin);
auto pos = mapper.to_position(occ.range.begin);
if(!pos)
continue;
auto sym_it = idx.symbols.find(occ.target);
std::string_view kind_name = "?";
if(sym_it != idx.symbols.end()) {
kind_name =
kota::meta::enum_name(static_cast<SymbolKind::Kind>(sym_it->second.kind),
"Unknown");
}
result += std::format("- {{ loc: \"{}:{}\", kind: {}, text: {}",
pos->line,
pos->character,
kind_name,
yaml_str(text));
auto rel_it = idx.main_file_index.relations.find(occ.target);
if(rel_it != idx.main_file_index.relations.end()) {
std::string rels;
for(auto& rel: rel_it->second) {
if(rel.range != occ.range)
continue;
if(!rels.empty())
rels += ", ";
rels += kota::meta::enum_name(static_cast<RelationKind::Kind>(rel.kind), "?");
}
if(!rels.empty()) {
result += std::format(", relations: [{}]", rels);
}
}
result += " }\n";
}
return result;
});
}
}; // TEST_SUITE(tu_index)
} // namespace
} // namespace clice::testing

View File

@@ -1,3 +1,5 @@
#include <string>
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
@@ -5,6 +7,12 @@
namespace clice::testing {
/// Set by --test-dir from the command line; empty if not specified.
inline std::string test_dir;
/// Set by --corpus-dir from the command line; empty if not specified.
inline std::string corpus_dir;
#ifdef _WIN32
constexpr inline bool Windows = true;
#else

View File

@@ -230,6 +230,17 @@ bool Tester::compile_with_modules(llvm::StringRef standard) {
return try_compile();
}
bool Tester::compile_file(llvm::StringRef path, llvm::StringRef standard) {
auto buffer = llvm::MemoryBuffer::getFile(path);
if(!buffer) {
LOG_ERROR("Failed to read file: {}", path);
return false;
}
auto filename = llvm::sys::path::filename(path);
add_main(filename, (*buffer)->getBuffer());
return compile(standard);
}
std::uint32_t Tester::point(llvm::StringRef name, llvm::StringRef file) {
if(file.empty()) {
file = src_path;

View File

@@ -1,5 +1,6 @@
#pragma once
#include <format>
#include <optional>
#include <string>
#include <vector>
@@ -63,6 +64,9 @@ struct Tester {
bool compile_with_modules(llvm::StringRef standard = "-std=c++20");
/// Read a file from disk and compile it directly (no VFS content needed).
bool compile_file(llvm::StringRef path, llvm::StringRef standard = "-std=c++20");
/// Driver path: uses CompilationDatabase + toolchain cache, has system headers.
void prepare_driver(llvm::StringRef standard = "-std=c++20");
@@ -85,4 +89,28 @@ struct Tester {
void clear();
};
inline std::string yaml_str(llvm::StringRef s) {
std::string result;
result.reserve(s.size() + 2);
result += '"';
for(char c: s) {
switch(c) {
case '"': result += "\\\""; break;
case '\\': result += "\\\\"; break;
case '\n': result += "\\n"; break;
case '\r': result += "\\r"; break;
case '\t': result += "\\t"; break;
default:
if(static_cast<unsigned char>(c) < 0x20) {
result += std::format("\\x{:02x}", static_cast<unsigned char>(c));
} else {
result += c;
}
break;
}
}
result += '"';
return result;
}
} // namespace clice::testing

View File

@@ -1,6 +1,7 @@
#include <string>
#include <string_view>
#include "test/platform.h"
#include "support/logging.h"
#include "kota/deco/deco.h"
@@ -11,23 +12,20 @@ namespace {
using kota::deco::decl::KVStyle;
struct TestOptions {
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--test-filter", "--test-filter="},
help = "Filter tests by name",
required = false)
<std::string> test_filter;
kota::zest::Options zest;
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--log-level", "--log-level="},
help = "Log level: trace/debug/info/warn/err",
required = false)
DecoKVStyled(KVStyle::JoinedOrSeparate, help = "log level: trace/debug/info/warn/err";
required = false)
<std::string> log_level;
DecoKV(style = KVStyle::JoinedOrSeparate,
names = {"--test-dir", "--test-dir="},
help = "Test data directory",
required = false)
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>"; help = "test data directory";
required = false)
<std::string> test_dir;
DecoKVStyled(KVStyle::JoinedOrSeparate, meta_var = "<DIR>";
help = "corpus directory for snapshot glob tests";
required = false)
<std::string> corpus_dir;
};
} // namespace
@@ -36,13 +34,20 @@ int main(int argc, const char** argv) {
auto args = kota::deco::util::argvify(argc, argv);
auto parsed = kota::deco::cli::parse<TestOptions>(args);
std::string_view filter = {};
if(parsed.has_value() && parsed->options.test_filter.has_value()) {
filter = *parsed->options.test_filter;
if(!parsed.has_value()) {
return 1;
}
if(parsed.has_value() && parsed->options.log_level.has_value()) {
auto level = *parsed->options.log_level;
auto& opts = parsed->options;
if(opts.test_dir.has_value())
clice::testing::test_dir = *opts.test_dir;
if(opts.corpus_dir.has_value())
clice::testing::corpus_dir = *opts.corpus_dir;
if(opts.log_level.has_value()) {
auto level = *opts.log_level;
if(level == "trace") {
clice::logging::options.level = clice::logging::Level::trace;
} else if(level == "debug") {
@@ -58,5 +63,5 @@ int main(int argc, const char** argv) {
clice::logging::stderr_logger("test", clice::logging::options);
return kota::zest::Runner::instance().run_tests(filter);
return kota::zest::run_tests(std::move(opts.zest));
}