refactor(command): split CompilationContext into ResolvedFlags → CompileCommand → to_argv() (#408)

## Summary

- Replace flat `CompilationContext { directory, arguments }` with a
three-layer abstraction: `ResolvedFlags` (file-independent flags) →
`CompileCommand` (+ source file) → `to_argv()` (full argv on demand)
- `ResolvedFlags.flags` never contains source file path or
`-main-file-name`, making it directly usable as a clean cache key input
(e.g. PCH sharing across files with identical preambles)
- `to_argv()` handles `-main-file-name` insertion for cc1 mode
automatically — consumers no longer need to search/replace in the
argument list
- Eliminates the pollute-then-clean anti-pattern in `lookup()` and the
manual source-file replacement in `fill_header_context_args()`

## Test plan

- [x] `pixi run format` — no changes
- [x] `pixi run unit-test` — 481 passed
- [x] `pixi run integration-test` — 113 passed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Unified compile-command handling across the server and tools for more
consistent argument and flag behavior (driver vs frontend modes).
* **New Features**
* Added an LRU-backed in-memory cache to improve performance and
eviction control.
* **Chores**
* Added an option to control injection of resource-directory flags
(enabled by default).
* **Tests**
* Updated unit and integration tests to adopt the new command
representation and verify cache behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ykiko
2026-04-08 22:18:25 +08:00
committed by GitHub
parent 9c9e6b0bcb
commit 2bbdf6c02b
9 changed files with 239 additions and 178 deletions

View File

@@ -23,6 +23,44 @@ namespace ranges = std::ranges;
} // namespace
std::vector<const char*> CompileCommand::to_argv() const {
std::vector<const char*> argv;
argv.reserve(resolved.flags.size() + 4);
if(resolved.is_cc1 && source_file) {
// cc1 mode requires TWO file-related arguments (both are needed):
// 1. -main-file-name <basename> — used by clang for diagnostics/debug info
// 2. <source_file> at the end — the actual input file path
// These are NOT duplicates: (1) is just the basename, (2) is the full path.
for(std::size_t i = 0; i < resolved.flags.size(); ++i) {
argv.push_back(resolved.flags[i]);
if(resolved.flags[i] == llvm::StringRef("-cc1")) {
argv.push_back("-main-file-name");
// path::filename returns a suffix of source_file (a pointer into
// the same buffer), so .data() is null-terminated because source_file is.
argv.push_back(path::filename(source_file).data());
}
}
} else {
argv.insert(argv.end(), resolved.flags.begin(), resolved.flags.end());
}
if(source_file) {
argv.push_back(source_file);
}
return argv;
}
std::vector<std::string> CompileCommand::to_string_argv() const {
auto argv = to_argv();
std::vector<std::string> result;
result.reserve(argv.size());
for(auto* arg: argv) {
result.emplace_back(arg);
}
return result;
}
CompilationDatabase::CompilationDatabase() = default;
CompilationDatabase::~CompilationDatabase() = default;
@@ -329,8 +367,8 @@ std::size_t CompilationDatabase::load(llvm::StringRef path) {
return entries.size();
}
llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRef file,
const CommandOptions& options) {
llvm::SmallVector<CompileCommand> CompilationDatabase::lookup(llvm::StringRef file,
const CommandOptions& options) {
auto path_id = paths.intern(file);
auto matched = find_entries(path_id);
@@ -338,17 +376,18 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
render_arg_to([&](llvm::StringRef s) { out.push_back(strings.save(s).data()); }, arg);
};
/// Build one CompilationContext from a single CompilationInfo.
auto build_context = [&](object_ptr<CompilationInfo> info) -> CompilationContext {
/// Build one CompileCommand from a single CompilationInfo.
auto build_command = [&](object_ptr<CompilationInfo> info) -> CompileCommand {
llvm::StringRef directory = info->directory;
std::vector<const char*> arguments;
std::vector<const char*> flags;
bool is_cc1 = false;
auto append_arg = [&](llvm::StringRef s) {
arguments.emplace_back(strings.save(s).data());
flags.emplace_back(strings.save(s).data());
};
auto append_args = [&](llvm::ArrayRef<const char*> args) {
arguments.insert(arguments.end(), args.begin(), args.end());
flags.insert(flags.end(), args.begin(), args.end());
};
if(options.query_toolchain) {
@@ -361,23 +400,20 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
append_args(info->canonical->arguments);
append_args(info->patch);
} else {
arguments.assign(cached.begin(), cached.end());
// TODO: add an assertion that the last arg is the temp source
// file (e.g., contains "query-toolchain") to guard against
// future changes in clang cc1 argument ordering.
arguments.pop_back(); // remove temp source file
flags.assign(cached.begin(), cached.end());
flags.pop_back(); // remove temp source file
// Replace resource dir if needed.
if(!resource_dir().empty()) {
llvm::StringRef old_resource_dir;
for(std::size_t i = 0; i + 1 < arguments.size(); ++i) {
if(arguments[i] == llvm::StringRef("-resource-dir")) {
old_resource_dir = arguments[i + 1];
for(std::size_t i = 0; i + 1 < flags.size(); ++i) {
if(flags[i] == llvm::StringRef("-resource-dir")) {
old_resource_dir = flags[i + 1];
break;
}
}
if(!old_resource_dir.empty() && old_resource_dir != resource_dir()) {
for(auto& arg: arguments) {
for(auto& arg: flags) {
llvm::StringRef s(arg);
if(s.starts_with(old_resource_dir)) {
auto replaced =
@@ -390,39 +426,42 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
append_args(info->patch);
// Fix -main-file-name to match the actual file.
bool next_main_file = false;
for(auto& arg: arguments) {
if(arg == llvm::StringRef("-main-file-name")) {
next_main_file = true;
// Strip -main-file-name and its value from flags (to_argv() will
// re-inject it with the correct basename when is_cc1 is set).
std::vector<const char*> cleaned;
cleaned.reserve(flags.size());
for(std::size_t i = 0; i < flags.size(); ++i) {
if(flags[i] == llvm::StringRef("-main-file-name") && i + 1 < flags.size()) {
++i; // skip the value
continue;
}
if(next_main_file) {
arg = strings.save(path::filename(file)).data();
next_main_file = false;
}
cleaned.push_back(flags[i]);
}
}
flags = std::move(cleaned);
// Inject our resource dir if not already present.
if(!resource_dir().empty()) {
bool has_resource_dir = false;
for(auto& arg: arguments) {
if(arg == llvm::StringRef("-resource-dir")) {
has_resource_dir = true;
break;
}
}
if(!has_resource_dir) {
append_arg("-resource-dir");
append_arg(resource_dir());
}
// Detect cc1 mode (search rather than assuming index).
is_cc1 = ranges::contains(flags, llvm::StringRef("-cc1"));
}
} else {
append_args(info->canonical->arguments);
append_args(info->patch);
}
// Inject our resource dir if not already present.
if(options.inject_resource_dir && !resource_dir().empty()) {
bool has_resource_dir = false;
for(auto& arg: flags) {
if(arg == llvm::StringRef("-resource-dir")) {
has_resource_dir = true;
break;
}
}
if(!has_resource_dir) {
append_arg("-resource-dir");
append_arg(resource_dir());
}
}
// Apply remove filter.
if(!options.remove.empty()) {
using Arg = std::unique_ptr<llvm::opt::Arg>;
@@ -440,12 +479,12 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
};
std::ranges::sort(remove_args, {}, get_id);
auto saved_args = std::move(arguments);
arguments.clear();
arguments.push_back(saved_args.front());
auto saved_flags = std::move(flags);
flags.clear();
flags.push_back(saved_flags.front());
parser->parse(
llvm::ArrayRef(saved_args).drop_front(),
llvm::ArrayRef(saved_flags).drop_front(),
[&](Arg arg) {
auto id = arg->getOption().getID();
auto range = std::ranges::equal_range(remove_args, id, {}, get_id);
@@ -461,7 +500,7 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
return;
}
}
render_arg(arguments, *arg);
render_arg(flags, *arg);
},
[](int, int) {});
}
@@ -470,26 +509,34 @@ llvm::SmallVector<CompilationContext> CompilationDatabase::lookup(llvm::StringRe
append_arg(arg);
}
arguments.emplace_back(paths.resolve(path_id).data());
return CompilationContext(directory, std::move(arguments));
return CompileCommand{
ResolvedFlags{directory, std::move(flags), is_cc1},
paths.resolve(path_id).data()
};
};
llvm::SmallVector<CompilationContext> results;
llvm::SmallVector<CompileCommand> results;
if(!matched.empty()) {
for(auto& entry: matched) {
results.push_back(build_context(entry.info));
results.push_back(build_command(entry.info));
}
} else {
// No matching entry — synthesize a default command.
std::vector<const char*> arguments;
std::vector<const char*> flags;
if(file.ends_with(".cpp") || file.ends_with(".hpp") || file.ends_with(".cc")) {
arguments = {"clang++", "-std=c++20"};
flags = {"clang++", "-std=c++20"};
} else {
arguments = {"clang"};
flags = {"clang"};
}
arguments.emplace_back(paths.resolve(path_id).data());
results.push_back(CompilationContext({}, std::move(arguments)));
if(options.inject_resource_dir && !resource_dir().empty()) {
flags.push_back(strings.save("-resource-dir").data());
flags.push_back(strings.save(resource_dir()).data());
}
results.push_back(CompileCommand{
ResolvedFlags{{}, std::move(flags), false},
paths.resolve(path_id).data()
});
}
return results;
@@ -513,8 +560,8 @@ SearchConfig CompilationDatabase::lookup_search_config(llvm::StringRef file,
}
auto results = lookup(file, options);
auto& ctx = results.front();
auto config = extract_search_config(ctx.arguments, ctx.directory);
auto& cmd = results.front();
auto config = extract_search_config(cmd.to_argv(), cmd.resolved.directory);
if(cacheable) {
auto key = ConfigCacheKey{matched.front().info.ptr, options_bits(options)};