Skip to content

feat: add SearchConfig, ToolchainProvider, PathPool - #367

Closed
16bit-ykiko wants to merge 2 commits into
mainfrom
feat/search-config
Closed

16bit-ykiko wants to merge 2 commits into
mainfrom
feat/search-config

Conversation

@16bit-ykiko

@16bit-ykiko 16bit-ykiko commented Mar 25, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add SearchConfig extraction pipeline modeling clang's four-segment include directory layout (Quoted → Angled → System → After)
  • Add ToolchainProvider cache for clang++ -### query results, keyed by toolchain-relevant flags
  • Add PathPool intern table for lightweight file-path identity (uint32_t IDs)
  • Add shared TempDir RAII test helper with cross-platform path handling
  • Extend command.h/cpp with lookup_search_config, resolve_toolchain_entries, toolchain(), has_cached_configs, resolve_path APIs
  • Add 7 ExtractSearchConfig unit tests

Test plan

  • All 207 unit tests pass (206 pass, 1 pre-existing InlayHint.Special failure)
  • All 7 new ExtractSearchConfig tests pass
  • Local build verified on Linux

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added header search configuration extraction with internal caching for improved performance.
    • Added toolchain query caching to optimize expensive compiler lookups.
    • Extended compilation database with direct search configuration and toolchain access APIs.
  • Refactor

    • Centralized resource directory handling within the compilation database.
    • Simplified initialization logic by removing explicit resource directory setup.
  • Tests

    • Updated test suite for new search configuration and toolchain functionality.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR introduces a new toolchain-provider abstraction with search-config extraction and caching to replace the global resource-directory mechanism. It removes the filesystem module's init_resource_dir function, migrates resource-dir computation to a static method on CompilationDatabase, and refactors core command-lookup workflows to query and cache toolchain results.

Changes

Cohort / File(s) Summary
Build Configuration
CMakeLists.txt
Added two new translation units (search_config.cpp, toolchain_provider.cpp) to the clice-core static library target.
Toolchain Provider Infrastructure
src/command/toolchain_provider.h, src/command/toolchain_provider.cpp
Introduced ToolchainProvider class for caching expensive toolchain queries keyed by canonical driver options. Provides query_cached() for cache-backed queries, get_pending_queries() to extract cache-miss work, inject_results() for bulk result insertion, and has_cached_entries() for cache state inspection.
Search Configuration
src/command/search_config.h, src/command/search_config.cpp
Added SearchConfig and SearchDir types to represent partitioned header search directories (Quoted, Angled, System, After segments). Implemented extract_search_config() to parse compiler arguments, resolve/normalize paths, and deduplicate directory entries while maintaining segment boundaries.
Path Pooling Utility
src/support/path_pool.h
Added PathPool struct for interning file paths with deduplication via StringMap cache and compact ID-based lookups for stable storage and reference handling.
Core Command Database Refactoring
src/command/command.h, src/command/command.cpp
Removed CommandOptions::resource_dir flag. Extended CompilationDatabase with static resource_dir() method, instance methods lookup_search_config() with internal caching and has_cached_configs(), and toolchain-related APIs (toolchain(), resolve_toolchain_entries(), resolve_path()). Rewrote lookup path to use ToolchainProvider::query_cached(), inject toolchain results, and re-inject user include flags while normalizing paths.
Resource Directory Migration
src/support/filesystem.h, src/clice.cc, src/server/master_server.cpp
Removed global fs::resource_dir variable and fs::init_resource_dir() function. Eliminated resource-dir initialization call in main(). Updated MasterServer::fill_compile_args to omit resource_dir flag from lookup options.
Test Infrastructure
tests/unit/test/temp_dir.h
Added TempDir RAII utility for managing temporary directory lifecycles in tests, with path generation and file/directory creation helpers.
Test Suite Additions
tests/unit/command/search_config_tests.cpp, tests/unit/command/toolchain_provider_tests.cpp
Added comprehensive unit tests for extract_search_config() (directory grouping, deduplication, prefix handling) and ToolchainProvider (cache population, query deduplication, pending entry filtering).
Test Updates
tests/unit/command/command_tests.cpp, tests/unit/command/toolchain_tests.cpp, tests/unit/server/stateless_worker_tests.cpp, tests/unit/server/worker_test_helpers.h, tests/unit/test/tester.cpp, tests/unit/unit_tests.cc, tests/unit/feature/inlay_hint_tests.cpp
Updated test invocations to use CompilationDatabase::resource_dir() instead of fs::resource_dir; removed CommandOptions::resource_dir = true assignments; removed init_resource_dir() initialization; marked Special inlay hint test as skipped.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A toolchain caches with delight,
No global state, just pooled bytes bright,
Search directories grouped with care,
Resource paths now static, fair!
Complex flows made clean and right. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the three primary new public entities introduced by the changeset: SearchConfig, ToolchainProvider, and PathPool.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/search-config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Base automatically changed from refactor/move-command to main March 25, 2026 15:55
Introduce the search-config extraction pipeline that models clang's
four-segment include directory layout (Quoted → Angled → System → After),
a ToolchainProvider cache for `clang++ -###` queries, and a PathPool
intern table for lightweight file-path identity.  Also adds a shared
TempDir test helper and ExtractSearchConfig unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/command/command.cpp (1)

