8 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
17 changed files with 269 additions and 29 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

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

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

@@ -34,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).
@@ -171,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.

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;

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

@@ -140,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]
@@ -266,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];