refactor: simplify CompilationDatabase, extract ArgumentParser, remove pimpl (#371)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
224
src/command/argument_parser.cpp
Normal file
224
src/command/argument_parser.cpp
Normal 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
|
||||
114
src/command/argument_parser.h
Normal file
114
src/command/argument_parser.h
Normal 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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
91
tests/unit/command/argument_parser_tests.cpp
Normal file
91
tests/unit/command/argument_parser_tests.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
});
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user