755-818: ⚠️ Potential issue | 🔴 Critical

Preserve the original frontend flags on toolchain cache hits.

Lines 764-818 rebuild arguments from the minimal -### probe result and only re-add -I/-isystem/-iquote. That drops every other user-facing flag omitted from the probe (-D/-U/-W, warning suppressions, and the caller-supplied -resource-dir on non-Windows) from lookup(). src/server/master_server.cpp:1-50 consumes ctx.arguments directly after lookup(..., {.resource_dir = true, .query_toolchain = true}), so warm-cache lookups can produce a materially different command line than cold-cache lookups.

Please merge the discovered toolchain entries into user_args instead of replacing the full argv, or re-inject all non-toolchain arguments before returning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 755 - 818, The current lookup path
(involving toolchain.query_cached, the local variables arguments and user_args,
and the parser.parse callback that only re-injects OPT_I/OPT_isystem/OPT_iquote)
replaces the caller argv with the minimal cc1 probe result and thus drops
user-facing flags (e.g. -D/-U/-W and caller -resource-dir); instead merge the
cached toolchain args into the original user_args: after obtaining cached from
self->toolchain.query_cached, iterate cached and for each token that is a
toolchain-provided argument (system include paths, driver flags, etc.) insert or
overwrite only those entries into user_args while preserving any original
non-toolchain flags (macros, warning flags, user -resource-dir); finally assign
arguments = std::move(user_args) (or reconstruct arguments by combining cached
toolchain entries + original non-toolchain entries) and keep the existing
Windows resource-dir replacement and include-path injection logic (use
functions/variables: toolchain.query_cached, cached, user_args, arguments,
append_arg, self->parser.parse to locate where to change).
🧹 Nitpick comments (1)
tests/unit/test/temp_dir.h (1)

59-67: Consider logging or propagating errors from touch().

The touch() method silently ignores file creation errors. If llvm::raw_fd_ostream fails to open the file, tests may proceed with missing files, leading to confusing failures downstream.

