Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2f011e9
refactor: extract ArgumentParser, remove pimpl from CompilationDataba…
16bit-ykiko Mar 26, 2026
3e0f388
fix: address review feedback for PR #371
16bit-ykiko Mar 26, 2026
9fd6d77
refactor: make CompilationDatabase and ToolchainProvider movable
16bit-ykiko Mar 26, 2026
cc02dc2
refactor: simplify CompilationDatabase with sorted vector, simdjson, …
16bit-ykiko Mar 26, 2026
06fb809
refactor: move option classification and resource_dir to argument_parser
16bit-ykiko Mar 26, 2026
11d19cc
refactor: replace StringID with const char* in CompilationDatabase st…
16bit-ykiko Mar 26, 2026
98aa021
refactor: move print_argv, multi-result lookup, cleanup CommandOptions
16bit-ykiko Mar 26, 2026
a431d3c
refactor: merge print_argv implementations, use quoting version
16bit-ykiko Mar 26, 2026
180d4ba
fix: address CodeRabbit review — empty command guard, sentinel fix, t…
16bit-ykiko Mar 26, 2026
957c4cf
fix: resolve relative file paths in CDB against directory before inte…
16bit-ykiko Mar 26, 2026
4288076
test: add RelativeFilePath test for CDB load path resolution
16bit-ykiko Mar 26, 2026
73a2c1c
style: apply clang-format
16bit-ykiko Mar 26, 2026
51d88e6
test: add CDB load tests (mixed formats, error recovery, reload, quot…
16bit-ykiko Mar 27, 2026
b65469a
fix: normalize path separators in tests for Windows CI
16bit-ykiko Mar 27, 2026
62e3614
fix: Windows CI — use relative paths in load tests, fix argument esca…
16bit-ykiko Mar 27, 2026
5d53e02
refactor: merge ToolchainProvider into CompilationDatabase
16bit-ykiko Mar 27, 2026
288021f
refactor: split ArgumentParser tests into own file
16bit-ykiko Mar 27, 2026
44d0324
refactor: clean up command_tests boilerplate
16bit-ykiko Mar 27, 2026
3caf872
fix: add assertions for empty arguments in save_compilation_info
16bit-ykiko Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
224 changes: 224 additions & 0 deletions src/command/argument_parser.cpp
Original file line number Diff line number Diff line change
@@ -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 changes: 114 additions & 0 deletions src/command/argument_parser.h
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading