refactor: simplify CompilationDatabase, extract ArgumentParser, remove pimpl (#371)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ykiko
2026-03-27 13:15:49 +08:00
committed by GitHub
parent 498c975042
commit 46ba1e4db6
19 changed files with 1724 additions and 1824 deletions

View File

@@ -131,10 +131,10 @@ add_custom_target(generate_flatbuffers_schema DEPENDS "${GENERATED_HEADER}")
# Temporary migration-only build graph.
add_library(clice-core STATIC
"${PROJECT_SOURCE_DIR}/src/command/argument_parser.cpp"
"${PROJECT_SOURCE_DIR}/src/command/command.cpp"
"${PROJECT_SOURCE_DIR}/src/command/search_config.cpp"
"${PROJECT_SOURCE_DIR}/src/command/toolchain.cpp"
"${PROJECT_SOURCE_DIR}/src/command/toolchain_provider.cpp"
"${PROJECT_SOURCE_DIR}/src/compile/compilation.cpp"
"${PROJECT_SOURCE_DIR}/src/compile/compilation_unit.cpp"
"${PROJECT_SOURCE_DIR}/src/compile/diagnostic.cpp"
@@ -187,6 +187,7 @@ target_link_libraries(clice-core PUBLIC
flatbuffers
eventide::ipc::lsp
eventide::serde::toml
simdjson::simdjson
)
add_executable(clice "${PROJECT_SOURCE_DIR}/src/clice.cc")

View File

@@ -0,0 +1,224 @@
#include "command/argument_parser.h"
#include <array>
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/raw_ostream.h"
#include "clang/Driver/Driver.h"
#include "clang/Driver/Options.h"
namespace clice {
namespace {
namespace opt = llvm::opt;
namespace driver = clang::driver;
/// Access private members of OptTable via the Thief pattern.
bool enable_dash_dash_parsing(const opt::OptTable& table);
bool enable_grouped_short_options(const opt::OptTable& table);
template <auto MP1, auto MP2>
struct Thief {
friend bool enable_dash_dash_parsing(const opt::OptTable& table) {
return table.*MP1;
}
friend bool enable_grouped_short_options(const opt::OptTable& table) {
return table.*MP2;
}
};
template struct Thief<&opt::OptTable::DashDashParsing, &opt::OptTable::GroupedShortOptions>;
auto& option_table = driver::getDriverOptTable();
} // namespace
std::unique_ptr<llvm::opt::Arg> ArgumentParser::parse_one(unsigned& index) {
assert(!enable_dash_dash_parsing(option_table));
assert(!enable_grouped_short_options(option_table));
return option_table.ParseOneArg(*this, index);
}
using ID = clang::driver::options::ID;
bool is_discarded_option(unsigned id) {
switch(id) {
/// Input file and output — we manage these ourselves.
case ID::OPT_INPUT:
case ID::OPT_c:
case ID::OPT_o:
case ID::OPT_dxc_Fc:
case ID::OPT_dxc_Fo:
/// PCH building.
case ID::OPT_emit_pch:
case ID::OPT_include_pch:
case ID::OPT__SLASH_Yu:
case ID::OPT__SLASH_Fp:
/// Dependency scan.
case ID::OPT_E:
case ID::OPT_M:
case ID::OPT_MM:
case ID::OPT_MD:
case ID::OPT_MMD:
case ID::OPT_MF:
case ID::OPT_MT:
case ID::OPT_MQ:
case ID::OPT_MG:
case ID::OPT_MP:
case ID::OPT_show_inst:
case ID::OPT_show_encoding:
case ID::OPT_show_includes:
case ID::OPT__SLASH_showFilenames:
case ID::OPT__SLASH_showFilenames_:
case ID::OPT__SLASH_showIncludes:
case ID::OPT__SLASH_showIncludes_user:
/// C++ modules — we handle these ourselves.
case ID::OPT_fmodule_file:
case ID::OPT_fmodule_output:
case ID::OPT_fprebuilt_module_path: return true;
default: return false;
}
}
bool is_user_content_option(unsigned id) {
switch(id) {
case ID::OPT_I:
case ID::OPT_isystem:
case ID::OPT_iquote:
case ID::OPT_idirafter:
case ID::OPT_D:
case ID::OPT_U:
case ID::OPT_include: return true;
default: return false;
}
}
bool is_include_path_option(unsigned id) {
switch(id) {
case ID::OPT_I:
case ID::OPT_isystem:
case ID::OPT_iquote:
case ID::OPT_idirafter: return true;
default: return false;
}
}
bool is_xclang_option(unsigned id) {
return id == ID::OPT_Xclang;
}
std::optional<std::uint32_t> get_option_id(llvm::StringRef argument) {
llvm::SmallString<64> buffer = argument;
if(argument.ends_with("=")) {
buffer += "placeholder";
}
unsigned index = 1;
std::array arguments = {"clang++", buffer.c_str(), "placeholder"};
llvm::opt::InputArgList arg_list(arguments.data(), arguments.data() + arguments.size());
if(auto arg = option_table.ParseOneArg(arg_list, index)) {
return arg->getOption().getID();
} else {
return {};
}
}
llvm::StringRef resource_dir() {
static std::string dir = [] {
// Use address of this lambda to locate our binary via dladdr/proc.
static int anchor;
auto exe = llvm::sys::fs::getMainExecutable("", &anchor);
if(exe.empty()) {
return std::string{};
}
return clang::driver::Driver::GetResourcesPath(exe);
}();
return dir;
}
bool is_codegen_option(unsigned id, const llvm::opt::Option& opt) {
/// Debug info options form a group (-g, -gdwarf-*, -gsplit-dwarf, etc.).
if(opt.matches(ID::OPT_DebugInfo_Group)) {
return true;
}
switch(id) {
/// Position-independent code — pure codegen, no macro or semantic effect.
case ID::OPT_fPIC:
case ID::OPT_fno_PIC:
case ID::OPT_fpic:
case ID::OPT_fno_pic:
case ID::OPT_fPIE:
case ID::OPT_fno_PIE:
case ID::OPT_fpie:
case ID::OPT_fno_pie:
/// Frame pointer and unwind tables — pure codegen.
case ID::OPT_fomit_frame_pointer:
case ID::OPT_fno_omit_frame_pointer:
case ID::OPT_funwind_tables:
case ID::OPT_fno_unwind_tables:
case ID::OPT_fasynchronous_unwind_tables:
case ID::OPT_fno_asynchronous_unwind_tables:
/// Stack protection — pure codegen.
case ID::OPT_fstack_protector:
case ID::OPT_fstack_protector_strong:
case ID::OPT_fstack_protector_all:
case ID::OPT_fno_stack_protector:
/// Section splitting, LTO, semantic interposition — pure codegen/linker.
case ID::OPT_fdata_sections:
case ID::OPT_fno_data_sections:
case ID::OPT_ffunction_sections:
case ID::OPT_fno_function_sections:
case ID::OPT_flto:
case ID::OPT_flto_EQ:
case ID::OPT_fno_lto:
case ID::OPT_fsemantic_interposition:
case ID::OPT_fno_semantic_interposition:
case ID::OPT_fvisibility_inlines_hidden:
/// Diagnostics output formatting — doesn't affect analysis.
case ID::OPT_fcolor_diagnostics:
case ID::OPT_fno_color_diagnostics:
/// Floating-point codegen — doesn't define macros (unlike -ffast-math).
case ID::OPT_ftrapping_math:
case ID::OPT_fno_trapping_math: return true;
default: return false;
}
}
std::string print_argv(llvm::ArrayRef<const char*> args) {
std::string buf;
llvm::raw_string_ostream os(buf);
bool sep = false;
for(llvm::StringRef arg: args) {
if(sep)
os << ' ';
sep = true;
if(llvm::all_of(arg, llvm::isPrint) &&
arg.find_first_of(" \t\n\"\\") == llvm::StringRef::npos) {
os << arg;
continue;
}
os << '"';
os.write_escaped(arg, /*UseHexEscapes=*/true);
os << '"';
}
return std::move(os.str());
}
} // namespace clice

View File

@@ -0,0 +1,114 @@
#pragma once
#include <cassert>
#include <cstdint>
#include <memory>
#include <optional>
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Option/ArgList.h"
#include "llvm/Support/Allocator.h"
namespace clice {
class ArgumentParser final : public llvm::opt::ArgList {
public:
ArgumentParser(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {}
~ArgumentParser() {
/// We never use the private `Args` field, so make sure it's empty.
if(getArgs().size() != 0) {
std::abort();
}
}
const char* getArgString(unsigned index) const override {
return arguments[index];
}
unsigned getNumInputArgStrings() const override {
return arguments.size();
}
const char* MakeArgStringRef(llvm::StringRef s) const override {
auto p = allocator->Allocate<char>(s.size() + 1);
std::ranges::copy(s, p);
p[s.size()] = '\0';
return p;
}
/// Parse a single argument at the given index. Defined out-of-line in
/// argument_parser.cpp to isolate the heavy clang driver option table include.
std::unique_ptr<llvm::opt::Arg> parse_one(unsigned& index);
void parse(llvm::ArrayRef<const char*> arguments, const auto& on_parse, const auto& on_error) {
this->arguments = arguments;
unsigned it = 0;
while(it != arguments.size()) {
llvm::StringRef s = arguments[it];
if(s.empty()) [[unlikely]] {
it += 1;
continue;
}
auto prev = it;
auto arg = parse_one(it);
assert(it > prev && "parser failed to consume argument");
if(!arg) [[unlikely]] {
assert(it >= arguments.size() && "unexpected parser error!");
assert(it - prev - 1 && "no missing arguments!");
on_error(prev, it - prev - 1);
break;
}
on_parse(std::move(arg));
}
}
private:
llvm::BumpPtrAllocator* allocator;
llvm::ArrayRef<const char*> arguments;
};
/// Check if an option is a codegen-only flag that doesn't affect frontend
/// semantics (parsing, diagnostics, code completion). These are pure
/// backend/linker concerns irrelevant to an LSP server.
///
/// Note: options that DO affect semantics are intentionally kept:
/// -fno-exceptions, -fno-rtti, -std=*, -march=*, -fsanitize=*, -O*, -W*
///
/// Defined out-of-line in argument_parser.cpp (needs clang driver option IDs).
bool is_codegen_option(unsigned id, const llvm::opt::Option& opt);
/// Options that are completely irrelevant to an LSP and should be discarded
/// (input/output, PCH building, dependency scan, C++ modules).
bool is_discarded_option(unsigned id);
/// User-content options that go into the per-file patch rather than the
/// shared canonical command: -I, -D, -U, -include, -isystem, -iquote, -idirafter.
bool is_user_content_option(unsigned id);
/// Subset of user-content options that are include-path flags
/// (-I, -isystem, -iquote, -idirafter) — used for path absolutization.
bool is_include_path_option(unsigned id);
/// Check if this is the -Xclang pass-through option.
bool is_xclang_option(unsigned id);
/// Get the option ID for a specific argument string.
std::optional<std::uint32_t> get_option_id(llvm::StringRef argument);
/// Get the resource directory for clang builtin headers. Computed once
/// from the current executable path using Driver::GetResourcesPath.
llvm::StringRef resource_dir();
/// Format an argument list as a human-readable string: "[arg1 arg2 ...]".
std::string print_argv(llvm::ArrayRef<const char*> args);
} // namespace clice

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
#pragma once
#include <cstdint>
#include <expected>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "command/argument_parser.h"
#include "command/search_config.h"
#include "command/toolchain_provider.h"
#include "support/format.h"
#include "support/object_pool.h"
#include "support/path_pool.h"
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/DenseMap.h"
#include "llvm/ADT/Hashing.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
struct CommandOptions {
/// Ignore unknown commands arguments.
bool ignore_unknown = true;
/// Query the compiler driver for additional information, such as system includes and target.
/// When enabled, also replaces the queried resource dir with our own (clang tools must use
/// builtin headers matching their parser version — see clangd's CommandMangler for precedent).
@@ -29,32 +29,13 @@ struct CommandOptions {
/// Set true in unittests to avoid cluttering test output.
bool suppress_logging = false;
/// The commands that you want to remove from original commands list.
/// Extra arguments to remove from the original command line.
llvm::ArrayRef<std::string> remove;
/// The commands that you want to add to original commands list.
/// Extra arguments to append to the original command line.
llvm::ArrayRef<std::string> append;
};
enum class UpdateKind : std::uint8_t {
Unchanged,
Inserted,
Deleted,
};
struct UpdateInfo {
/// The kind of update.
UpdateKind kind;
/// The updated file.
std::uint32_t path_id;
/// The compilation context of this file command, which could
/// be used to identity the same file with different compilation
/// contexts.
const void* context;
};
struct CompilationContext {
/// The working directory of compilation.
llvm::StringRef directory;
@@ -63,73 +44,167 @@ struct CompilationContext {
std::vector<const char*> arguments;
};
std::string print_argv(llvm::ArrayRef<const char*> args);
/// Shared compiler identity — driver + all semantics-affecting flags.
/// Deduped via ObjectSet so most files share one instance. This directly
/// serves as the toolchain cache key (no re-parsing needed at query time).
struct CanonicalCommand {
/// Driver path followed by semantics-affecting flags (e.g. -std=, -target, -W*).
/// All pointers are interned in StringSet and pointer-stable.
llvm::ArrayRef<const char*> arguments;
friend bool operator==(const CanonicalCommand&, const CanonicalCommand&) = default;
};
/// Per-file compilation entry = shared canonical + per-file user-content patch.
/// Parsed and classified once at CDB load time; no further parsing needed.
struct CompilationInfo {
/// Working directory (interned in StringSet, pointer-stable).
const char* directory = nullptr;
/// Shared canonical command (driver + semantic flags).
object_ptr<CanonicalCommand> canonical = {nullptr};
/// Per-file user-content options: -I, -D, -U, -include, -isystem, -iquote,
/// -idirafter. Pre-rendered as flat arg list with -I paths already absolutized.
llvm::ArrayRef<const char*> patch;
friend bool operator==(const CompilationInfo&, const CompilationInfo&) = default;
};
/// A single entry in the compilation database, stored in a flat sorted vector.
struct CompilationEntry {
/// Interned path ID for the source file (from PathPool).
std::uint32_t file;
/// Parsed compilation info (directory + canonical + patch).
object_ptr<CompilationInfo> info;
};
/// A pending toolchain query, ready to be executed (possibly in parallel).
struct ToolchainQuery {
std::string key;
std::vector<const char*> query_args;
std::string file;
std::string directory;
};
/// Result of a toolchain query, to be injected back into the cache.
struct ToolchainResult {
std::string key;
std::vector<std::string> cc1_args;
};
} // namespace clice
namespace llvm {
template <>
struct DenseMapInfo<clice::CanonicalCommand> {
using T = clice::CanonicalCommand;
inline static T getEmptyKey() {
return T{
llvm::ArrayRef<const char*>(reinterpret_cast<const char**>(~uintptr_t(0)), size_t(0))};
}
inline static T getTombstoneKey() {
return T{llvm::ArrayRef<const char*>(reinterpret_cast<const char**>(~uintptr_t(0) - 1),
size_t(0))};
}
static unsigned getHashValue(const T& cmd) {
return llvm::hash_combine_range(cmd.arguments);
}
static bool isEqual(const T& lhs, const T& rhs) {
// Sentinels have distinct data pointers but both have size 0,
// and ArrayRef equality is content-based — so we must compare
// data pointers first to keep sentinels distinguishable.
if(lhs.arguments.data() == rhs.arguments.data())
return lhs.arguments.size() == rhs.arguments.size();
if(lhs.arguments.data() == getEmptyKey().arguments.data() ||
lhs.arguments.data() == getTombstoneKey().arguments.data() ||
rhs.arguments.data() == getEmptyKey().arguments.data() ||
rhs.arguments.data() == getTombstoneKey().arguments.data())
return false;
return lhs == rhs;
}
};
template <>
struct DenseMapInfo<clice::CompilationInfo> {
using T = clice::CompilationInfo;
inline static T getEmptyKey() {
return T{llvm::DenseMapInfo<const char*>::getEmptyKey()};
}
inline static T getTombstoneKey() {
return T{llvm::DenseMapInfo<const char*>::getTombstoneKey()};
}
static unsigned getHashValue(const T& info) {
return llvm::hash_combine(info.directory,
info.canonical.ptr,
llvm::hash_combine_range(info.patch));
}
static bool isEqual(const T& lhs, const T& rhs) {
return lhs == rhs;
}
};
} // namespace llvm
namespace clice {
class CompilationDatabase {
public:
CompilationDatabase();
CompilationDatabase(const CompilationDatabase&) = delete;
CompilationDatabase(CompilationDatabase&& other);
CompilationDatabase& operator=(const CompilationDatabase&) = delete;
CompilationDatabase& operator=(CompilationDatabase&& other);
~CompilationDatabase();
CompilationDatabase(const CompilationDatabase&) = delete;
CompilationDatabase& operator=(const CompilationDatabase&) = delete;
CompilationDatabase(CompilationDatabase&&) = default;
CompilationDatabase& operator=(CompilationDatabase&&) = default;
public:
/// Read the compilation database on the give file and return the
/// incremental update infos.
std::vector<UpdateInfo> load_compile_database(llvm::StringRef file);
/// Load (or reload) the compilation database from the given file.
/// Full reload: old entries are replaced, SearchConfig cache is cleared,
/// but toolchain cache survives. Returns the number of entries loaded.
std::size_t load(llvm::StringRef path);
/// Lookup the compilation context of specific file. If the context
/// param is provided, we will return the compilation context corresponding
/// to the handle. Otherwise we just return the first one(if the file have)
/// multiple compilation contexts.
CompilationContext lookup(llvm::StringRef file,
const CommandOptions& options = {},
const void* context = nullptr);
/// TODO: list all compilation context of the file, this is useful to show
/// all contexts and let user choose one.
/// std::vector<CompilationContext> fetch_all(llvm::StringRef file);
/// Lookup the compilation contexts for a file. A file may have multiple
/// compilation commands (e.g. different build configurations); all are returned.
llvm::SmallVector<CompilationContext> lookup(llvm::StringRef file,
const CommandOptions& options = {});
/// Combined lookup + extract_search_config with internal caching.
/// Results are cached by CompilationInfo pointer, avoiding repeated
/// argument parsing across multiple calls with the same context.
SearchConfig lookup_search_config(llvm::StringRef file,
const CommandOptions& options = {},
const void* context = nullptr);
SearchConfig lookup_search_config(llvm::StringRef file, const CommandOptions& options = {});
/// Check if SearchConfig cache is populated (non-empty).
bool has_cached_configs() const;
/// Get an the option for specific argument.
static std::optional<std::uint32_t> get_option_id(llvm::StringRef argument);
/// Get the resource directory for clang builtin headers. Computed once
/// from the current executable path using Driver::GetResourcesPath.
static llvm::StringRef resource_dir();
/// Resolve a path_id (from UpdateInfo) back to the file path string.
/// Resolve a path_id back to the file path string.
llvm::StringRef resolve_path(std::uint32_t path_id);
/// Access the toolchain provider for batch pre-warming and direct queries.
ToolchainProvider& toolchain();
/// Entry for batch pre-warming: file + directory + raw compilation arguments.
struct PendingEntry {
llvm::StringRef file;
llvm::StringRef directory;
llvm::SmallVector<const char*, 32> arguments;
};
/// Resolve (file, context) pairs to PendingEntry tuples for toolchain queries.
/// Converts CDB-internal context pointers to raw (file, directory, arguments)
/// that the ToolchainProvider can consume.
std::vector<ToolchainProvider::PendingEntry>
resolve_toolchain_entries(llvm::ArrayRef<std::pair<llvm::StringRef, const void*>> files);
/// Get pending toolchain queries for a batch of compilation entries.
/// Returns queries only for cache-miss keys (deduplicated).
std::vector<ToolchainQuery> get_pending_queries(llvm::ArrayRef<PendingEntry> entries);
/// FIXME: bad interface design ...
std::vector<const char*> files();
/// Inject pre-computed toolchain results into the cache. Strings are copied
/// into the internal string pool.
void inject_results(llvm::ArrayRef<ToolchainResult> results);
/// FIXME: remove this api?
auto save_string(llvm::StringRef string) -> llvm::StringRef;
/// Check if toolchain cache has any entries.
bool has_cached_toolchain() const;
#ifdef CLICE_ENABLE_TEST
@@ -139,15 +214,72 @@ public:
void add_command(llvm::StringRef directory, llvm::StringRef file, llvm::StringRef command);
/// FIXME: remove this
/// Update commands from json file and return all updated file.
std::expected<std::vector<UpdateInfo>, std::string> load_commands(llvm::StringRef json_content,
llvm::StringRef workspace);
#endif
private:
struct Impl;
std::unique_ptr<Impl> self;
/// Find all CompilationEntry items for a file by path_id (binary search).
/// Returns a sub-range of `entries`; may be empty.
llvm::ArrayRef<CompilationEntry> find_entries(std::uint32_t path_id) const;
/// Allocate a persistent copy of a const char* array on the bump allocator.
llvm::ArrayRef<const char*> persist_args(llvm::ArrayRef<const char*> args);
/// Parse and classify a compilation command into canonical + patch.
object_ptr<CompilationInfo> save_compilation_info(llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments);
object_ptr<CompilationInfo> save_compilation_info(llvm::StringRef file,
llvm::StringRef directory,
llvm::StringRef command);
static std::uint8_t options_bits(const CommandOptions& options) {
return options.query_toolchain ? 1u : 0u;
}
struct ToolchainExtract {
std::string key;
std::vector<const char*> query_args;
};
/// Extract toolchain-relevant flags and build a cache key.
ToolchainExtract extract_toolchain_flags(llvm::StringRef file,
llvm::ArrayRef<const char*> arguments);
/// Query toolchain with caching. Returns cached cc1 args, running the
/// expensive compiler query only on cache miss.
llvm::ArrayRef<const char*> query_toolchain_cached(llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments);
/// The memory pool which holds all elements of compilation database.
/// Heap-allocated so its address is stable across moves.
std::unique_ptr<llvm::BumpPtrAllocator> allocator = std::make_unique<llvm::BumpPtrAllocator>();
/// Keep all strings (arguments, directories, etc.).
StringSet strings{allocator.get()};
/// Shared canonical commands — most files share one instance.
ObjectSet<CanonicalCommand> canonicals{allocator.get()};
/// Per-file compilation infos (canonical + patch + directory).
ObjectSet<CompilationInfo> infos{allocator.get()};
/// Intern pool for file paths → compact uint32_t IDs.
PathPool paths;
/// All compilation entries, sorted by file path_id.
/// Multiple entries for the same file are adjacent.
std::vector<CompilationEntry> entries;
/// Cache of SearchConfig keyed by (CompilationInfo*, options_bits).
using ConfigCacheKey = std::pair<const CompilationInfo*, std::uint8_t>;
llvm::DenseMap<ConfigCacheKey, SearchConfig> search_config_cache;
/// Cache of toolchain query results, keyed by canonical toolchain key.
llvm::StringMap<std::vector<const char*>> toolchain_cache;
std::unique_ptr<ArgumentParser> parser = std::make_unique<ArgumentParser>(allocator.get());
};
} // namespace clice

View File

@@ -1,146 +0,0 @@
#pragma once
#include <cassert>
#include <cstdlib>
#include <memory>
#include <ranges>
#include "command/command.h"
#include "llvm/Support/Allocator.h"
#include "clang/Driver/Driver.h"
namespace clice {
namespace {
namespace opt = llvm::opt;
namespace driver = clang::driver;
/// Checks if dash-dash (`--`) parsing is enabled. If enabled, all arguments
/// after a standalone `--` are treated as positional arguments (e.g., input files).
bool enable_dash_dash_parsing(const opt::OptTable& table);
/// Checks if grouped short options are enabled. If enabled, a short option group
/// like `-ab` is parsed as separate options `-a` and `-b`.
bool enable_grouped_short_options(const opt::OptTable& table);
/// Get the specific toolchain of given target, we mainly use it to get msvc toolchain.
const driver::ToolChain& get_toolchain(driver::Driver& driver,
const opt::ArgList& Args,
const llvm::Triple& Target);
template <auto MP1, auto MP2, auto MP3>
struct Thief {
friend bool enable_dash_dash_parsing(const opt::OptTable& table) {
return table.*MP1;
}
friend bool enable_grouped_short_options(const opt::OptTable& table) {
return table.*MP2;
}
friend const driver::ToolChain& get_toolchain(driver::Driver& driver,
const opt::ArgList& args,
const llvm::Triple& target) {
return (driver.*MP3)(args, target);
}
};
template struct Thief<&opt::OptTable::DashDashParsing,
&opt::OptTable::GroupedShortOptions,
&driver::Driver::getToolChain>;
class ArgumentParser final : public llvm::opt::ArgList {
public:
ArgumentParser(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {}
~ArgumentParser() {
/// We never use the private `Args` field, so make sure it's empty.
if(getArgs().size() != 0) {
std::abort();
}
}
const char* getArgString(unsigned index) const override {
return arguments[index];
}
unsigned getNumInputArgStrings() const override {
return arguments.size();
}
const char* MakeArgStringRef(llvm::StringRef s) const override {
auto p = allocator->Allocate<char>(s.size() + 1);
std::ranges::copy(s, p);
p[s.size()] = '\0';
return p;
}
inline static auto& option_table = clang::driver::getDriverOptTable();
void set_arguments(llvm::ArrayRef<const char*> arguments) {
if(getArgs().size() != 0) {
std::abort();
}
this->arguments = arguments;
}
std::unique_ptr<llvm::opt::Arg> parse_one(unsigned& index) {
/// Make sure we are not using
assert(!enable_dash_dash_parsing(option_table));
assert(!enable_grouped_short_options(option_table));
return option_table.ParseOneArg(*this, index);
}
void parse(llvm::ArrayRef<const char*> arguments, const auto& on_parse, const auto& on_error) {
this->arguments = arguments;
unsigned it = 0;
while(it != arguments.size()) {
llvm::StringRef s = arguments[it];
if(s.empty()) [[unlikely]] {
it += 1;
continue;
}
auto prev = it;
auto arg = parse_one(it);
assert(it > prev && "parser failed to consume argument");
if(!arg) [[unlikely]] {
assert(it >= arguments.size() && "unexpected parser error!");
assert(it - prev - 1 && "no missing arguments!");
/// FIXME: When parsing fails, the parser may have encountered unknown
/// arguments (e.g., options for a different compiler like nvcc).
/// We should allow the user to provide a custom option registry
/// (mainly for these pass-through arguments).
///
/// This would let us ignore them correctly. For example, when
/// parsing `nvcc --option-dir x.txt main.cpp`, our parser fails
/// because it discards `--option-dir` but doesn't know it also
/// consumes the next argument (`x.txt`).
///
/// With a custom registry, we could register that `--option-dir`
/// takes one argument, allowing us to skip both and continue
/// parsing from `main.cpp`.
on_error(prev, it - prev - 1);
break;
}
on_parse(std::move(arg));
}
}
private:
llvm::BumpPtrAllocator* allocator;
llvm::ArrayRef<const char*> arguments;
};
} // namespace
} // namespace clice

View File

@@ -1,11 +1,12 @@
#include "command/search_config.h"
#include "command/driver.h"
#include "command/argument_parser.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringSet.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "clang/Driver/Options.h"
namespace clice {

View File

@@ -4,6 +4,7 @@
#include <string>
#include <vector>
#include "command/argument_parser.h"
#include "eventide/reflection/enum.h"
#include "support/filesystem.h"
#include "support/logging.h"
@@ -62,19 +63,6 @@ namespace clice::toolchain {
namespace {
std::string print_argv(llvm::ArrayRef<const char*> args) {
std::string s = "[";
if(!args.empty()) {
s += args.consume_front();
for(auto arg: args) {
s += " ";
s += arg;
}
}
s += "]";
return s;
}
std::optional<std::string> execute_command(llvm::ArrayRef<const char*> arguments,
bool capture_stdout = false) {
LOG_INFO("Execute command: {}", print_argv(arguments));

View File

@@ -1,209 +0,0 @@
#include "command/toolchain_provider.h"
#include "command/driver.h"
#include "command/toolchain.h"
#include "support/filesystem.h"
#include "support/logging.h"
#include "support/object_pool.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringMap.h"
namespace clice {
using ID = clang::driver::options::ID;
struct ToolchainProvider::Impl {
llvm::BumpPtrAllocator allocator;
StringSet strings{allocator};
ArgumentParser parser{&allocator};
/// Cache of toolchain query results, keyed by canonical toolchain key.
/// The key includes all flags except user-content options (-I/-D/-U/etc.),
/// so the cc1 result reflects the correct compiler semantics (-f/-W/-O/etc.)
/// and only user-content options need to be replayed after cache lookup.
llvm::StringMap<std::vector<const char*>> toolchain_cache;
/// Options excluded from the cache key and toolchain query. These are
/// per-file user content (include paths, defines, forced includes) or
/// input files. They don't affect compiler semantics or system path
/// discovery, and are replayed into the cc1 result afterward.
static bool is_excluded_option(unsigned id) {
switch(id) {
case ID::OPT_I:
case ID::OPT_isystem:
case ID::OPT_iquote:
case ID::OPT_idirafter:
case ID::OPT_D:
case ID::OPT_U:
case ID::OPT_include:
case ID::OPT_INPUT: return true;
default: return false;
}
}
/// Extract flags for the toolchain query. All options except user-content
/// options (-I/-D/-U/etc.) are included in both the cache key and query args,
/// so the cc1 result correctly reflects compiler semantics (-f/-W/-O/etc.).
struct ToolchainExtract {
std::string key;
std::vector<const char*> query_args;
};
ToolchainExtract extract_toolchain_flags(this Impl& self,
llvm::StringRef file,
llvm::ArrayRef<const char*> arguments) {
ToolchainExtract result;
// Driver binary (first arg) — e.g. "clang++" vs "clang" affects language mode.
result.key += arguments[0];
result.key += '\0';
// File extension affects language mode (C vs C++).
result.key += path::extension(file);
result.key += '\0';
result.query_args.push_back(arguments[0]);
self.parser.parse(
llvm::ArrayRef(arguments).drop_front(),
[&](std::unique_ptr<llvm::opt::Arg> arg) {
auto id = arg->getOption().getID();
if(is_excluded_option(id)) {
return;
}
// Add option ID and all its values to the cache key.
result.key += std::to_string(id);
result.key += '\0';
for(auto value: arg->getValues()) {
result.key += value;
result.key += '\0';
}
// Render the argument back to query args, respecting the option's
// render style (joined vs separate).
switch(arg->getOption().getRenderStyle()) {
case llvm::opt::Option::RenderJoinedStyle: {
// e.g. -std=c++17, --target=x86_64-linux-gnu
llvm::SmallString<64> joined(arg->getSpelling());
if(arg->getNumValues() > 0) {
joined += arg->getValue(0);
}
result.query_args.push_back(self.strings.save(joined).data());
break;
}
case llvm::opt::Option::RenderSeparateStyle: {
// e.g. -target x86_64-linux-gnu, -isysroot /path
result.query_args.push_back(self.strings.save(arg->getSpelling()).data());
for(auto value: arg->getValues()) {
result.query_args.push_back(self.strings.save(value).data());
}
break;
}
default: {
// Flags (no value): -nostdinc, -nostdinc++
result.query_args.push_back(self.strings.save(arg->getSpelling()).data());
break;
}
}
},
[](int, int) {
// Unknown arguments are silently dropped — they can't be
// reliably parsed, so we skip them rather than corrupting
// the cache key.
});
return result;
}
/// Query toolchain with caching. Returns the cached cc1 args for the given
/// toolchain key, running the expensive query only on cache miss.
llvm::ArrayRef<const char*> query_toolchain_cached(this Impl& self,
llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments) {
auto [key, query_args] = self.extract_toolchain_flags(file, arguments);
auto it = self.toolchain_cache.find(key);
if(it != self.toolchain_cache.end()) {
return it->second;
}
LOG_WARN("Toolchain cache miss (spawning process): file={}, cache_size={}, key_len={}",
file,
self.toolchain_cache.size(),
key.size());
auto callback = [&](const char* s) -> const char* {
return self.strings.save(s).data();
};
toolchain::QueryParams params = {file, directory, query_args, callback};
auto result = toolchain::query_toolchain(params);
auto [entry, _] = self.toolchain_cache.try_emplace(std::move(key), std::move(result));
return entry->second;
}
};
ToolchainProvider::ToolchainProvider() : self(std::make_unique<Impl>()) {}
ToolchainProvider::~ToolchainProvider() = default;
ToolchainProvider::ToolchainProvider(ToolchainProvider&&) noexcept = default;
ToolchainProvider& ToolchainProvider::operator=(ToolchainProvider&&) noexcept = default;
llvm::ArrayRef<const char*> ToolchainProvider::query_cached(llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments) {
return self->query_toolchain_cached(file, directory, arguments);
}
std::vector<ToolchainQuery>
ToolchainProvider::get_pending_queries(llvm::ArrayRef<PendingEntry> entries) {
llvm::StringMap<bool> seen_keys;
std::vector<ToolchainQuery> queries;
for(auto& entry: entries) {
if(entry.arguments.empty()) {
continue;
}
auto [key, query_args] = self->extract_toolchain_flags(entry.file, entry.arguments);
// Skip if already cached or already queued.
if(self->toolchain_cache.count(key) || !seen_keys.try_emplace(key, true).second) {
continue;
}
LOG_DEBUG("Pre-warm: new toolchain key (len={}) for file={}", key.size(), entry.file);
queries.push_back(
{std::move(key), std::move(query_args), entry.file.str(), entry.directory.str()});
}
LOG_INFO("Pre-warm: {} unique keys from {} entries, {} queries needed",
seen_keys.size(),
entries.size(),
queries.size());
return queries;
}
void ToolchainProvider::inject_results(llvm::ArrayRef<ToolchainResult> results) {
for(auto& result: results) {
if(self->toolchain_cache.count(result.key)) {
continue;
}
std::vector<const char*> saved;
saved.reserve(result.cc1_args.size());
for(auto& arg: result.cc1_args) {
saved.push_back(self->strings.save(arg).data());
}
self->toolchain_cache.try_emplace(result.key, std::move(saved));
}
}
bool ToolchainProvider::has_cached_entries() const {
return !self->toolchain_cache.empty();
}
} // namespace clice

View File

@@ -1,75 +0,0 @@
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
namespace clice {
/// A pending toolchain query, ready to be executed (possibly in parallel).
struct ToolchainQuery {
std::string key;
std::vector<const char*> query_args;
std::string file;
std::string directory;
};
/// Result of a toolchain query, to be injected back into the cache.
struct ToolchainResult {
std::string key;
std::vector<std::string> cc1_args;
};
/// Manages toolchain queries and caching, separated from CompilationDatabase.
///
/// Given compilation arguments, this component:
/// 1. Extracts toolchain-relevant flags (driver, target, sysroot, stdlib, etc.)
/// 2. Builds a canonical cache key from those flags
/// 3. Queries the compiler driver for system include paths (expensive: spawns a process)
/// 4. Caches results so identical toolchain configurations share one query
///
/// Designed to be pluggable: CompilationDatabase holds a ToolchainProvider by
/// composition and delegates all toolchain operations to it.
class ToolchainProvider {
public:
ToolchainProvider();
~ToolchainProvider();
ToolchainProvider(ToolchainProvider&&) noexcept;
ToolchainProvider& operator=(ToolchainProvider&&) noexcept;
/// Query toolchain with caching. Returns cached cc1 args for the given
/// compilation arguments, running the expensive compiler query only on
/// cache miss. The returned ArrayRef is valid for the provider's lifetime.
llvm::ArrayRef<const char*> query_cached(llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments);
/// Entry for batch pre-warming: file + directory + raw compilation arguments.
struct PendingEntry {
llvm::StringRef file;
llvm::StringRef directory;
llvm::SmallVector<const char*, 32> arguments;
};
/// Get pending queries for a batch of compilation entries.
/// Returns queries only for cache-miss keys (deduplicated).
std::vector<ToolchainQuery> get_pending_queries(llvm::ArrayRef<PendingEntry> entries);
/// Inject pre-computed results into the cache. Strings are copied into
/// the provider's internal string pool.
void inject_results(llvm::ArrayRef<ToolchainResult> results);
/// Check if the cache has any entries.
bool has_cached_entries() const;
private:
struct Impl;
std::unique_ptr<Impl> self;
};
} // namespace clice

View File

@@ -192,14 +192,15 @@ et::task<> MasterServer::load_workspace() {
co_return;
}
auto updates = cdb.load_compile_database(cdb_path);
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, updates.size());
auto count = cdb.load(cdb_path);
LOG_INFO("Loaded CDB from {} with {} entries", cdb_path, count);
}
void MasterServer::fill_compile_args(llvm::StringRef path,
std::string& directory,
std::vector<std::string>& arguments) {
auto ctx = cdb.lookup(path, {.query_toolchain = true});
auto results = cdb.lookup(path, {.query_toolchain = true});
auto& ctx = results.front();
directory = ctx.directory.str();
arguments.clear();
for(auto* arg: ctx.arguments) {

View File

@@ -19,14 +19,14 @@ class StringSet {
public:
using ID = std::uint32_t;
explicit StringSet(llvm::BumpPtrAllocator& allocator) : allocator(allocator) {
explicit StringSet(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {
strings.emplace_back();
}
StringSet(const StringSet&) = delete;
StringSet(StringSet&&) = delete;
StringSet& operator=(const StringSet&) = delete;
StringSet& operator=(StringSet&&) = delete;
StringSet(StringSet&&) = default;
StringSet& operator=(StringSet&&) = default;
~StringSet() = default;
ID get(llvm::StringRef s) {
@@ -40,7 +40,7 @@ public:
}
const auto size = s.size();
auto* p = allocator.Allocate<char>(size + 1);
auto* p = allocator->Allocate<char>(size + 1);
std::memcpy(p, s.data(), size);
p[size] = '\0';
@@ -60,7 +60,7 @@ public:
}
private:
llvm::BumpPtrAllocator& allocator;
llvm::BumpPtrAllocator* allocator;
std::vector<llvm::StringRef> strings;
llvm::DenseMap<llvm::StringRef, ID> cache;
};
@@ -98,12 +98,12 @@ class ObjectSet {
public:
using ID = std::uint32_t;
explicit ObjectSet(llvm::BumpPtrAllocator& allocator) : allocator(allocator) {}
explicit ObjectSet(llvm::BumpPtrAllocator* allocator) : allocator(allocator) {}
ObjectSet(const ObjectSet&) = delete;
ObjectSet(ObjectSet&&) = delete;
ObjectSet& operator=(const ObjectSet&) = delete;
ObjectSet& operator=(ObjectSet&&) = delete;
ObjectSet(ObjectSet&&) = default;
ObjectSet& operator=(ObjectSet&&) = default;
~ObjectSet() {
if constexpr(!std::is_trivially_destructible_v<T>) {
@@ -137,7 +137,7 @@ public:
it->second = id;
objects[id] = o;
} else {
auto* p = allocator.Allocate<T>(1);
auto* p = allocator->Allocate<T>(1);
p = new (p) T(object);
it->first = object_ptr<T>{p};
@@ -170,7 +170,7 @@ public:
}
private:
llvm::BumpPtrAllocator& allocator;
llvm::BumpPtrAllocator* allocator;
std::vector<object_ptr<T>> objects;
llvm::SmallVector<std::pair<object_ptr<T>, ID>> removed;
llvm::DenseMap<object_ptr<T>, ID> cache;

View File

@@ -0,0 +1,91 @@
#include "test/test.h"
#include "command/argument_parser.h"
#include "clang/Driver/Options.h"
namespace clice::testing {
namespace {
TEST_SUITE(ArgumentParser) {
using option = clang::driver::options::ID;
void expect_id(llvm::StringRef command, option opt) {
auto id = get_option_id(command);
ASSERT_TRUE(id.has_value());
ASSERT_EQ(*id, int(opt));
}
TEST_CASE(GetOptionID) {
/// GroupClass
expect_id("-g", option::OPT_g_Flag);
/// InputClass
expect_id("main.cpp", option::OPT_INPUT);
/// UnknownClass
expect_id("--clice", option::OPT_UNKNOWN);
/// FlagClass
expect_id("-v", option::OPT_v);
expect_id("-c", option::OPT_c);
expect_id("-pedantic", option::OPT_pedantic);
expect_id("--pedantic", option::OPT_pedantic);
/// JoinedClass
expect_id("-Wno-unused-variable", option::OPT_W_Joined);
expect_id("-W*", option::OPT_W_Joined);
expect_id("-W", option::OPT_W_Joined);
/// ValuesClass
/// SeparateClass
expect_id("-Xclang", option::OPT_Xclang);
/// expect_id(GET_ID("-Xclang -ast-dump") , option::OPT_Xclang);
/// RemainingArgsClass
/// RemainingArgsJoinedClass
/// CommaJoinedClass
expect_id("-Wl,", option::OPT_Wl_COMMA);
/// MultiArgClass
/// JoinedOrSeparateClass
expect_id("-o", option::OPT_o);
expect_id("-omain.o", option::OPT_o);
expect_id("-I", option::OPT_I);
expect_id("--include-directory=", option::OPT_I);
expect_id("-x", option::OPT_x);
expect_id("--language=", option::OPT_x);
/// JoinedAndSeparateClass
};
TEST_CASE(PrintArgv) {
/// Normal args.
std::vector<const char*> args = {"clang++", "-std=c++20", "main.cpp"};
ASSERT_EQ(print_argv(args), "clang++ -std=c++20 main.cpp");
/// Empty args.
std::vector<const char*> empty = {};
ASSERT_EQ(print_argv(empty), "");
/// Args with spaces get quoted.
std::vector<const char*> spaced = {"clang++", "-DFOO=hello world"};
auto result = print_argv(spaced);
EXPECT_TRUE(llvm::StringRef(result).contains("\""));
/// Args with backslash get quoted/escaped.
std::vector<const char*> escaped = {"clang++", "-DPATH=C:\\foo"};
auto result2 = print_argv(escaped);
EXPECT_TRUE(llvm::StringRef(result2).contains("\""));
};
}; // TEST_SUITE(ArgumentParser)
} // namespace
} // namespace clice::testing

View File

@@ -1,102 +1,33 @@
#include "test/test.h"
#include "command/argument_parser.h"
#include "command/command.h"
#include "compile/compilation.h"
#include "support/filesystem.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Program.h"
#include "clang/Driver/Driver.h"
#include "llvm/Support/raw_ostream.h"
namespace clice::testing {
namespace {
std::string print_argv(llvm::ArrayRef<const char*> args) {
std::string buf;
llvm::raw_string_ostream os(buf);
bool Sep = false;
for(llvm::StringRef arg: args) {
if(Sep)
os << ' ';
Sep = true;
if(llvm::all_of(arg, llvm::isPrint) &&
arg.find_first_of(" \t\n\"\\") == llvm::StringRef::npos) {
os << arg;
continue;
}
os << '"';
os.write_escaped(arg, /*UseHexEscapes=*/true);
os << '"';
}
return std::move(os.str());
using namespace std::literals;
CommandOptions quiet_options() {
CommandOptions options;
options.suppress_logging = true;
return options;
}
#define EXPECT_CONTAINS(haystack, needle) EXPECT_TRUE(llvm::StringRef(haystack).contains(needle))
#define EXPECT_NOT_CONTAINS(haystack, needle) \
EXPECT_FALSE(llvm::StringRef(haystack).contains(needle))
TEST_SUITE(Command) {
using option = clang::driver::options::ID;
void expect_id(llvm::StringRef command, option opt) {
auto id = CompilationDatabase::get_option_id(command);
ASSERT_TRUE(id.has_value());
ASSERT_EQ(*id, int(opt));
}
TEST_CASE(GetOptionID) {
/// GroupClass
expect_id("-g", option::OPT_g_Flag);
/// InputClass
expect_id("main.cpp", option::OPT_INPUT);
/// UnknownClass
expect_id("--clice", option::OPT_UNKNOWN);
/// FlagClass
expect_id("-v", option::OPT_v);
expect_id("-c", option::OPT_c);
expect_id("-pedantic", option::OPT_pedantic);
expect_id("--pedantic", option::OPT_pedantic);
/// JoinedClass
expect_id("-Wno-unused-variable", option::OPT_W_Joined);
expect_id("-W*", option::OPT_W_Joined);
expect_id("-W", option::OPT_W_Joined);
/// ValuesClass
/// SeparateClass
expect_id("-Xclang", option::OPT_Xclang);
/// expect_id(GET_ID("-Xclang -ast-dump") , option::OPT_Xclang);
/// RemainingArgsClass
/// RemainingArgsJoinedClass
/// CommaJoinedClass
expect_id("-Wl,", option::OPT_Wl_COMMA);
/// MultiArgClass
/// JoinedOrSeparateClass
expect_id("-o", option::OPT_o);
expect_id("-omain.o", option::OPT_o);
expect_id("-I", option::OPT_I);
expect_id("--include-directory=", option::OPT_I);
expect_id("-x", option::OPT_x);
expect_id("--language=", option::OPT_x);
/// JoinedAndSeparateClass
};
void expect_strip(llvm::StringRef argv, llvm::StringRef result) {
CompilationDatabase database;
llvm::StringRef file = "main.cpp";
database.add_command("fake/", file, argv);
CommandOptions options;
options.suppress_logging = true;
ASSERT_EQ(result, print_argv(database.lookup(file, options).arguments));
ASSERT_EQ(result, print_argv(database.lookup(file, quiet_options()).front().arguments));
};
TEST_CASE(DefaultFilters) {
@@ -122,16 +53,13 @@ TEST_CASE(DefaultFilters) {
};
TEST_CASE(Reuse) {
using namespace std::literals;
CompilationDatabase database;
database.add_command("fake", "test.cpp", "clang++ -std=c++23 test.cpp"sv);
database.add_command("fake", "test2.cpp", "clang++ -std=c++23 test2.cpp"sv);
CommandOptions options;
options.suppress_logging = true;
auto command1 = database.lookup("test.cpp", options).arguments;
auto command2 = database.lookup("test2.cpp", options).arguments;
auto options = quiet_options();
auto command1 = database.lookup("test.cpp", options).front().arguments;
auto command2 = database.lookup("test2.cpp", options).front().arguments;
ASSERT_EQ(command1.size(), 3U);
ASSERT_EQ(command2.size(), 3U);
@@ -158,42 +86,459 @@ TEST_CASE(RemoveAppend) {
CompilationDatabase database;
database.add_command("/fake", "main.cpp", args);
CommandOptions options;
auto options = quiet_options();
llvm::SmallVector<std::string> remove;
llvm::SmallVector<std::string> append;
remove = {"-DA"};
options.remove = remove;
auto result = database.lookup("main.cpp", options).arguments;
auto result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ -D B=0 main.cpp");
remove = {"-D", "A"};
options.remove = remove;
result = database.lookup("main.cpp", options).arguments;
result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ -D B=0 main.cpp");
remove = {"-DA", "-D", "B=0"};
options.remove = remove;
result = database.lookup("main.cpp", options).arguments;
result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ main.cpp");
remove = {"-D*"};
options.remove = remove;
result = database.lookup("main.cpp", options).arguments;
result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ main.cpp");
remove = {"-D", "*"};
options.remove = remove;
result = database.lookup("main.cpp", options).arguments;
result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ main.cpp");
append = {"-D", "C"};
options.append = append;
result = database.lookup("main.cpp", options).arguments;
result = database.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(print_argv(result), "clang++ -D C main.cpp");
};
TEST_CASE(DefaultFallback) {
/// Lookup for a file not in the CDB should synthesize a default command.
CompilationDatabase database;
/// C++ files get "clang++ -std=c++20 <file>".
auto cpp_results = database.lookup("unknown.cpp");
ASSERT_EQ(cpp_results.size(), 1U);
auto& cpp_ctx = cpp_results.front();
ASSERT_EQ(cpp_ctx.arguments.size(), 3U);
ASSERT_EQ(cpp_ctx.arguments[0], "clang++"sv);
ASSERT_EQ(cpp_ctx.arguments[1], "-std=c++20"sv);
ASSERT_EQ(cpp_ctx.arguments[2], "unknown.cpp"sv);
/// .hpp files also get C++ default.
auto hpp_results = database.lookup("header.hpp");
ASSERT_EQ(hpp_results.front().arguments.size(), 3U);
ASSERT_EQ(hpp_results.front().arguments[0], "clang++"sv);
/// .cc files also get C++ default.
auto cc_results = database.lookup("file.cc");
ASSERT_EQ(cc_results.front().arguments.size(), 3U);
ASSERT_EQ(cc_results.front().arguments[0], "clang++"sv);
/// C files get "clang <file>".
auto c_results = database.lookup("unknown.c");
ASSERT_EQ(c_results.size(), 1U);
auto& c_ctx = c_results.front();
ASSERT_EQ(c_ctx.arguments.size(), 2U);
ASSERT_EQ(c_ctx.arguments[0], "clang"sv);
ASSERT_EQ(c_ctx.arguments[1], "unknown.c"sv);
/// Other extensions also get plain clang.
auto h_results = database.lookup("foo.h");
ASSERT_EQ(h_results.front().arguments.size(), 2U);
ASSERT_EQ(h_results.front().arguments[0], "clang"sv);
};
TEST_CASE(MultiCommand) {
/// A file can have multiple compilation commands (e.g. different configs).
CompilationDatabase database;
database.add_command("fake", "main.cpp", "clang++ -std=c++17 main.cpp"sv);
database.add_command("fake", "main.cpp", "clang++ -std=c++20 main.cpp"sv);
database.add_command("fake", "other.cpp", "clang++ -std=c++23 other.cpp"sv);
auto options = quiet_options();
auto results = database.lookup("main.cpp", options);
ASSERT_EQ(results.size(), 2U);
/// Both commands are present (order depends on insert position).
bool has_17 = false, has_20 = false;
for(auto& ctx: results) {
auto argv = print_argv(ctx.arguments);
if(llvm::StringRef(argv).contains("-std=c++17"))
has_17 = true;
if(llvm::StringRef(argv).contains("-std=c++20"))
has_20 = true;
}
EXPECT_TRUE(has_17);
EXPECT_TRUE(has_20);
/// other.cpp has only one.
auto other = database.lookup("other.cpp", options);
ASSERT_EQ(other.size(), 1U);
};
TEST_CASE(CodegenFilter) {
/// Codegen-only options should be stripped from the canonical command.
CompilationDatabase database;
database.add_command(
"fake",
"main.cpp",
"clang++ -std=c++20 -fPIC -fno-omit-frame-pointer -fstack-protector-strong " "-fdata-sections -ffunction-sections -flto -fcolor-diagnostics -g main.cpp"sv);
auto result = database.lookup("main.cpp", quiet_options()).front().arguments;
auto argv = print_argv(result);
/// -std=c++20 must survive (semantic).
EXPECT_CONTAINS(argv, "-std=c++20");
/// All codegen flags must be stripped.
EXPECT_NOT_CONTAINS(argv, "-fPIC");
EXPECT_NOT_CONTAINS(argv, "-fno-omit-frame-pointer");
EXPECT_NOT_CONTAINS(argv, "-fstack-protector");
EXPECT_NOT_CONTAINS(argv, "-fdata-sections");
EXPECT_NOT_CONTAINS(argv, "-ffunction-sections");
EXPECT_NOT_CONTAINS(argv, "-flto");
EXPECT_NOT_CONTAINS(argv, "-fcolor-diagnostics");
EXPECT_NOT_CONTAINS(argv, "-g");
};
TEST_CASE(DependencyScanFilter) {
/// Dependency scan options should be stripped.
CompilationDatabase database;
database.add_command("fake",
"main.cpp",
"clang++ -std=c++20 -MD -MF main.d -MT main.o main.cpp"sv);
auto result = database.lookup("main.cpp", quiet_options()).front().arguments;
auto argv = print_argv(result);
EXPECT_CONTAINS(argv, "-std=c++20");
EXPECT_NOT_CONTAINS(argv, "-MD");
EXPECT_NOT_CONTAINS(argv, "-MF");
EXPECT_NOT_CONTAINS(argv, "-MT");
EXPECT_NOT_CONTAINS(argv, "main.d");
};
TEST_CASE(ModuleFilter) {
/// Module-related options should be stripped.
expect_strip("clang++ -std=c++20 -fmodule-file=mod.pcm main.cpp",
"clang++ -std=c++20 main.cpp");
expect_strip("clang++ -std=c++20 -fprebuilt-module-path=/tmp main.cpp",
"clang++ -std=c++20 main.cpp");
};
TEST_CASE(UserContentClassification) {
/// -D, -U, -include go to per-file patch; -std=, -W go to canonical.
/// Files with different -D but same -std/-W share canonical.
CompilationDatabase database;
database.add_command("fake", "a.cpp", "clang++ -std=c++20 -Wall -DA=1 -DFOO a.cpp"sv);
database.add_command("fake", "b.cpp", "clang++ -std=c++20 -Wall -DB=2 b.cpp"sv);
auto options = quiet_options();
auto a_argv = print_argv(database.lookup("a.cpp", options).front().arguments);
auto b_argv = print_argv(database.lookup("b.cpp", options).front().arguments);
/// Both must contain canonical flags.
EXPECT_CONTAINS(a_argv, "-std=c++20");
EXPECT_CONTAINS(a_argv, "-Wall");
EXPECT_CONTAINS(b_argv, "-std=c++20");
EXPECT_CONTAINS(b_argv, "-Wall");
/// a.cpp has its own defines.
EXPECT_CONTAINS(a_argv, "-D");
EXPECT_CONTAINS(a_argv, "A=1");
EXPECT_CONTAINS(a_argv, "FOO");
/// b.cpp has its own defines.
EXPECT_CONTAINS(b_argv, "-D");
EXPECT_CONTAINS(b_argv, "B=2");
/// Cross check: a.cpp should not have B=2, b.cpp should not have A=1.
EXPECT_NOT_CONTAINS(a_argv, "B=2");
EXPECT_NOT_CONTAINS(b_argv, "A=1");
};
TEST_CASE(IncludePathAbsolutize) {
/// Relative include paths should be absolutized against the directory.
CompilationDatabase database;
database.add_command("/project/build",
"main.cpp",
"clang++ -Iinclude -isystem sys/inc -iquote ../src main.cpp"sv);
auto result = database.lookup("main.cpp", quiet_options()).front().arguments;
/// Check each argument individually with separator normalization
/// (print_argv escapes backslashes, breaking convert_to_slash on Windows).
auto has_path = [](llvm::ArrayRef<const char*> args, llvm::StringRef needle) {
for(auto* arg: args) {
if(path::convert_to_slash(arg).find(needle.str()) != std::string::npos)
return true;
}
return false;
};
/// Relative paths must be resolved against /project/build.
EXPECT_TRUE(has_path(result, "/project/build/include"));
EXPECT_TRUE(has_path(result, "/project/build/sys/inc"));
/// ../src relative to /project/build → /project/src (or /project/build/../src)
EXPECT_TRUE(has_path(result, "/project/"));
/// Absolute paths should be kept as-is.
CompilationDatabase database2;
database2.add_command("/project/build", "main.cpp", "clang++ -I/usr/include main.cpp"sv);
auto result2 = database2.lookup("main.cpp", quiet_options()).front().arguments;
EXPECT_TRUE(has_path(result2, "/usr/include"));
};
TEST_CASE(SemanticOptionsPreserved) {
/// Flags that affect semantics must survive.
expect_strip("clang++ -std=c++20 -fno-exceptions -fno-rtti -pedantic main.cpp",
"clang++ -std=c++20 -fno-exceptions -fno-rtti -pedantic main.cpp");
expect_strip("clang++ -std=c++20 -Wall -Werror main.cpp",
"clang++ -std=c++20 -Wall -Werror main.cpp");
};
TEST_CASE(LookupSearchConfig) {
CompilationDatabase database;
database.add_command(
"/project",
"main.cpp",
"clang++ -std=c++20 -I/usr/include -isystem /usr/local/include main.cpp"sv);
ASSERT_FALSE(database.has_cached_configs());
auto options = quiet_options();
auto config = database.lookup_search_config("main.cpp", options);
/// Should have search dirs from the command.
EXPECT_FALSE(config.dirs.empty());
/// Second call should hit cache.
EXPECT_TRUE(database.has_cached_configs());
auto config2 = database.lookup_search_config("main.cpp", options);
ASSERT_EQ(config.dirs.size(), config2.dirs.size());
};
TEST_CASE(ResolvePath) {
CompilationDatabase database;
database.add_command("fake", "test/main.cpp", "clang++ test/main.cpp"sv);
/// After add_command, lookup should work and resolve_path via the file in arguments.
auto result = database.lookup("test/main.cpp", quiet_options()).front().arguments;
/// The last argument is the file, resolved from PathPool.
ASSERT_EQ(result.back(), "test/main.cpp"sv);
};
TEST_CASE(MoveSemantics) {
CompilationDatabase db1;
db1.add_command("fake", "main.cpp", "clang++ -std=c++23 main.cpp"sv);
/// Move construct.
CompilationDatabase db2 = std::move(db1);
auto options = quiet_options();
auto result = db2.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(result.size(), 3U);
ASSERT_EQ(result[1], "-std=c++23"sv);
/// Move assign.
CompilationDatabase db3;
db3 = std::move(db2);
result = db3.lookup("main.cpp", options).front().arguments;
ASSERT_EQ(result.size(), 3U);
ASSERT_EQ(result[1], "-std=c++23"sv);
};
/// Write JSON to a temp file, load into a CDB, remove the file.
/// Returns the number of entries loaded.
std::size_t load_json(CompilationDatabase& database, llvm::StringRef json) {
auto path = fs::createTemporaryFile("cdb", "json");
if(!path)
return 0;
{
std::error_code ec;
llvm::raw_fd_ostream out(*path, ec);
if(ec)
return 0;
out << json;
}
auto count = database.load(*path);
llvm::sys::fs::remove(*path);
return count;
}
TEST_CASE(LoadMixedFormats) {
/// "arguments" array and "command" string can coexist in the same CDB.
/// Use relative file paths so that the test works on both Linux and Windows
/// (paths like "/src/a.cpp" are not absolute on Windows — no drive letter).
CompilationDatabase database;
auto count = load_json(database, R"([
{"directory": "/build", "file": "a.cpp",
"arguments": ["clang++", "-std=c++20", "a.cpp"]},
{"directory": "/build", "file": "b.cpp",
"command": "clang++ -std=c++23 b.cpp"}
])");
ASSERT_EQ(count, 2U);
auto options = quiet_options();
auto a = database.lookup(path::join("/build", "a.cpp"), options);
ASSERT_EQ(a.size(), 1U);
EXPECT_CONTAINS(print_argv(a.front().arguments), "-std=c++20");
auto b = database.lookup(path::join("/build", "b.cpp"), options);
ASSERT_EQ(b.size(), 1U);
EXPECT_CONTAINS(print_argv(b.front().arguments), "-std=c++23");
};
TEST_CASE(LoadErrorRecovery) {
/// Bad entries should be skipped; good entries still load.
CompilationDatabase database;
auto count = load_json(database, R"([
{"file": "no_dir.cpp",
"arguments": ["clang++", "no_dir.cpp"]},
{"directory": "/build",
"arguments": ["clang++", "no_file.cpp"]},
{"directory": "/build", "file": "no_args.cpp"},
{"directory": "/build", "file": "good.cpp",
"arguments": ["clang++", "-std=c++20", "good.cpp"]},
42,
{"directory": "/build", "file": "also_good.cpp",
"command": "clang++ -Wall also_good.cpp"}
])");
/// Only the two valid entries should survive.
ASSERT_EQ(count, 2U);
auto options = quiet_options();
auto good = database.lookup(path::join("/build", "good.cpp"), options);
ASSERT_EQ(good.size(), 1U);
EXPECT_CONTAINS(print_argv(good.front().arguments), "-std=c++20");
auto also = database.lookup(path::join("/build", "also_good.cpp"), options);
ASSERT_EQ(also.size(), 1U);
EXPECT_CONTAINS(print_argv(also.front().arguments), "-Wall");
};
TEST_CASE(LoadEmptyCommand) {
/// Whitespace-only or empty "command" should not crash.
CompilationDatabase database;
auto count = load_json(database, R"([
{"directory": "/build", "file": "empty.cpp", "command": ""},
{"directory": "/build", "file": "spaces.cpp", "command": " "},
{"directory": "/build", "file": "ok.cpp",
"command": "clang++ -std=c++20 ok.cpp"}
])");
/// Only the valid entry survives.
ASSERT_EQ(count, 1U);
auto ok = database.lookup(path::join("/build", "ok.cpp"), quiet_options());
ASSERT_EQ(ok.size(), 1U);
EXPECT_CONTAINS(print_argv(ok.front().arguments), "-std=c++20");
};
TEST_CASE(LoadReload) {
/// Second load() replaces all entries from the first.
CompilationDatabase database;
auto file_a = path::join("/build", "a.cpp");
auto file_b = path::join("/build", "b.cpp");
load_json(database, R"([
{"directory": "/build", "file": "a.cpp",
"arguments": ["clang++", "-std=c++17", "a.cpp"]}
])");
auto options = quiet_options();
auto a = database.lookup(file_a, options);
ASSERT_EQ(a.size(), 1U);
EXPECT_CONTAINS(print_argv(a.front().arguments), "-std=c++17");
/// Reload with different content.
auto count = load_json(database, R"([
{"directory": "/build", "file": "b.cpp",
"arguments": ["clang++", "-std=c++23", "b.cpp"]}
])");
ASSERT_EQ(count, 1U);
/// Old entry gone (falls back to default).
auto a2 = database.lookup(file_a, options);
ASSERT_EQ(a2.size(), 1U);
EXPECT_NOT_CONTAINS(print_argv(a2.front().arguments), "-std=c++17");
/// New entry present.
auto b = database.lookup(file_b, options);
ASSERT_EQ(b.size(), 1U);
EXPECT_CONTAINS(print_argv(b.front().arguments), "-std=c++23");
};
TEST_CASE(LoadCommandQuoting) {
/// "command" string with spaces in paths and quoted defines.
CompilationDatabase database;
auto count = load_json(database, R"([
{"directory": "/build", "file": "main.cpp",
"command": "clang++ -std=c++20 \"-DMSG=hello world\" -I\"/path with spaces\" main.cpp"}
])");
ASSERT_EQ(count, 1U);
auto result = database.lookup(path::join("/build", "main.cpp"), quiet_options());
ASSERT_EQ(result.size(), 1U);
auto argv = print_argv(result.front().arguments);
/// The define and include path should be present after shell tokenization.
EXPECT_CONTAINS(argv, "hello world");
EXPECT_CONTAINS(argv, "/path with spaces");
};
TEST_CASE(LoadRelativePath) {
/// load() should resolve relative file paths against directory.
CompilationDatabase database;
auto count = load_json(database, R"([
{"directory": "/project/build", "file": "src/main.cpp",
"arguments": ["clang++", "-std=c++20", "src/main.cpp"]},
{"directory": "/other/build", "file": "src/main.cpp",
"arguments": ["clang++", "-std=c++17", "src/main.cpp"]}
])");
ASSERT_EQ(count, 2U);
auto options = quiet_options();
/// Lookup by the resolved absolute path (use path::join for correct separator).
auto results = database.lookup(path::join("/project/build", "src/main.cpp"), options);
ASSERT_EQ(results.size(), 1U);
EXPECT_CONTAINS(print_argv(results.front().arguments), "-std=c++20");
auto results2 = database.lookup(path::join("/other/build", "src/main.cpp"), options);
ASSERT_EQ(results2.size(), 1U);
EXPECT_CONTAINS(print_argv(results2.front().arguments), "-std=c++17");
/// Relative path lookup should not match (different path_id).
auto results3 = database.lookup("src/main.cpp", options);
ASSERT_EQ(results3.size(), 1U);
/// Falls back to default command since no match.
EXPECT_CONTAINS(print_argv(results3.front().arguments), "clang");
};
TEST_CASE(Module) {
// TODO: revisit module command handling.
}
@@ -201,134 +546,32 @@ TEST_CASE(Module) {
TEST_CASE(ResourceDir) {
// When query_toolchain is enabled, resource dir is injected automatically.
CompilationDatabase database;
using namespace std::literals;
database.add_command("/fake", "main.cpp", "clang++ -std=c++23 test.cpp"sv);
// Without query_toolchain, no resource dir injection.
auto args_no_tc = database.lookup("main.cpp").arguments;
auto args_no_tc = database.lookup("main.cpp").front().arguments;
ASSERT_EQ(args_no_tc.size(), 3U);
ASSERT_EQ(args_no_tc[0], "clang++"sv);
ASSERT_EQ(args_no_tc[1], "-std=c++23"sv);
ASSERT_EQ(args_no_tc[2], "main.cpp"sv);
// With query_toolchain, resource dir is present in the result.
auto args_tc = database.lookup("main.cpp", {.query_toolchain = true}).arguments;
auto args_tc = database.lookup("main.cpp", {.query_toolchain = true}).front().arguments;
bool has_resource_dir = false;
for(size_t i = 0; i + 1 < args_tc.size(); ++i) {
if(args_tc[i] == llvm::StringRef("-resource-dir")) {
EXPECT_EQ(llvm::StringRef(args_tc[i + 1]), CompilationDatabase::resource_dir());
if(args_tc[i] == "-resource-dir"sv) {
EXPECT_EQ(llvm::StringRef(args_tc[i + 1]), resource_dir());
has_resource_dir = true;
break;
}
}
EXPECT_TRUE(has_resource_dir);
};
void expect_load(llvm::StringRef content,
llvm::StringRef workspace,
llvm::StringRef file,
llvm::StringRef directory,
llvm::ArrayRef<const char*> arguments) {
CompilationDatabase database;
auto loaded = database.load_commands(content, workspace);
ASSERT_TRUE(loaded.has_value());
CommandOptions options;
options.suppress_logging = true;
auto info = database.lookup(file, options);
ASSERT_EQ(info.directory, directory);
ASSERT_EQ(info.arguments.size(), arguments.size());
for(size_t i = 0; i < arguments.size(); i++) {
llvm::StringRef arg = info.arguments[i];
llvm::StringRef expect_arg = arguments[i];
ASSERT_EQ(arg, expect_arg);
if(resource_dir().empty()) {
EXPECT_FALSE(has_resource_dir);
} else {
EXPECT_TRUE(has_resource_dir);
}
};
/// TODO: add windows path testcase
// skip_unless(Linux || macOS) / test("LoadAbsoluteUnixStyle") = [expect_load] {
// constexpr const char* cmake = R"([
// {
// "directory": "/home/developer/clice/build",
// "command": "/usr/bin/c++ -I/home/developer/clice/include
// -I/home/developer/clice/build/_deps/libuv-src/include -isystem
// /home/developer/clice/build/_deps/tomlplusplus-src/include -std=gnu++23 -fno-rtti
// -fno-exceptions -Wno-deprecated-declarations -Wno-undefined-inline -O3 -o
// CMakeFiles/clice-core.dir/src/Driver/clice.cpp.o -c
// /home/developer/clice/src/Driver/clice.cpp", "file":
// "/home/developer/clice/src/Driver/clice.cpp", "output":
// "CMakeFiles/clice-core.dir/src/Driver/clice.cpp.o"
// }
// ])";
//
// expect_load(cmake,
// "/home/developer/clice",
// "/home/developer/clice/src/Driver/clice.cpp",
// "/home/developer/clice/build",
// {
// "/usr/bin/c++",
// "-I",
// "/home/developer/clice/include",
// "-I",
// "/home/developer/clice/build/_deps/libuv-src/include",
// "-isystem",
// "/home/developer/clice/build/_deps/tomlplusplus-src/include",
// "-std=gnu++23",
// "-fno-rtti",
// "-fno-exceptions",
// "-Wno-deprecated-declarations",
// "-Wno-undefined-inline",
// "-O3",
// "/home/developer/clice/src/Driver/clice.cpp",
// });
// };
// skip_unless(Linux || macOS) / test("LoadRelativeUnixStyle") = [expect_load] {
// constexpr const char* xmake = R"([
// {
// "directory": "/home/developer/clice",
// "arguments": ["/usr/bin/clang", "-c", "-Qunused-arguments", "-m64", "-g", "-O0",
// "-std=c++23", "-Iinclude", "-I/home/developer/clice/include", "-fno-exceptions",
// "-fno-cxx-exceptions", "-isystem",
// "/home/developer/.xmake/packages/l/libuv/v1.51.0/3ca1562e6c5d485f9ccafec8e0c50b6f/include",
// "-isystem",
// "/home/developer/.xmake/packages/t/toml++/v3.4.0/bde7344d843e41928b1d325fe55450e0/include",
// "-fsanitize=address", "-fno-rtti", "-o",
// "build/.objs/clice/linux/x86_64/debug/src/Driver/clice.cc.o", "src/Driver/clice.cc"],
// "file": "src/Driver/clice.cc"
// }
// ])";
//
// expect_load(
// xmake,
// "/home/developer/clice",
// "/home/developer/clice/src/Driver/clice.cc",
// "/home/developer/clice",
// {
// "/usr/bin/clang",
// "-Qunused-arguments",
// "-m64",
// "-g",
// "-O0",
// "-std=c++23",
// // parameter "-Iinclude" in CDB, should be convert to absolute path
// "-I",
// "/home/developer/clice/include",
// "-I",
// "/home/developer/clice/include",
// "-fno-exceptions",
// "-fno-cxx-exceptions",
// "-isystem",
// "/home/developer/.xmake/packages/l/libuv/v1.51.0/3ca1562e6c5d485f9ccafec8e0c50b6f/include",
// "-isystem",
// "/home/developer/.xmake/packages/t/toml++/v3.4.0/bde7344d843e41928b1d325fe55450e0/include",
// "-fsanitize=address",
// "-fno-rtti",
// "/home/developer/clice/src/Driver/clice.cc",
// });
//};
}; // TEST_SUITE(Command)
} // namespace

View File

@@ -1,5 +1,5 @@
#include "test/test.h"
#include "command/toolchain_provider.h"
#include "command/command.h"
namespace clice::testing {
@@ -8,25 +8,25 @@ namespace {
TEST_SUITE(ToolchainProvider) {
TEST_CASE(InitiallyEmpty) {
ToolchainProvider provider;
EXPECT_FALSE(provider.has_cached_entries());
CompilationDatabase cdb;
EXPECT_FALSE(cdb.has_cached_toolchain());
}
TEST_CASE(InjectResultsPopulatesCache) {
ToolchainProvider provider;
CompilationDatabase cdb;
std::vector<ToolchainResult> results;
results.push_back({
"key1",
{"-cc1", "-triple", "x86_64-linux-gnu"}
});
provider.inject_results(results);
cdb.inject_results(results);
EXPECT_TRUE(provider.has_cached_entries());
EXPECT_TRUE(cdb.has_cached_toolchain());
}
TEST_CASE(InjectResultsSkipsDuplicateKeys) {
ToolchainProvider provider;
CompilationDatabase cdb;
std::vector<ToolchainResult> results;
results.push_back({
@@ -37,141 +37,140 @@ TEST_CASE(InjectResultsSkipsDuplicateKeys) {
"key1",
{"-cc1", "-triple", "aarch64"}
});
provider.inject_results(results);
cdb.inject_results(results);
// After injection, query_cached with same key should return the first result.
// We verify indirectly: inject twice, cache should still work.
EXPECT_TRUE(provider.has_cached_entries());
// After injection, cache should still work.
EXPECT_TRUE(cdb.has_cached_toolchain());
}
TEST_CASE(GetPendingQueriesReturnsUncachedOnly) {
ToolchainProvider provider;
CompilationDatabase cdb;
// Two entries with same flags but different user-content options.
// They share the same cache key, so only one query is needed.
ToolchainProvider::PendingEntry entry1;
CompilationDatabase::PendingEntry entry1;
entry1.file = "a.cpp";
entry1.directory = "/tmp";
entry1.arguments = {"clang++", "-std=c++17", "-DFOO", "a.cpp"};
ToolchainProvider::PendingEntry entry2;
CompilationDatabase::PendingEntry entry2;
entry2.file = "b.cpp";
entry2.directory = "/tmp";
entry2.arguments = {"clang++", "-std=c++17", "-DBAR", "b.cpp"};
auto queries = provider.get_pending_queries({entry1, entry2});
auto queries = cdb.get_pending_queries({entry1, entry2});
// Same driver, same extension, same non-content flags → one query.
EXPECT_EQ(queries.size(), 1u);
}
TEST_CASE(GetPendingQueriesDeduplicatesSameKey) {
ToolchainProvider provider;
CompilationDatabase cdb;
// Three entries with same driver and same flags (only -I/-D differ,
// which are user-content options excluded from the cache key).
ToolchainProvider::PendingEntry entry1;
CompilationDatabase::PendingEntry entry1;
entry1.file = "x.cpp";
entry1.directory = "/project";
entry1.arguments = {"clang++", "-Wall", "-O2", "-DFOO=1", "-I/inc/a", "x.cpp"};
ToolchainProvider::PendingEntry entry2;
CompilationDatabase::PendingEntry entry2;
entry2.file = "y.cpp";
entry2.directory = "/project";
entry2.arguments = {"clang++", "-Wall", "-O2", "-DBAR=2", "-I/inc/b", "y.cpp"};
ToolchainProvider::PendingEntry entry3;
CompilationDatabase::PendingEntry entry3;
entry3.file = "z.cpp";
entry3.directory = "/project";
entry3.arguments = {"clang++", "-Wall", "-O2", "-Uhello", "z.cpp"};
auto queries = provider.get_pending_queries({entry1, entry2, entry3});
auto queries = cdb.get_pending_queries({entry1, entry2, entry3});
// Same driver, same extension, same non-content flags → same key.
EXPECT_EQ(queries.size(), 1u);
}
TEST_CASE(GetPendingQueriesDifferentDrivers) {
ToolchainProvider provider;
CompilationDatabase cdb;
ToolchainProvider::PendingEntry entry1;
CompilationDatabase::PendingEntry entry1;
entry1.file = "a.cpp";
entry1.directory = "/tmp";
entry1.arguments = {"clang++", "a.cpp"};
ToolchainProvider::PendingEntry entry2;
CompilationDatabase::PendingEntry entry2;
entry2.file = "b.cpp";
entry2.directory = "/tmp";
entry2.arguments = {"g++", "b.cpp"};
auto queries = provider.get_pending_queries({entry1, entry2});
auto queries = cdb.get_pending_queries({entry1, entry2});
// Different drivers → different keys → two queries.
EXPECT_EQ(queries.size(), 2u);
}
TEST_CASE(GetPendingQueriesDifferentTargets) {
ToolchainProvider provider;
CompilationDatabase cdb;
ToolchainProvider::PendingEntry entry1;
CompilationDatabase::PendingEntry entry1;
entry1.file = "a.cpp";
entry1.directory = "/tmp";
entry1.arguments = {"clang++", "--target=x86_64-linux-gnu", "a.cpp"};
ToolchainProvider::PendingEntry entry2;
CompilationDatabase::PendingEntry entry2;
entry2.file = "b.cpp";
entry2.directory = "/tmp";
entry2.arguments = {"clang++", "--target=aarch64-linux-gnu", "b.cpp"};
auto queries = provider.get_pending_queries({entry1, entry2});
auto queries = cdb.get_pending_queries({entry1, entry2});
// Different targets → different keys → two queries.
EXPECT_EQ(queries.size(), 2u);
}
TEST_CASE(GetPendingQueriesDifferentLanguageMode) {
ToolchainProvider provider;
CompilationDatabase cdb;
// clang foo.h (default: c-header) vs clang -x c++ foo.h (c++)
// produce different system include paths, so they must have different keys.
ToolchainProvider::PendingEntry entry1;
CompilationDatabase::PendingEntry entry1;
entry1.file = "foo.h";
entry1.directory = "/tmp";
entry1.arguments = {"clang", "foo.h"};
ToolchainProvider::PendingEntry entry2;
CompilationDatabase::PendingEntry entry2;
entry2.file = "foo.h";
entry2.directory = "/tmp";
entry2.arguments = {"clang", "-x", "c++", "foo.h"};
auto queries = provider.get_pending_queries({entry1, entry2});
auto queries = cdb.get_pending_queries({entry1, entry2});
// -x c++ changes language mode → different keys → two queries.
EXPECT_EQ(queries.size(), 2u);
}
TEST_CASE(GetPendingQueriesSkipsEmptyArgs) {
ToolchainProvider provider;
CompilationDatabase cdb;
ToolchainProvider::PendingEntry empty;
CompilationDatabase::PendingEntry empty;
empty.file = "empty.cpp";
empty.directory = "/tmp";
// arguments is empty
ToolchainProvider::PendingEntry valid;
CompilationDatabase::PendingEntry valid;
valid.file = "valid.cpp";
valid.directory = "/tmp";
valid.arguments = {"clang++", "valid.cpp"};
auto queries = provider.get_pending_queries({empty, valid});
auto queries = cdb.get_pending_queries({empty, valid});
EXPECT_EQ(queries.size(), 1u);
}
TEST_CASE(InjectThenGetPendingSkipsCached) {
ToolchainProvider provider;
CompilationDatabase cdb;
// First, get pending queries to learn what key is generated.
ToolchainProvider::PendingEntry entry;
CompilationDatabase::PendingEntry entry;
entry.file = "test.cpp";
entry.directory = "/tmp";
entry.arguments = {"clang++", "test.cpp"};
auto queries = provider.get_pending_queries({entry});
auto queries = cdb.get_pending_queries({entry});
ASSERT_EQ(queries.size(), 1u);
// Inject a result for that key.
@@ -180,23 +179,13 @@ TEST_CASE(InjectThenGetPendingSkipsCached) {
queries[0].key,
{"-cc1", "-triple", "x86_64-linux-gnu"}
});
provider.inject_results(results);
cdb.inject_results(results);
// Now the same entry should produce no pending queries.
auto queries2 = provider.get_pending_queries({entry});
auto queries2 = cdb.get_pending_queries({entry});
EXPECT_EQ(queries2.size(), 0u);
}
TEST_CASE(MoveConstruction) {
ToolchainProvider provider;
std::vector<ToolchainResult> results;
results.push_back({"key1", {"-cc1"}});
provider.inject_results(results);
ToolchainProvider moved(std::move(provider));
EXPECT_TRUE(moved.has_cached_entries());
}
}; // TEST_SUITE(ToolchainProvider)
} // namespace

View File

@@ -1,4 +1,5 @@
#include "test/test.h"
#include "command/argument_parser.h"
#include "command/command.h"
#include "command/toolchain.h"
#include "compile/compilation.h"
@@ -6,7 +7,6 @@
#include "llvm/Support/Allocator.h"
#include "llvm/Support/StringSaver.h"
#include "clang/Driver/Driver.h"
namespace clice::testing {
namespace {
@@ -52,10 +52,8 @@ TEST_CASE(GCC, {.skip = !(CIEnvironment && (Windows || Linux))}) {
llvm::BumpPtrAllocator a;
llvm::StringSaver s(a);
auto arguments = toolchain::query_toolchain({
.arguments = {"g++",
"-std=c++23", "-resource-dir",
CompilationDatabase::resource_dir().data(),
"-xc++", file->c_str()},
.arguments =
{"g++", "-std=c++23", "-resource-dir", resource_dir().data(), "-xc++", file->c_str()},
.callback = [&](const char* str) { return s.save(str).data(); }
});
@@ -93,7 +91,7 @@ TEST_CASE(Clang, {.skip = !CIEnvironment}) {
auto arguments = toolchain::query_toolchain({
.arguments = {"clang++",
"-std=c++23", "-resource-dir",
CompilationDatabase::resource_dir().data(),
resource_dir().data(),
"-xc++", file->c_str()},
.callback = [&](const char* str) { return s.save(str).data(); }
});

View File

@@ -94,12 +94,8 @@ TEST_CASE(BuildPCHRequest) {
worker::BuildPCHParams params;
params.file = hdr.path;
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(CompilationDatabase::resource_dir()),
"-x",
"c++-header",
hdr.path};
params.arguments =
{"clang++", "-resource-dir", std::string(resource_dir()), "-x", "c++-header", hdr.path};
params.content = "#pragma once\nint pch_global = 42;\n";
auto result = co_await w.peer->send_request(params);
@@ -157,7 +153,7 @@ TEST_CASE(BuildPCMRequest) {
params.directory = "/tmp";
params.arguments = {"clang++",
"-resource-dir",
std::string(CompilationDatabase::resource_dir()),
std::string(resource_dir()),
"-std=c++20",
"--precompile",
src.path};

View File

@@ -10,6 +10,7 @@
#include <unistd.h>
#endif
#include "command/argument_parser.h"
#include "command/command.h"
#include "eventide/async/async.h"
#include "eventide/ipc/peer.h"
@@ -36,7 +37,7 @@ namespace et = eventide;
/// Resolve path to the clice binary for spawning workers.
inline std::string clice_binary() {
auto res_dir = CompilationDatabase::resource_dir();
auto res_dir = resource_dir();
// res_dir is <build>/lib/clang/...
// clice binary is at <build>/bin/clice
auto build_dir = llvm::sys::path::parent_path(
@@ -74,12 +75,8 @@ struct TempFile {
/// Build compile arguments for a source file, including -resource-dir.
inline std::vector<std::string> make_args(const std::string& file_path,
const std::string& extra = "") {
std::vector<std::string> args = {"clang++",
"-fsyntax-only",
"-resource-dir",
std::string(CompilationDatabase::resource_dir()),
"-c",
file_path};
std::vector<std::string> args =
{"clang++", "-fsyntax-only", "-resource-dir", std::string(resource_dir()), "-c", file_path};
if(!extra.empty()) {
args.insert(args.begin() + 1, extra);
}

View File

@@ -17,7 +17,7 @@ void Tester::prepare(llvm::StringRef standard) {
options.query_toolchain = true;
options.suppress_logging = true;
params.arguments = database.lookup(src_path, options).arguments;
params.arguments = database.lookup(src_path, options).front().arguments;
for(auto& [file, source]: sources.all_files) {
if(file == src_path) {
@@ -54,7 +54,7 @@ bool Tester::compile_with_pch(llvm::StringRef standard) {
options.query_toolchain = true;
options.suppress_logging = true;
params.arguments = database.lookup(src_path, options).arguments;
params.arguments = database.lookup(src_path, options).front().arguments;
auto pch_path = fs::createTemporaryFile("clice", "pch");
if(!pch_path) {