💡 Optional: Add error logging for test debugging
     void touch(llvm::StringRef relative, llvm::StringRef content = "") {
         auto p = path(relative);
         llvm::sys::fs::create_directories(llvm::sys::path::parent_path(p));
         std::error_code ec;
         llvm::raw_fd_ostream out(p, ec);
-        if(!ec) {
+        if(ec) {
+            llvm::errs() << "TempDir::touch failed for " << p << ": " << ec.message() << "\n";
+            return;
+        }
-            out << content;
-        }
+        out << content;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/test/temp_dir.h` around lines 59 - 67, The touch() helper
currently swallows file creation errors; update it to detect and propagate or
log the failure: after creating llvm::raw_fd_ostream(p, ec) check if ec is set
and then either throw a std::runtime_error (including p and ec.message()) or
emit a clear diagnostic via llvm::errs()/llvm::report_fatal_error so tests fail
fast; reference the touch() function, the path() helper, llvm::raw_fd_ostream
and the std::error_code ec when implementing the check and message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/command/command.cpp`:
- Around line 155-158: The search_config_cache currently keys solely by
CompilationInfo* (search_config_cache) but lookup() also depends on
CommandOptions (query_toolchain, resource_dir, append, remove) and on
ToolchainProvider state changed by inject_results(), so update the cache key to
include those discriminating inputs (e.g., form a composite key struct
containing const CompilationInfo*, CommandOptions values that affect argv, and a
toolchain cache/version token from ToolchainProvider) or skip cached entries
when lookup() could mutate argv beyond the canonical CompilationInfo;
specifically modify lookup() and where search_config_cache is accessed/inserted
(symbols: search_config_cache, CompilationInfo, SearchConfig, lookup(),
CommandOptions.{query_toolchain,resource_dir,append,remove}, inject_results(),
ToolchainProvider) to ensure you either broaden the key or invalidate/bypass the
cache when toolchain warming or differing CommandOptions are in effect.

In `@src/command/toolchain_provider.cpp`:
- Around line 137-145: The code currently caches whatever
toolchain::query_toolchain(params) returns into self.toolchain_cache via
try_emplace(key, result), which persists empty vectors and prevents retries;
change the logic in the function that builds params and calls query_toolchain so
that after obtaining auto result you check if result is empty and only insert
into self.toolchain_cache when non-empty (i.e., skip try_emplace when
result.empty()), returning the fresh result directly in the empty case so
callers can retry later; reference symbols: toolchain::query_toolchain,
toolchain::QueryParams, result, key, self.toolchain_cache, try_emplace.

In `@src/support/path_pool.h`:
- Around line 14-36: PathPool currently hands out 0 as a valid ID and has no
bounds checking in resolve; change PathPool so ID 0 is reserved as a sentinel by
inserting an initial empty entry into paths (and ensure cache indices start from
1) before any intern() calls, adjust intern(llvm::StringRef path) to assign IDs
starting at 1 (e.g., use paths.size() after the sentinel) and keep existing
null-terminated allocation logic, and update resolve(std::uint32_t id) to check
that id != 0 and id < paths.size() returning an empty llvm::StringRef (or
assert/handle error) for invalid ids to avoid crashes.

---

Outside diff comments:
In `@src/command/command.cpp`:
- Around line 755-818: The current lookup path (involving
toolchain.query_cached, the local variables arguments and user_args, and the
parser.parse callback that only re-injects OPT_I/OPT_isystem/OPT_iquote)
replaces the caller argv with the minimal cc1 probe result and thus drops
user-facing flags (e.g. -D/-U/-W and caller -resource-dir); instead merge the
cached toolchain args into the original user_args: after obtaining cached from
self->toolchain.query_cached, iterate cached and for each token that is a
toolchain-provided argument (system include paths, driver flags, etc.) insert or
overwrite only those entries into user_args while preserving any original
non-toolchain flags (macros, warning flags, user -resource-dir); finally assign
arguments = std::move(user_args) (or reconstruct arguments by combining cached
toolchain entries + original non-toolchain entries) and keep the existing
Windows resource-dir replacement and include-path injection logic (use
functions/variables: toolchain.query_cached, cached, user_args, arguments,
append_arg, self->parser.parse to locate where to change).

---

Nitpick comments:
In `@tests/unit/test/temp_dir.h`:
- Around line 59-67: The touch() helper currently swallows file creation errors;
update it to detect and propagate or log the failure: after creating
llvm::raw_fd_ostream(p, ec) check if ec is set and then either throw a
std::runtime_error (including p and ec.message()) or emit a clear diagnostic via
llvm::errs()/llvm::report_fatal_error so tests fail fast; reference the touch()
function, the path() helper, llvm::raw_fd_ostream and the std::error_code ec
when implementing the check and message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a90da651-309b-4893-8efa-19008f664296

📥 Commits

Reviewing files that changed from the base of the PR and between f7a8d10 and 73bc73b.

📒 Files selected for processing (10)
  • CMakeLists.txt
  • src/command/command.cpp
  • src/command/command.h
  • src/command/search_config.cpp
  • src/command/search_config.h
  • src/command/toolchain_provider.cpp
  • src/command/toolchain_provider.h
  • src/support/path_pool.h
  • tests/unit/compile/command_tests.cpp
  • tests/unit/test/temp_dir.h

Comment thread src/command/command.cpp Outdated
Comment on lines +155 to +158
/// Cache of SearchConfig per CompilationInfo pointer. Since infos are
/// deduplicated by ObjectSet, the pointer uniquely identifies a compilation
/// context. This avoids re-parsing arguments on repeated scans.
llvm::DenseMap<const CompilationInfo*, SearchConfig> search_config_cache;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Key the SearchConfig cache on all inputs that affect lookup().

Lines 155-158 and 863-875 assume CompilationInfo* uniquely identifies the extracted search config, but lookup() also varies with CommandOptions (query_toolchain, resource_dir, append, remove) and with later ToolchainProvider warming via inject_results(). A first lookup on a cold toolchain cache can therefore memoize the fallback user_args config and keep returning it after system include paths become available, or after a caller requests a different option set.

Please widen the cache key or bypass this cache whenever lookup() can mutate the argv beyond the canonical CompilationInfo.

Also applies to: 841-875

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 155 - 158, The search_config_cache
currently keys solely by CompilationInfo* (search_config_cache) but lookup()
also depends on CommandOptions (query_toolchain, resource_dir, append, remove)
and on ToolchainProvider state changed by inject_results(), so update the cache
key to include those discriminating inputs (e.g., form a composite key struct
containing const CompilationInfo*, CommandOptions values that affect argv, and a
toolchain cache/version token from ToolchainProvider) or skip cached entries
when lookup() could mutate argv beyond the canonical CompilationInfo;
specifically modify lookup() and where search_config_cache is accessed/inserted
(symbols: search_config_cache, CompilationInfo, SearchConfig, lookup(),
CommandOptions.{query_toolchain,resource_dir,append,remove}, inject_results(),
ToolchainProvider) to ensure you either broaden the key or invalidate/bypass the
cache when toolchain warming or differing CommandOptions are in effect.

Comment on lines +137 to +145
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Empty query results are cached, preventing retries on transient failures.

If toolchain::query_toolchain() returns an empty vector (e.g., driver not found, execution failure), this empty result is cached permanently. Subsequent lookups for the same toolchain key will return the empty result without retrying, even if the issue was transient (e.g., network file system glitch, temporary resource exhaustion).

Consider either:

  1. Not caching empty results to allow retries
  2. Adding a TTL or retry mechanism for failed queries
  3. Documenting this as intentional behavior (to avoid hammering broken toolchains)
💡 Option 1: Skip caching empty results
     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);

