9 Commits

Author SHA1 Message Date
Myriad-Dreamin
04af0e51f8 test(agentic): remove lint integration test 2026-06-05 09:46:38 +08:00
Myriad-Dreamin
7a37222fd3 fix(prune): keep clang-tidy module libraries 2026-06-05 09:11:07 +08:00
Myriad-Dreamin
8e49d7b9e9 Force link clang-tidy check modules 2026-06-05 08:58:52 +08:00
Myriad-Dreamin
98e258d50c Format agentic lint test 2026-06-05 02:29:55 +08:00
Myriad-Dreamin
cb190d3d99 Test agentic clang-tidy lint diagnostics 2026-06-05 02:24:58 +08:00
Myriad-Dreamin
2baf947bff Run clang-tidy for agentic lint 2026-06-05 02:05:38 +08:00
Myriad-Dreamin
a17b3f6b7b Add agentic lint request 2026-06-05 01:26:54 +08:00
Myriad-Dreamin
3b45888622 fix(semantic-tokens): filter ineligible highlight references (#434)
Add a reusable declaration-name eligibility helper that mirrors clangd's
`canHighlightName`, use it to suppress unsupported reference tokens in
the semantic-token collector, and cover the change with focused
semantic-token regression tests plus a constructor/destructor positive
case.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved semantic token highlighting to suppress ineligible operator
references lacking meaningful source text.
* Ensured constructor and destructor names remain properly highlighted
with correct visual modifiers.

* **Tests**
* Added test coverage for semantic token highlighting behavior across
various declaration types.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/clice-io/clice/pull/434?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-05 00:03:18 +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
40 changed files with 907 additions and 156 deletions

3
.gitignore vendored
View File

@@ -68,8 +68,7 @@ tests/unit/Local/
.pixi/*
!.pixi/config.toml
.codex/
.codex
.claude/*
!.claude/CLAUDE.md
!.claude/commands/
openspec/

View File

@@ -124,8 +124,31 @@ if(CLICE_CI_ENVIRONMENT)
target_compile_definitions(clice_options INTERFACE CLICE_CI_ENVIRONMENT=1)
endif()
set(CLICE_CLANG_TIDY_MODULE_LIBRARIES)
set(CLICE_MISSING_CLANG_TIDY_MODULES)
foreach(module IN LISTS CLICE_CLANG_TIDY_MODULE_COMPONENTS)
find_library(CLICE_${module}_LIBRARY
NAMES "${module}"
PATHS "${LLVM_INSTALL_PATH}/lib"
NO_DEFAULT_PATH
)
if(CLICE_${module}_LIBRARY)
list(APPEND CLICE_CLANG_TIDY_MODULE_LIBRARIES "${CLICE_${module}_LIBRARY}")
else()
list(APPEND CLICE_MISSING_CLANG_TIDY_MODULES "${module}")
endif()
endforeach()
if(CLICE_MISSING_CLANG_TIDY_MODULES)
message(STATUS "Clang-tidy module libraries not available: ${CLICE_MISSING_CLANG_TIDY_MODULES}")
else()
target_compile_definitions(clice_options INTERFACE CLICE_HAS_CLANG_TIDY_MODULES=1)
endif()
set(FBS_SCHEMA_FILE "${PROJECT_SOURCE_DIR}/src/index/schema.fbs")
set(GENERATED_HEADER "${PROJECT_BINARY_DIR}/generated/schema_generated.h")
set(CLANG_TIDY_CONFIG_SOURCE_FILE "${PROJECT_SOURCE_DIR}/config/clang-tidy-config.h")
set(CLANG_TIDY_CONFIG_GENERATED_FILE "${PROJECT_BINARY_DIR}/generated/clang-tidy-config.h")
if(CMAKE_CROSSCOMPILING)
find_program(FLATC_EXECUTABLE flatc REQUIRED)
@@ -143,10 +166,21 @@ add_custom_command(
add_custom_target(generate_flatbuffers_schema DEPENDS "${GENERATED_HEADER}")
add_custom_command(
OUTPUT "${CLANG_TIDY_CONFIG_GENERATED_FILE}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${CLANG_TIDY_CONFIG_SOURCE_FILE}"
"${CLANG_TIDY_CONFIG_GENERATED_FILE}"
DEPENDS "${CLANG_TIDY_CONFIG_SOURCE_FILE}"
COMMENT "Generating C++ header from ${CLANG_TIDY_CONFIG_SOURCE_FILE}"
)
add_custom_target(generate_clang_tidy_config DEPENDS "${CLANG_TIDY_CONFIG_GENERATED_FILE}")
file(GLOB_RECURSE CLICE_CORE_SOURCES CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/src/*.cpp")
add_library(clice-core STATIC ${CLICE_CORE_SOURCES})
add_library(clice::core ALIAS clice-core)
add_dependencies(clice-core generate_flatbuffers_schema)
add_dependencies(clice-core generate_flatbuffers_schema generate_clang_tidy_config)
target_include_directories(clice-core PUBLIC
"${PROJECT_SOURCE_DIR}/src"
@@ -162,6 +196,9 @@ target_link_libraries(clice-core PUBLIC
kota::codec::toml
simdjson::simdjson
)
if(CLICE_CLANG_TIDY_MODULE_LIBRARIES)
target_link_libraries(clice-core PUBLIC ${CLICE_CLANG_TIDY_MODULE_LIBRARIES})
endif()
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")
target_link_libraries(clice PRIVATE clice::core kota::deco)

View File

@@ -1,5 +1,34 @@
include_guard()
set(CLICE_CLANG_TIDY_MODULE_COMPONENTS
# Keep this in sync with scripts/llvm-components.json and the old
# ALL_CLANG_TIDY_CHECKS list. MPIModule is intentionally excluded because
# clice disables static analyzer checks in ClangTidyForceLinker.h.
clangTidyAndroidModule
clangTidyAbseilModule
clangTidyAlteraModule
clangTidyBoostModule
clangTidyBugproneModule
clangTidyCERTModule
clangTidyConcurrencyModule
clangTidyCppCoreGuidelinesModule
clangTidyDarwinModule
clangTidyFuchsiaModule
clangTidyGoogleModule
clangTidyHICPPModule
clangTidyLinuxKernelModule
clangTidyLLVMModule
clangTidyLLVMLibcModule
clangTidyMiscModule
clangTidyModernizeModule
clangTidyObjCModule
clangTidyOpenMPModule
clangTidyPerformanceModule
clangTidyPortabilityModule
clangTidyReadabilityModule
clangTidyZirconModule
)
function(setup_llvm LLVM_VERSION)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
@@ -87,29 +116,6 @@ function(setup_llvm LLVM_VERSION)
clangSerialization
clangTidy
clangTidyUtils
clangTidyAndroidModule
clangTidyAbseilModule
clangTidyAlteraModule
clangTidyBoostModule
clangTidyBugproneModule
clangTidyCERTModule
clangTidyConcurrencyModule
clangTidyCppCoreGuidelinesModule
clangTidyDarwinModule
clangTidyFuchsiaModule
clangTidyGoogleModule
clangTidyHICPPModule
clangTidyLinuxKernelModule
clangTidyLLVMModule
clangTidyLLVMLibcModule
clangTidyMiscModule
clangTidyModernizeModule
clangTidyObjCModule
clangTidyOpenMPModule
clangTidyPerformanceModule
clangTidyPortabilityModule
clangTidyReadabilityModule
clangTidyZirconModule
clangTooling
clangToolingCore
clangToolingInclusions

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

@@ -16,7 +16,10 @@ import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List, Optional
from typing import Iterable, List, Optional, Set
LLVM_COMPONENTS_FILE = Path(__file__).with_name("llvm-components.json")
def parse_args() -> argparse.Namespace:
@@ -102,12 +105,33 @@ def run_build(build_dir: Path) -> bool:
return False
def protected_library_names() -> Set[str]:
data = json.loads(LLVM_COMPONENTS_FILE.read_text())
components = data.get("components", [])
if not isinstance(components, list):
raise ValueError(f"{LLVM_COMPONENTS_FILE} missing 'components' list")
names: Set[str] = set()
for component in components:
if not isinstance(component, str):
continue
if not (component.startswith("clangTidy") and component.endswith("Module")):
continue
names.add(f"lib{component}.a")
names.add(f"{component}.lib")
return names
def candidate_files(install_dir: Path) -> Iterable[Path]:
if not install_dir.is_dir():
raise FileNotFoundError(f"lib dir not found: {install_dir}")
protected = protected_library_names()
for path in sorted(install_dir.iterdir()):
if not path.is_file():
continue
if path.name in protected:
print(f"Keeping protected clang-tidy module library: {path.name}")
continue
if path.suffix.lower() in {".a", ".lib"}:
yield path
else:
@@ -156,7 +180,11 @@ def apply_manifest(manifest: Path, install_dir: Path) -> None:
removed = data.get("removed", [])
if not isinstance(removed, list):
raise ValueError("Manifest missing 'removed' list")
protected = protected_library_names()
for name in removed:
if name in protected:
print(f"Keeping protected clang-tidy module library from manifest: {name}")
continue
target = install_dir / name
if target.exists():
print(f"Deleting {target}")

View File

@@ -53,7 +53,7 @@ struct Options {
help =
"Agentic method (compileCommand, symbolSearch, definition, references, "
"documentSymbols, readSymbol, callGraph, typeHierarchy, projectFiles, "
"fileDeps, impactAnalysis, status, shutdown)",
"lint, fileDeps, impactAnalysis, status, shutdown)",
required = false)
<std::string> method;

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

@@ -418,6 +418,8 @@ CompilationUnit compile(CompilationParams& params, PCMInfo& out) {
}
CompilationUnit complete(CompilationParams& params, clang::CodeCompleteConsumer* consumer) {
params.kind = CompilationKind::Completion;
auto& [file, offset] = params.completion;
/// The location of clang is 1-1 based.

View File

@@ -65,7 +65,7 @@ struct PCMInfo : ModuleInfo {
struct CompilationParams {
/// The kind of this compilation.
CompilationKind kind;
CompilationKind kind = CompilationKind::Content;
/// Whether to run clang-tidy.
bool clang_tidy = false;

View File

@@ -12,6 +12,10 @@
#include "clang-tidy/ClangTidyDiagnosticConsumer.h"
#include "clang-tidy/ClangTidyModuleRegistry.h"
#include "clang-tidy/ClangTidyOptions.h"
#ifdef CLICE_HAS_CLANG_TIDY_MODULES
#define CLANG_TIDY_DISABLE_STATIC_ANALYZER_CHECKS
#include "clang-tidy/ClangTidyForceLinker.h"
#endif
namespace clice::tidy {

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);
}
@@ -40,6 +34,34 @@ bool is_dependent(const clang::Decl* D) {
return isa<clang::UnresolvedUsingValueDecl>(D);
}
/// Whether a declaration name is backed by source text that should be highlighted.
bool can_highlight_name(clang::DeclarationName name) {
switch(name.getNameKind()) {
case clang::DeclarationName::Identifier: {
auto* info = name.getAsIdentifierInfo();
return info && !info->getName().empty();
}
case clang::DeclarationName::CXXConstructorName:
case clang::DeclarationName::CXXDestructorName: {
return true;
}
case clang::DeclarationName::CXXConversionFunctionName:
case clang::DeclarationName::CXXOperatorName:
case clang::DeclarationName::CXXDeductionGuideName:
case clang::DeclarationName::CXXLiteralOperatorName:
case clang::DeclarationName::CXXUsingDirective:
case clang::DeclarationName::ObjCZeroArgSelector:
case clang::DeclarationName::ObjCOneArgSelector:
case clang::DeclarationName::ObjCMultiArgSelector: {
return false;
}
}
std::unreachable();
}
/// Returns true if `decl` is considered to be from a default/system library.
/// This currently checks the systemness of the file by include type, although
/// different heuristics may be used in the future (e.g. sysroot paths).
@@ -166,7 +188,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();
@@ -177,6 +199,10 @@ public:
void handleDeclOccurrence(const clang::NamedDecl* decl,
RelationKind relation,
clang::SourceLocation location) {
if(relation.isReference() && !can_highlight_name(decl->getDeclName())) {
return;
}
std::uint32_t modifiers = 0;
if(relation.is_one_of(RelationKind::Definition)) {
// todo: clangd add both Declaration and Definition modifiers for definitions.
@@ -398,7 +424,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 +440,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 +474,7 @@ private:
}
public:
std::vector<RawToken> tokens;
std::vector<SemanticToken> tokens;
};
class SemanticTokenEncoder {
@@ -458,7 +484,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 +568,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

@@ -669,6 +669,7 @@ kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptr<Session::P
params.path = file_path;
params.version = sess->version;
params.text = sess->text;
params.clang_tidy = workspace.config.project.clang_tidy.value;
if(!fill_compile_args(file_path, params.directory, params.arguments, sess)) {
finish_compile();
co_return;

View File

@@ -5,6 +5,7 @@
#include <string>
#include <vector>
#include "kota/ipc/lsp/protocol.h"
#include "kota/ipc/protocol.h"
namespace clice::agentic {
@@ -202,6 +203,13 @@ struct TypeHierarchyResult {
std::vector<TypeHierarchyEntry> subtypes;
};
struct LintParams {
std::string path;
std::optional<int> line;
};
using LintResult = std::vector<kota::ipc::protocol::Diagnostic>;
struct StatusParams {};
struct StatusResult {
@@ -283,6 +291,12 @@ struct RequestTraits<clice::agentic::TypeHierarchyParams> {
constexpr inline static std::string_view method = "agentic/typeHierarchy";
};
template <>
struct RequestTraits<clice::agentic::LintParams> {
using Result = clice::agentic::LintResult;
constexpr inline static std::string_view method = "agentic/lint";
};
template <>
struct RequestTraits<clice::agentic::StatusParams> {
using Result = clice::agentic::StatusResult;

View File

@@ -43,6 +43,7 @@ struct CompileParams {
std::string text;
std::string directory;
std::vector<std::string> arguments;
bool clang_tidy = false;
std::pair<std::string, uint32_t> pch;
std::unordered_map<std::string, std::string> pcms;
};

View File

@@ -6,11 +6,14 @@
#include <string>
#include <vector>
#include "compile/compilation.h"
#include "feature/feature.h"
#include "server/protocol/agentic.h"
#include "server/service/master_server.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "kota/async/async.h"
#include "kota/ipc/lsp/uri.h"
#include "kota/meta/enum.h"
#include "llvm/ADT/DenseSet.h"
@@ -769,6 +772,36 @@ AgentClient::AgentClient(MasterServer& server, kota::ipc::JsonPeer& peer) :
co_return result;
});
peer.on_request([&srv](RequestContext&, const LintParams& params) -> RequestResult<LintParams> {
std::string directory;
std::vector<std::string> arguments;
if(!srv.compiler.fill_compile_args(params.path, directory, arguments)) {
co_return kota::outcome_error(
kota::ipc::Error{std::format("no compile command found for {}", params.path)});
}
auto result = co_await kota::queue([path = params.path,
directory = std::move(directory),
arguments = std::move(arguments)]() mutable {
CompilationParams cp;
cp.kind = CompilationKind::Content;
cp.clang_tidy = true;
cp.directory = std::move(directory);
for(auto& arg: arguments) {
cp.arguments.push_back(arg.c_str());
}
auto unit = compile(cp);
if(!unit.completed() && !unit.fatal_error()) {
LOG_WARN("Lint compilation failed: {}", path);
return LintResult{};
}
return feature::diagnostics(unit);
});
co_return result.value();
});
peer.on_request([&srv](RequestContext&, const StatusParams&) -> RequestResult<StatusParams> {
StatusResult result;
result.idle = srv.indexer.is_idle();

View File

@@ -85,6 +85,9 @@ static kota::task<> agentic_request(kota::ipc::JsonPeer& peer,
.line = line,
.direction = dir,
});
} else if(opts.method == "lint") {
auto line = opts.line > 0 ? std::optional(opts.line) : std::nullopt;
ok = co_await send_and_print(peer, agentic::LintParams{.path = opts.path, .line = line});
} else if(opts.method == "fileDeps") {
auto dir = opts.direction.empty() ? std::nullopt : std::optional(opts.direction);
ok = co_await send_and_print(peer,

View File

@@ -152,6 +152,7 @@ void StatefulWorker::register_handlers() {
CompilationParams cp;
cp.kind = CompilationKind::Content;
cp.clang_tidy = params.clang_tidy;
fill_args(cp, doc->directory, doc->arguments);
if(!doc->pch.first.empty()) {
cp.pch = doc->pch;
@@ -245,26 +246,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

@@ -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

@@ -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

@@ -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

@@ -1,5 +1,6 @@
#include "test/test.h"
#include "compile/compilation.h"
#include "compile/implement.h"
namespace clice::testing {
namespace {
@@ -7,6 +8,10 @@ namespace {
TEST_SUITE(ClangTidy) {
TEST_CASE(FastCheck) {
#ifdef CLICE_HAS_CLANG_TIDY_MODULES
ASSERT_TRUE(tidy::is_registered_tidy_check("bugprone-integer-division"));
#endif
// ASSERT_TRUE(tidy::is_fast_tidy_check("readability-misleading-indentation"));
// ASSERT_TRUE(tidy::is_fast_tidy_check("bugprone-unused-return-value"));
//
@@ -22,6 +27,7 @@ TEST_CASE(Tidy) {
std::string main_path = TestVFS::path("main.cpp");
CompilationParams params;
params.kind = CompilationKind::Content;
params.clang_tidy = true;
params.vfs = vfs;
params.arguments = {"clang++", "-ffreestanding", "-Xclang", "-undef", main_path.c_str()};
@@ -30,6 +36,37 @@ TEST_CASE(Tidy) {
ASSERT_FALSE(unit.diagnostics().empty());
}
#ifdef CLICE_HAS_CLANG_TIDY_MODULES
TEST_CASE(BugproneIntegerDivision) {
auto vfs = llvm::makeIntrusiveRefCnt<TestVFS>();
vfs->add("main.cpp",
"int main() {"
" double d;"
" int i = 42;"
" d = 32 * 8 / (2 + i);"
" return static_cast<int>(d);"
"}");
std::string main_path = TestVFS::path("main.cpp");
CompilationParams params;
params.kind = CompilationKind::Content;
params.clang_tidy = true;
params.vfs = vfs;
params.arguments = {"clang++", "-ffreestanding", "-Xclang", "-undef", main_path.c_str()};
auto unit = compile(params);
ASSERT_TRUE(unit.completed());
bool found = false;
for(auto& diagnostic: unit.diagnostics()) {
if(llvm::StringRef(diagnostic.message).contains("integer division")) {
found = true;
break;
}
}
ASSERT_TRUE(found);
}
#endif
}; // TEST_SUITE(ClangTidy)
} // namespace
} // namespace clice::testing

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;
@@ -138,6 +140,10 @@ void EXPECT_TOKEN(llvm::StringRef name,
ASSERT_EQ(token->modifiers, expected_modifiers);
}
void EXPECT_NO_TOKEN(llvm::StringRef name) {
ASSERT_TRUE(find_by_range(name) == nullptr);
}
TEST_CASE(BasicLexicalKinds) {
run_utf8(R"cpp(
@d1[#define] @m0[FOO]
@@ -264,6 +270,44 @@ int main() {
EXPECT_TOKEN("x3", SymbolKind::Variable, 0);
}
TEST_CASE(IneligibleOperatorReferenceIsSuppressed) {
run_utf8(R"cpp(
struct S {};
S operator+(S lhs, S rhs);
void use(S lhs, S rhs) {
(void)(lhs @plus[+] rhs);
}
)cpp");
EXPECT_NO_TOKEN("plus");
}
TEST_CASE(ConstructorAndDestructorNamesRemainHighlighted) {
run_utf8(R"cpp(
struct S {
@ctor_decl[S]();
@dtor_decl[~]S();
};
S::@ctor_def[S]() {}
void use(S* value) {
value->@dtor_ref[~]S();
}
)cpp");
auto declaration = modifier_mask({SymbolModifiers::Declaration});
auto definition = modifier_mask({SymbolModifiers::Definition});
auto special_member = modifier_mask({SymbolModifiers::ConstructorOrDestructor});
EXPECT_TOKEN("ctor_decl", SymbolKind::Method, declaration | special_member);
EXPECT_TOKEN("dtor_decl", SymbolKind::Method, declaration | special_member);
EXPECT_TOKEN("ctor_def", SymbolKind::Method, definition | special_member);
EXPECT_TOKEN("dtor_ref", SymbolKind::Method, special_member);
}
TEST_CASE(LegacyVarDeclTemplates) {
run_utf8(R"cpp(
extern int @x1[x];
@@ -539,7 +583,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));
}