diff --git a/CMakeLists.txt b/CMakeLists.txt index bbca11d60..8ed192591 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,6 +126,8 @@ 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 +145,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" diff --git a/cmake/llvm.cmake b/cmake/llvm.cmake index 694857ef5..5c83fb5f8 100644 --- a/cmake/llvm.cmake +++ b/cmake/llvm.cmake @@ -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) @@ -69,6 +98,21 @@ function(setup_llvm LLVM_VERSION) # add to include directories target_include_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/include") + set(CLICE_MISSING_CLANG_TIDY_MODULES) + foreach(module IN LISTS CLICE_CLANG_TIDY_MODULE_COMPONENTS) + set(module_library "${LLVM_INSTALL_PATH}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${module}${CMAKE_STATIC_LIBRARY_SUFFIX}") + if(NOT EXISTS "${module_library}") + 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(llvm-libs INTERFACE CLICE_HAS_CLANG_TIDY_MODULES=1) + set(CLICE_DEBUG_CLANG_TIDY_MODULE_LIBRARIES ${CLICE_CLANG_TIDY_MODULE_COMPONENTS}) + endif() + if(CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT WIN32) target_link_directories(llvm-libs INTERFACE "${LLVM_INSTALL_PATH}/lib") target_link_libraries(llvm-libs INTERFACE @@ -87,29 +131,7 @@ 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 + ${CLICE_DEBUG_CLANG_TIDY_MODULE_LIBRARIES} clangTooling clangToolingCore clangToolingInclusions diff --git a/scripts/prune-llvm-bin.py b/scripts/prune-llvm-bin.py index 9a392156f..f1577d373 100644 --- a/scripts/prune-llvm-bin.py +++ b/scripts/prune-llvm-bin.py @@ -16,7 +16,10 @@ 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}") diff --git a/src/clice.cc b/src/clice.cc index fccae05e4..30a23db6b 100644 --- a/src/clice.cc +++ b/src/clice.cc @@ -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) method; diff --git a/src/compile/compilation.cpp b/src/compile/compilation.cpp index ffc5bf386..ce42a7a90 100644 --- a/src/compile/compilation.cpp +++ b/src/compile/compilation.cpp @@ -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. diff --git a/src/compile/compilation.h b/src/compile/compilation.h index 9f3ba0ed4..1900b4189 100644 --- a/src/compile/compilation.h +++ b/src/compile/compilation.h @@ -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; diff --git a/src/compile/tidy.cpp b/src/compile/tidy.cpp index b0e180e2c..919cb0efa 100644 --- a/src/compile/tidy.cpp +++ b/src/compile/tidy.cpp @@ -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 { diff --git a/src/server/compiler/compiler.cpp b/src/server/compiler/compiler.cpp index ded067958..92d834a66 100644 --- a/src/server/compiler/compiler.cpp +++ b/src/server/compiler/compiler.cpp @@ -669,6 +669,7 @@ kota::task<> Compiler::run_compile(std::uint32_t pid, std::shared_ptrversion; 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; diff --git a/src/server/protocol/agentic.h b/src/server/protocol/agentic.h index 90f32568e..810d48889 100644 --- a/src/server/protocol/agentic.h +++ b/src/server/protocol/agentic.h @@ -5,6 +5,7 @@ #include #include +#include "kota/ipc/lsp/protocol.h" #include "kota/ipc/protocol.h" namespace clice::agentic { @@ -202,6 +203,13 @@ struct TypeHierarchyResult { std::vector subtypes; }; +struct LintParams { + std::string path; + std::optional line; +}; + +using LintResult = std::vector; + struct StatusParams {}; struct StatusResult { @@ -283,6 +291,12 @@ struct RequestTraits { constexpr inline static std::string_view method = "agentic/typeHierarchy"; }; +template <> +struct RequestTraits { + using Result = clice::agentic::LintResult; + constexpr inline static std::string_view method = "agentic/lint"; +}; + template <> struct RequestTraits { using Result = clice::agentic::StatusResult; diff --git a/src/server/protocol/worker.h b/src/server/protocol/worker.h index 4fe9e7153..1535d6486 100644 --- a/src/server/protocol/worker.h +++ b/src/server/protocol/worker.h @@ -43,6 +43,7 @@ struct CompileParams { std::string text; std::string directory; std::vector arguments; + bool clang_tidy = false; std::pair pch; std::unordered_map pcms; }; diff --git a/src/server/service/agent_client.cpp b/src/server/service/agent_client.cpp index 69c7a81b2..13daeb06e 100644 --- a/src/server/service/agent_client.cpp +++ b/src/server/service/agent_client.cpp @@ -6,11 +6,14 @@ #include #include +#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 { + std::string directory; + std::vector 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 { StatusResult result; result.idle = srv.indexer.is_idle(); diff --git a/src/server/service/agentic.cpp b/src/server/service/agentic.cpp index 584f13e13..1276455af 100644 --- a/src/server/service/agentic.cpp +++ b/src/server/service/agentic.cpp @@ -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, diff --git a/src/server/worker/stateful_worker.cpp b/src/server/worker/stateful_worker.cpp index 18999ed47..699d538b4 100644 --- a/src/server/worker/stateful_worker.cpp +++ b/src/server/worker/stateful_worker.cpp @@ -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; diff --git a/tests/integration/agentic/test_agentic.py b/tests/integration/agentic/test_agentic.py index 549647a4b..d2beb8060 100644 --- a/tests/integration/agentic/test_agentic.py +++ b/tests/integration/agentic/test_agentic.py @@ -527,6 +527,82 @@ async def test_rpc_impact_analysis_unknown(indexed_agentic, workspace): assert resp["result"]["directDependents"] == [] +async def test_rpc_lint_clang_tidy_diagnostics(executable, tmp_path): + """agentic/lint returns a Diagnostic[] for clang-tidy results.""" + from tests.integration.utils.client import CliceClient + from tests.conftest import _shutdown_client, _find_free_port + + workspace = tmp_path / "clang_tidy" + workspace.mkdir() + + clean = workspace / "clean.cpp" + clean.write_text( + "int add(int a, int b) { return a + b; }\nint main() { return add(1, 2); }\n" + ) + + problem = workspace / "problem.cpp" + problem.write_text( + "int main() {\n" + " double d;\n" + " int i = 42;\n" + " d = 32 * 8 / (2 + i);\n" + " return static_cast(d);\n" + "}\n" + ) + + entries = [] + for source in (clean, problem): + entries.append( + { + "directory": workspace.as_posix(), + "file": source.as_posix(), + "arguments": [ + "clang++", + "-std=c++17", + "-fsyntax-only", + source.as_posix(), + ], + } + ) + (workspace / "compile_commands.json").write_text(json.dumps(entries)) + + host = "127.0.0.1" + port = _find_free_port() + cmd = [str(executable), "--mode", "pipe", "--host", host, "--port", str(port)] + + c = CliceClient() + await c.start_io(*cmd) + init_options = {"project": {"cache_dir": str(workspace / ".clice")}} + await c.initialize(workspace, initialization_options=init_options) + + rpc = AgenticRpcClient(host, port) + try: + clean_resp = rpc.request("agentic/lint", {"path": clean.as_posix()}) + assert "result" in clean_resp, f"unexpected response: {clean_resp}" + assert clean_resp["result"] == [] + + problem_resp = rpc.request("agentic/lint", {"path": problem.as_posix()}) + assert "result" in problem_resp, f"unexpected response: {problem_resp}" + diagnostics = problem_resp["result"] + assert isinstance(diagnostics, list) + assert diagnostics, "expected at least one clang-tidy diagnostic" + + diag = next( + (d for d in diagnostics if "integer division" in d.get("message", "")), + diagnostics[0], + ) + assert isinstance(diag["message"], str) + assert "range" in diag + assert set(diag["range"].keys()) == {"start", "end"} + for key in ("start", "end"): + assert isinstance(diag["range"][key]["line"], int) + assert isinstance(diag["range"][key]["character"], int) + assert isinstance(diag.get("severity"), int) + finally: + rpc.close() + await _shutdown_client(c) + + async def test_shutdown_during_indexing(executable, tmp_path): """Shutdown during active background indexing must exit cleanly.""" from tests.integration.utils.client import CliceClient diff --git a/tests/unit/compile/tidy_tests.cpp b/tests/unit/compile/tidy_tests.cpp index 037d04ce9..b222fab97 100644 --- a/tests/unit/compile/tidy_tests.cpp +++ b/tests/unit/compile/tidy_tests.cpp @@ -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(); + vfs->add("main.cpp", + "int main() {" + " double d;" + " int i = 42;" + " d = 32 * 8 / (2 + i);" + " return static_cast(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