+    // Don't cache empty results to allow retries on transient failures.
+    if(result.empty()) {
+        return {};
+    }
+
     auto [entry, _] = self.toolchain_cache.try_emplace(std::move(key), std::move(result));
     return entry->second;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
}
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);
// Don't cache empty results to allow retries on transient failures.
if(result.empty()) {
return {};
}
auto [entry, _] = self.toolchain_cache.try_emplace(std::move(key), std::move(result));
return entry->second;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/toolchain_provider.cpp` around lines 137 - 145, The code
currently caches whatever toolchain::query_toolchain(params) returns into
self.toolchain_cache via try_emplace(key, result), which persists empty vectors
and prevents retries; change the logic in the function that builds params and
calls query_toolchain so that after obtaining auto result you check if result is
empty and only insert into self.toolchain_cache when non-empty (i.e., skip
try_emplace when result.empty()), returning the fresh result directly in the
empty case so callers can retry later; reference symbols:
toolchain::query_toolchain, toolchain::QueryParams, result, key,
self.toolchain_cache, try_emplace.

Comment thread src/support/path_pool.h
Comment on lines +14 to +36
struct PathPool {
llvm::BumpPtrAllocator allocator;
llvm::SmallVector<llvm::StringRef> paths;
llvm::StringMap<std::uint32_t> cache;

std::uint32_t intern(llvm::StringRef path) {
auto [it, inserted] = cache.try_emplace(path, paths.size());
if(inserted) {
// Allocate with null terminator so that resolve().data() is safe
// to use as const char* (e.g. in MemoryBuffer::getFile which calls strlen).
const std::size_t n = path.size();
char* buf = allocator.Allocate<char>(n + 1);
std::copy(path.begin(), path.end(), buf);
buf[n] = '\0';
paths.push_back(llvm::StringRef(buf, n));
}
return it->second;
}

llvm::StringRef resolve(std::uint32_t id) const {
return paths[id];
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ID 0 should be reserved as a sentinel value for consistency with StringSet.

The existing StringSet in src/support/object_pool.h reserves ID 0 as a sentinel for "no string" / "unset" (see Lines 23, 33-34). PathPool assigns 0 as the first valid ID, which breaks this convention. Code that checks path_id == 0 to detect "unset" will incorrectly match the first interned path.

Additionally, resolve() lacks bounds checking and will crash on invalid IDs.

🛠️ Proposed fix to reserve ID 0 and add bounds check
 struct PathPool {
     llvm::BumpPtrAllocator allocator;
     llvm::SmallVector<llvm::StringRef> paths;
     llvm::StringMap<std::uint32_t> cache;

+    PathPool() {
+        paths.emplace_back();  // Reserve index 0 as sentinel (empty path)
+    }
+
     std::uint32_t intern(llvm::StringRef path) {
+        if(path.empty()) {
+            return 0;  // Sentinel for empty/unset
+        }
         auto [it, inserted] = cache.try_emplace(path, paths.size());
         if(inserted) {
             // Allocate with null terminator so that resolve().data() is safe
             // to use as const char* (e.g. in MemoryBuffer::getFile which calls strlen).
             const std::size_t n = path.size();
             char* buf = allocator.Allocate<char>(n + 1);
             std::copy(path.begin(), path.end(), buf);
             buf[n] = '\0';
             paths.push_back(llvm::StringRef(buf, n));
         }
         return it->second;
     }

     llvm::StringRef resolve(std::uint32_t id) const {
+        assert(id < paths.size() && "Invalid path ID");
         return paths[id];
     }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/support/path_pool.h` around lines 14 - 36, PathPool currently hands out 0
