## 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>
241 lines
5.1 KiB
C++
241 lines
5.1 KiB
C++
#include <cstddef>
|
|
#include <format>
|
|
#include <functional>
|
|
#include <memory>
|
|
#include <vector>
|
|
|
|
#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(document_symbol, Tester) {
|
|
|
|
std::vector<protocol::DocumentSymbol> symbols;
|
|
|
|
void run(llvm::StringRef code) {
|
|
add_main("main.cpp", code);
|
|
ASSERT_TRUE(compile_with_pch());
|
|
symbols = feature::document_symbols(*unit, feature::PositionEncoding::UTF8);
|
|
}
|
|
|
|
auto total_size(const std::vector<protocol::DocumentSymbol>& nodes) -> std::size_t {
|
|
std::size_t size = 0;
|
|
|
|
std::function<void(const std::vector<protocol::DocumentSymbol>&)> visit_symbol_vector;
|
|
std::function<void(const std::vector<std::shared_ptr<protocol::DocumentSymbol>>&)>
|
|
visit_ptr_vector;
|
|
|
|
visit_symbol_vector = [&](const std::vector<protocol::DocumentSymbol>& current) {
|
|
for(const auto& node: current) {
|
|
++size;
|
|
if(node.children.has_value()) {
|
|
visit_ptr_vector(*node.children);
|
|
}
|
|
}
|
|
};
|
|
|
|
visit_ptr_vector = [&](const std::vector<std::shared_ptr<protocol::DocumentSymbol>>& current) {
|
|
for(const auto& node: current) {
|
|
++size;
|
|
if(node->children.has_value()) {
|
|
visit_ptr_vector(*node->children);
|
|
}
|
|
}
|
|
};
|
|
|
|
visit_symbol_vector(nodes);
|
|
return size;
|
|
}
|
|
|
|
TEST_CASE(Namespace) {
|
|
run(R"cpp(
|
|
namespace _1 {
|
|
namespace _2 {
|
|
|
|
}
|
|
}
|
|
|
|
namespace _1 {
|
|
namespace _2 {
|
|
namespace _3 {
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {}
|
|
|
|
namespace _1::_2{
|
|
}
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 8U);
|
|
}
|
|
|
|
TEST_CASE(Struct) {
|
|
run(R"cpp(
|
|
struct _1 {};
|
|
struct _2 {};
|
|
|
|
struct _3 {
|
|
struct _4 {};
|
|
struct _5 {};
|
|
};
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 5U);
|
|
}
|
|
|
|
TEST_CASE(Field) {
|
|
run(R"cpp(
|
|
struct x {
|
|
int x1;
|
|
int x2;
|
|
|
|
struct y {
|
|
int y1;
|
|
int y2;
|
|
};
|
|
|
|
static int x3;
|
|
};
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 7U);
|
|
}
|
|
|
|
TEST_CASE(Constructor) {
|
|
run(R"cpp(
|
|
struct S {
|
|
int x;
|
|
|
|
S(): x(0) {}
|
|
S(int x) : x(x) {}
|
|
S(const S& s) : x(s.x) {}
|
|
~S() {}
|
|
};
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 6U);
|
|
}
|
|
|
|
TEST_CASE(Method) {
|
|
run(R"cpp(
|
|
struct _0 {
|
|
void f(int x) {}
|
|
void f(int* x) {}
|
|
void f1(int& x) {}
|
|
void f2(const int& x) {}
|
|
void f2(const _0& x) {}
|
|
void f2(_0 x) {}
|
|
};
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 7U);
|
|
}
|
|
|
|
TEST_CASE(Enum) {
|
|
run(R"cpp(
|
|
enum class A {
|
|
_1,
|
|
_2,
|
|
_3,
|
|
};
|
|
|
|
enum B {
|
|
_a,
|
|
_b,
|
|
_c,
|
|
};
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 8U);
|
|
}
|
|
|
|
TEST_CASE(TopLevelVariable) {
|
|
run(R"cpp(
|
|
constexpr auto x = 1;
|
|
int y = 2;
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 2U);
|
|
}
|
|
|
|
TEST_CASE(Macro) {
|
|
run(R"cpp(
|
|
#define CLASS(X) class X
|
|
|
|
CLASS(test) {
|
|
int x = 1;
|
|
};
|
|
|
|
#define VAR(X) int X = 1;
|
|
VAR(test)
|
|
)cpp");
|
|
|
|
ASSERT_EQ(total_size(symbols), 3U);
|
|
}
|
|
|
|
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
|
|
|
|
} // namespace clice::testing
|