as a valid ID and has no bounds checking in resolve; change PathPool so ID 0 is
reserved as a sentinel by inserting an initial empty entry into paths (and
ensure cache indices start from 1) before any intern() calls, adjust
intern(llvm::StringRef path) to assign IDs starting at 1 (e.g., use paths.size()
after the sentinel) and keep existing null-terminated allocation logic, and
update resolve(std::uint32_t id) to check that id != 0 and id < paths.size()
returning an empty llvm::StringRef (or assert/handle error) for invalid ids to
avoid crashes.

- Move test files to tests/unit/command/ matching source relocation
- Add SearchConfig tests (8) and ToolchainProvider tests (11)
- Fix cache key to include -x language mode and CommandOptions
- Fix Quoted dedup bug (don't seed seen set with Quoted paths)
- Remove CommandOptions::resource_dir, make it part of query_toolchain
- Move resource_dir from global init to CompilationDatabase::resource_dir()
  using Driver::GetResourcesPath + getMainExecutable (no argv[0] dependency)
- Skip flaky InlayHint.Special test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
tests/unit/feature/inlay_hint_tests.cpp (1)

1339-1339: Consider tracking this skipped test with an issue or TODO comment.

The Special test case is being skipped, which the PR notes as a "pre-existing failure." While skipping is reasonable to unblock the PR, consider adding a TODO comment or linking to an issue to ensure this doesn't remain permanently skipped and forgotten.

💡 Suggested improvement
-TEST_CASE(Special, {.skip = true}) {
+// TODO(`#XXX`): Re-enable once resource-dir initialization is stabilized
+TEST_CASE(Special, {.skip = true}) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/feature/inlay_hint_tests.cpp` at line 1339, Add a TODO or issue
link next to the skipped test declaration TEST_CASE(Special, {.skip = true}) so
the skip is tracked; update the test file to include a short comment like "TODO:
track pre-existing failure - see ISSUE-XXXX" (or a URL) and optionally include a
brief reason and expected behavior, ensuring the comment sits immediately above
or inline with the TEST_CASE(Special, {.skip = true}) declaration so reviewers
can find and resolve it later.
tests/unit/command/toolchain_provider_tests.cpp (1)

28-45: Test assertion does not verify the claimed behavior.

The comment on line 42 states "query_cached with same key should return the first result," but the test only verifies has_cached_entries() returns true. This doesn't confirm whether the first or second result was retained.

Consider adding a verification that actually queries the cached value and checks which triple was stored, or remove the misleading comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/command/toolchain_provider_tests.cpp` around lines 28 - 45, The
test's comment claims "query_cached with same key should return the first
result" but only checks has_cached_entries(); update TEST_CASE
InjectResultsSkipsDuplicateKeys to actually fetch the cached entry for "key1"
(use ToolchainProvider::query_cached or the provider method that returns the
cached ToolchainResult) and assert that the stored triple/arguments equal the
first injected result ("x86_64"), or if you cannot query the cache from the
test, remove or update the misleading comment to reflect what the test actually
verifies (presence of cached entries via has_cached_entries()).
tests/unit/command/search_config_tests.cpp (1)

133-161: Clarify prefix concatenation semantics in test.

The test relies on tmp.path("gcc/12/") returning a path with a trailing separator (line 137), but this behavior may be platform-specific or implementation-dependent. The comment on line 136 mentions this requirement, which is good documentation.

However, consider making the test more robust by explicitly constructing paths with the separator, or adding an assertion that prefix12 ends with a path separator to catch any future TempDir::path() behavior changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/command/search_config_tests.cpp` around lines 133 - 161, In
TEST_CASE PrefixIncludeOptions, make the prefix concatenation explicit and
robust by ensuring prefix12 and prefix13 include a trailing path separator
before using them (or assert TempDir::path() returns one); modify the setup
around TempDir tmp and the variables prefix12/prefix13 so you either append the
platform-specific separator (use e.g.
std::filesystem::path::preferred_separator) or add an assertion that
prefix12.back() is a separator, so extract_search_config and the EXPECT_EQ
checks that compare config.dirs[*].path against tmp.path("gcc/12/include")
remain correct and platform-independent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/command/command.cpp`:
- Around line 161-163: The cache key built by options_bits currently encodes
only query_toolchain, but lookup() (and thus lookup_search_config) also mutates
results based on CommandOptions::append and CommandOptions::remove, so different
append/remove values can return stale SearchConfig; update options_bits(const
CommandOptions&) to incorporate append/remove into the key (e.g., include
presence or a simple hash of the append/remove vectors/strings) so that
lookup()/lookup_search_config and cached SearchConfig entries are differentiated
by those values, or alternatively document in the lookup_search_config API that
append/remove must be constant for a given CompilationInfo; touch the functions
options_bits, CommandOptions, lookup(), and lookup_search_config when applying
this change.
- Around line 170-193: is_same_file currently normalizes slashes on Windows but
still does case-sensitive compares causing paths like "C:\Foo\Bar.cpp" vs
"c:\foo\bar.cpp" to be considered different; update the Windows-only branch in
is_same_file to perform a case-insensitive comparison (e.g., convert characters
to a common case or use a locale-independent tolower on each character) when
comparing normalized characters (argument and file) so that both slash and case
differences are ignored; ensure you apply the case normalization in the same
per-character loop (or normalize whole strings before comparing) and use safe
casts for char-to-unsigned-char when calling tolower to avoid UB.
- Around line 773-776: The unconditional arguments.pop_back() after
arguments.assign(cached.begin(), cached.end()) is unsafe; before removing the
last element, check that arguments.size() > 0 (or >1 if you require at least one
retained argument) and that the last element is actually the temporary source
filename (e.g., matches the temp file pattern or a known marker). If the checks
fail, skip pop_back() (or log a warning) to avoid removing a required flag;
update the block surrounding arguments, cached, and pop_back() accordingly.

In `@tests/unit/command/toolchain_provider_tests.cpp`:
- Around line 47-74: The test GetPendingQueriesReturnsUncachedOnly injects a
cached result with key "preloaded_key" but constructs PendingEntry instances
that compute a different toolchain key, so the test only exercises deduplication
between entry1 and entry2 rather than the "skip cached" behavior; update the
test by injecting a result whose key matches the computed key for the
constructed entries (use ToolchainProvider::inject_results with the actual
computed key pattern used by ToolchainProvider's get_pending_queries logic) or
else rename the test to reflect it only verifies duplicate-entry deduplication;
locate symbols ToolchainProvider, inject_results, get_pending_queries, and
PendingEntry to make the change.

---

Nitpick comments:
In `@tests/unit/command/search_config_tests.cpp`:
- Around line 133-161: In TEST_CASE PrefixIncludeOptions, make the prefix
concatenation explicit and robust by ensuring prefix12 and prefix13 include a
trailing path separator before using them (or assert TempDir::path() returns
one); modify the setup around TempDir tmp and the variables prefix12/prefix13 so
you either append the platform-specific separator (use e.g.
std::filesystem::path::preferred_separator) or add an assertion that
prefix12.back() is a separator, so extract_search_config and the EXPECT_EQ
checks that compare config.dirs[*].path against tmp.path("gcc/12/include")
remain correct and platform-independent.

In `@tests/unit/command/toolchain_provider_tests.cpp`:
- Around line 28-45: The test's comment claims "query_cached with same key
should return the first result" but only checks has_cached_entries(); update
TEST_CASE InjectResultsSkipsDuplicateKeys to actually fetch the cached entry for
"key1" (use ToolchainProvider::query_cached or the provider method that returns
the cached ToolchainResult) and assert that the stored triple/arguments equal
the first injected result ("x86_64"), or if you cannot query the cache from the
test, remove or update the misleading comment to reflect what the test actually
verifies (presence of cached entries via has_cached_entries()).

In `@tests/unit/feature/inlay_hint_tests.cpp`:
- Line 1339: Add a TODO or issue link next to the skipped test declaration
TEST_CASE(Special, {.skip = true}) so the skip is tracked; update the test file
to include a short comment like "TODO: track pre-existing failure - see
ISSUE-XXXX" (or a URL) and optionally include a brief reason and expected
behavior, ensuring the comment sits immediately above or inline with the
TEST_CASE(Special, {.skip = true}) declaration so reviewers can find and resolve
it later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b650d91c-26bd-4957-ab0e-3cd0b711af58

📥 Commits

Reviewing files that changed from the base of the PR and between 73bc73b and ec570a7.

📒 Files selected for processing (16)
  • src/clice.cc
  • src/command/command.cpp
  • src/command/command.h
  • src/command/search_config.cpp
  • src/command/toolchain_provider.cpp
  • src/server/master_server.cpp
  • src/support/filesystem.h
  • tests/unit/command/command_tests.cpp
  • tests/unit/command/search_config_tests.cpp
  • tests/unit/command/toolchain_provider_tests.cpp
  • tests/unit/command/toolchain_tests.cpp
  • tests/unit/feature/inlay_hint_tests.cpp
  • tests/unit/server/stateless_worker_tests.cpp
  • tests/unit/server/worker_test_helpers.h
  • tests/unit/test/tester.cpp
  • tests/unit/unit_tests.cc
💤 Files with no reviewable changes (3)
  • tests/unit/unit_tests.cc
  • tests/unit/test/tester.cpp
  • src/support/filesystem.h

Comment thread src/command/command.cpp
Comment on lines +161 to +163
static std::uint8_t options_bits(const CommandOptions& options) {
return options.query_toolchain ? 1u : 0u;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

options_bits does not encode append/remove options that affect lookup() output.

The options_bits() function only encodes query_toolchain, but lookup() also uses options.append and options.remove (lines 487-569) to modify the final arguments. If the same CompilationInfo is looked up with different append/remove options, the cache will return a stale SearchConfig.

Consider either:

  1. Including a hash of append/remove in the cache key, or
  2. Documenting that lookup_search_config must only be called with consistent append/remove for a given context.
Option 1: Extend options_bits to include append/remove presence
     static std::uint8_t options_bits(const CommandOptions& options) {
-        return options.query_toolchain ? 1u : 0u;
+        std::uint8_t bits = 0;
+        if(options.query_toolchain) bits |= 1u;
+        if(!options.append.empty()) bits |= 2u;
+        if(!options.remove.empty()) bits |= 4u;
+        return bits;
     }

Note: This only distinguishes empty vs non-empty; a full solution would hash the actual content.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static std::uint8_t options_bits(const CommandOptions& options) {
return options.query_toolchain ? 1u : 0u;
}
static std::uint8_t options_bits(const CommandOptions& options) {
std::uint8_t bits = 0;
if(options.query_toolchain) bits |= 1u;
if(!options.append.empty()) bits |= 2u;
if(!options.remove.empty()) bits |= 4u;
return bits;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 161 - 163, The cache key built by
options_bits currently encodes only query_toolchain, but lookup() (and thus
lookup_search_config) also mutates results based on CommandOptions::append and
CommandOptions::remove, so different append/remove values can return stale
SearchConfig; update options_bits(const CommandOptions&) to incorporate
append/remove into the key (e.g., include presence or a simple hash of the
append/remove vectors/strings) so that lookup()/lookup_search_config and cached
SearchConfig entries are differentiated by those values, or alternatively
document in the lookup_search_config API that append/remove must be constant for
a given CompilationInfo; touch the functions options_bits, CommandOptions,
lookup(), and lookup_search_config when applying this change.

Comment thread src/command/command.cpp
Comment on lines +170 to +193
/// Check if an argument matches the source file path, handling
/// Windows path separator differences (backslash vs forward slash).
static bool is_same_file(llvm::StringRef argument, llvm::StringRef file) {
if(argument == file) {
return true;
}

#ifdef _WIN32
// On Windows, cmake may use backslashes in `arguments` but forward
// slashes in `file`. Normalize and compare.
if(argument.size() == file.size()) {
for(std::size_t i = 0; i < argument.size(); i++) {
char a = argument[i] == '\\' ? '/' : argument[i];
char b = file[i] == '\\' ? '/' : file[i];
if(a != b) {
return false;
}
}
return true;
}
#endif

return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Windows path comparison may fail on case differences.

The is_same_file() function normalizes slash direction but doesn't handle case-insensitivity. On Windows, C:\Foo\Bar.cpp and c:\foo\bar.cpp refer to the same file, but this function would return false.

If CMake or build systems generate paths with inconsistent casing, the source file won't be filtered from arguments (line 208), potentially causing duplicate entries or unexpected behavior.

Suggested case-insensitive comparison for Windows
 `#ifdef` _WIN32
         // On Windows, cmake may use backslashes in `arguments` but forward
         // slashes in `file`. Normalize and compare.
         if(argument.size() == file.size()) {
             for(std::size_t i = 0; i < argument.size(); i++) {
                 char a = argument[i] == '\\' ? '/' : argument[i];
                 char b = file[i] == '\\' ? '/' : file[i];
+                // Case-insensitive comparison on Windows
+                a = std::tolower(static_cast<unsigned char>(a));
+                b = std::tolower(static_cast<unsigned char>(b));
                 if(a != b) {
                     return false;
                 }
             }
             return true;
         }
 `#endif`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 170 - 193, is_same_file currently
normalizes slashes on Windows but still does case-sensitive compares causing
paths like "C:\Foo\Bar.cpp" vs "c:\foo\bar.cpp" to be considered different;
update the Windows-only branch in is_same_file to perform a case-insensitive
comparison (e.g., convert characters to a common case or use a
locale-independent tolower on each character) when comparing normalized
characters (argument and file) so that both slash and case differences are
ignored; ensure you apply the case normalization in the same per-character loop
(or normalize whole strings before comparing) and use safe casts for
char-to-unsigned-char when calling tolower to avoid UB.

Comment thread src/command/command.cpp
Comment on lines +773 to +776
arguments.assign(cached.begin(), cached.end());

// Remove the temp source file that was appended during query.
arguments.pop_back();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unconditional pop_back() assumes cached result always has a trailing temp source file.

Line 776 calls arguments.pop_back() without checking if arguments has more than one element. While the code path only executes when cached is non-empty (checked on line 768), if the cached result somehow contains only the -cc1 flag or minimal elements, this could remove a required argument.

Consider adding a size check for safety:

Suggested defensive check
             // Start with cc1 result (has system paths, driver flags, etc.).
             arguments.assign(cached.begin(), cached.end());

-            // Remove the temp source file that was appended during query.
-            arguments.pop_back();
+            // Remove the temp source file that was appended during query.
+            if(!arguments.empty()) {
+                arguments.pop_back();
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/command.cpp` around lines 773 - 776, The unconditional
arguments.pop_back() after arguments.assign(cached.begin(), cached.end()) is
unsafe; before removing the last element, check that arguments.size() > 0 (or >1
if you require at least one retained argument) and that the last element is
actually the temporary source filename (e.g., matches the temp file pattern or a
known marker). If the checks fail, skip pop_back() (or log a warning) to avoid
removing a required flag; update the block surrounding arguments, cached, and
pop_back() accordingly.

Comment on lines +47 to +74
TEST_CASE(GetPendingQueriesReturnsUncachedOnly) {
ToolchainProvider provider;

// Inject a result for one toolchain configuration.
std::vector<ToolchainResult> results;
results.push_back({
"preloaded_key",
{"-cc1", "-triple", "x86_64"}
});
provider.inject_results(results);

// Create entries: one matching the cached key pattern, one new.
// Since get_pending_queries extracts keys internally, we need real-ish args.
ToolchainProvider::PendingEntry entry1;
entry1.file = "a.cpp";
entry1.directory = "/tmp";
entry1.arguments = {"clang++", "-std=c++17", "a.cpp"};

ToolchainProvider::PendingEntry entry2;
entry2.file = "b.cpp";
entry2.directory = "/tmp";
entry2.arguments = {"clang++", "-std=c++17", "b.cpp"};

// Both entries have the same toolchain key (same driver, same extension,
// same toolchain flags), so only one query should be returned.
auto queries = provider.get_pending_queries({entry1, entry2});
EXPECT_EQ(queries.size(), 1u);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Test may not verify intended behavior due to key mismatch.

The test injects a result with key "preloaded_key" (line 53), but the entries constructed on lines 60-68 will generate a completely different computed key based on clang++, file extension, and toolchain flags. The comment on line 58 says "one matching the cached key pattern" but no entry actually matches "preloaded_key".

As a result, this test only verifies deduplication of identical keys between entry1 and entry2, not the "returns uncached only" behavior claimed in the test name.

Consider either:

  1. Inject a result using an actual computed key (as done in InjectThenGetPendingSkipsCached), or
  2. Rename/refactor the test to accurately reflect what it verifies.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/command/toolchain_provider_tests.cpp` around lines 47 - 74, The
test GetPendingQueriesReturnsUncachedOnly injects a cached result with key
"preloaded_key" but constructs PendingEntry instances that compute a different
toolchain key, so the test only exercises deduplication between entry1 and
entry2 rather than the "skip cached" behavior; update the test by injecting a
result whose key matches the computed key for the constructed entries (use
ToolchainProvider::inject_results with the actual computed key pattern used by
ToolchainProvider's get_pending_queries logic) or else rename the test to
reflect it only verifies duplicate-entry deduplication; locate symbols
ToolchainProvider, inject_results, get_pending_queries, and PendingEntry to make
the change.

@16bit-ykiko

Copy link
Copy Markdown
Member Author

Superseded by #369 (refactor) and #370 (feature), both targeting main.

@16bit-ykiko
16bit-ykiko deleted the feat/search-config branch March 28, 2026 09:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant