#include "project.h" #include "cache_manager.h" #include "clang_utils.h" #include "filesystem.hh" #include "language.h" #include "match.h" #include "platform.h" #include "queue_manager.h" #include "serializers/json.h" #include "timer.h" #include "utils.h" #include "working_files.h" #include #include #include #include #if defined(__unix__) || defined(__APPLE__) #include #endif #include #include #include #include struct CompileCommandsEntry { fs::path directory; std::string file; std::string command; std::vector args; fs::path ResolveIfRelative(fs::path path) const { if (path.is_absolute()) return path; return directory / path; } }; MAKE_REFLECT_STRUCT(CompileCommandsEntry, directory, file, command, args); namespace { enum class ProjectMode { CompileCommandsJson, DotCcls, ExternalCommand }; struct ProjectConfig { std::unordered_set quote_dirs; std::unordered_set angle_dirs; std::vector extra_flags; fs::path project_dir; ProjectMode mode = ProjectMode::CompileCommandsJson; }; // TODO: See // https://github.com/Valloric/ycmd/blob/master/ycmd/completers/cpp/flags.py. // Flags '-include' and '-include-pch' are blacklisted here cause libclang returns error in case when // precompiled header was generated by a different compiler (even two different builds of same version // of clang for the same platform are incompatible). Note that libclang always generate it's own pch // internally. For details, see https://github.com/Valloric/ycmd/issues/892 . std::vector kBlacklistMulti = { "-MF", "-MT", "-MQ", "-o", "--serialize-diagnostics", "-Xclang"}; // Blacklisted flags which are always removed from the command line. std::vector kBlacklist = { "-c", "-MP", "-MD", "-MMD", "--fcolor-diagnostics", "-showIncludes" }; // Arguments which are followed by a potentially relative path. We need to make // all relative paths absolute, otherwise libclang will not resolve them. std::vector kPathArgs = { "-I", "-iquote", "-cxx-isystem", "-isystem", "--sysroot=", "-isysroot", "-gcc-toolchain", "-include-pch", "-iframework", "-F", "-imacros", "-include", "/I", "-idirafter"}; // Arguments which always require an absolute path, ie, clang -working-directory // does not work as expected. Argument processing assumes that this is a subset // of kPathArgs. std::vector kNormalizePathArgs = {"--sysroot="}; // Arguments whose path arguments should be injected into include dir lookup // for #include completion. std::vector kQuoteIncludeArgs = {"-iquote", "-I", "/I"}; std::vector kAngleIncludeArgs = {"-cxx-isystem", "-isystem", "-I", "/I"}; bool ShouldAddToQuoteIncludes(const std::string& arg) { return StartsWithAny(arg, kQuoteIncludeArgs); } bool ShouldAddToAngleIncludes(const std::string& arg) { return StartsWithAny(arg, kAngleIncludeArgs); } Project::Entry GetCompilationEntryFromCompileCommandEntry( ProjectConfig* config, const CompileCommandsEntry& entry) { Project::Entry result; result.filename = entry.file; const std::string base_name = fs::path(entry.file).filename(); // Expand %c %cpp %clang std::vector args; const LanguageId lang = SourceFileLanguage(entry.file); for (const std::string& arg : entry.args) { if (arg.compare(0, 3, "%c ") == 0) { if (lang == LanguageId::C) args.push_back(arg.substr(3)); } else if (arg.compare(0, 5, "%cpp ") == 0) { if (lang == LanguageId::Cpp) args.push_back(arg.substr(5)); } else if (arg == "%clang") { args.push_back(lang == LanguageId::Cpp ? "clang++" : "clang"); } else { args.push_back(arg); } } if (args.empty()) return result; std::string first_arg = args[0]; // Windows' filesystem is not case sensitive, so we compare only // the lower case variant. std::transform(first_arg.begin(), first_arg.end(), first_arg.begin(), tolower); bool clang_cl = strstr(first_arg.c_str(), "clang-cl") || strstr(first_arg.c_str(), "cl.exe"); // Clang only cares about the last --driver-mode flag, so the loop // iterates in reverse to find the last one as soon as possible // in case of multiple --driver-mode flags. for (int i = args.size() - 1; i >= 0; --i) { if (strstr(args[i].c_str(), "--dirver-mode=")) { clang_cl = clang_cl || strstr(args[i].c_str(), "--driver-mode=cl"); break; } } // Compiler driver. result.args.push_back(args[0]); // Add -working-directory if not provided. if (!AnyStartsWith(args, "-working-directory")) result.args.emplace_back("-working-directory=" + entry.directory.string()); bool next_flag_is_path = false; bool add_next_flag_to_quote_dirs = false; bool add_next_flag_to_angle_dirs = false; // Note that when processing paths, some arguments support multiple forms, ie, // {"-Ifoo"} or {"-I", "foo"}. Support both styles. size_t i = 1; result.args.reserve(args.size() + config->extra_flags.size()); for (; i < args.size(); ++i) { std::string arg = args[i]; // Finish processing path for the previous argument, which was a switch. // {"-I", "foo"} style. if (next_flag_is_path) { std::string normalized_arg = entry.ResolveIfRelative(arg); if (add_next_flag_to_quote_dirs) config->quote_dirs.insert(normalized_arg); if (add_next_flag_to_angle_dirs) config->angle_dirs.insert(normalized_arg); if (clang_cl) arg = normalized_arg; next_flag_is_path = false; add_next_flag_to_quote_dirs = false; add_next_flag_to_angle_dirs = false; } else { // If blacklist skip. if (StartsWithAny(arg, kBlacklistMulti)) { i++; continue; } // Check to see if arg is a path and needs to be updated. for (const std::string& flag_type : kPathArgs) { // {"-I", "foo"} style. if (arg == flag_type) { next_flag_is_path = true; add_next_flag_to_quote_dirs = ShouldAddToQuoteIncludes(arg); add_next_flag_to_angle_dirs = ShouldAddToAngleIncludes(arg); goto done; } // {"-Ifoo"} style. if (StartsWith(arg, flag_type)) { std::string path = arg.substr(flag_type.size()); assert(!path.empty()); path = entry.ResolveIfRelative(path); if (clang_cl || StartsWithAny(arg, kNormalizePathArgs)) arg = flag_type + path; if (ShouldAddToQuoteIncludes(flag_type)) config->quote_dirs.insert(path); if (ShouldAddToAngleIncludes(flag_type)) config->angle_dirs.insert(path); goto done; } } if (StartsWithAny(arg, kBlacklist)) continue; // This is most likely the file path we will be passing to clang. The // path needs to be absolute, otherwise clang_codeCompleteAt is extremely // slow. See // https://github.com/cquery-project/cquery/commit/af63df09d57d765ce12d40007bf56302a0446678. if (EndsWith(arg, base_name)) arg = entry.ResolveIfRelative(arg); // TODO Exclude .a .o to make link command in compile_commands.json work. // Also, clang_parseTranslationUnit2FullArgv does not seem to accept // multiple source filenames. else if (EndsWith(arg, ".a") || EndsWith(arg, ".o")) continue; } done: result.args.push_back(arg); } // We don't do any special processing on user-given extra flags. for (const auto& flag : config->extra_flags) result.args.push_back(flag); // Add -resource-dir so clang can correctly resolve system includes like // if (!AnyStartsWith(result.args, "-resource-dir")) result.args.push_back("-resource-dir=" + g_config->clang.resourceDir); // There could be a clang version mismatch between what the project uses and // what ccls uses. Make sure we do not emit warnings for mismatched options. if (!AnyStartsWith(result.args, "-Wno-unknown-warning-option")) result.args.push_back("-Wno-unknown-warning-option"); // Using -fparse-all-comments enables documentation in the indexer and in // code completion. if (g_config->index.comments > 1 && !AnyStartsWith(result.args, "-fparse-all-comments")) { result.args.push_back("-fparse-all-comments"); } return result; } std::vector ReadCompilerArgumentsFromFile( const std::string& path) { std::vector args; std::ifstream fin(path); for (std::string line; std::getline(fin, line);) { TrimInPlace(line); if (line.empty() || StartsWith(line, "#")) continue; args.push_back(line); } return args; } std::vector LoadFromDirectoryListing(ProjectConfig* config) { std::vector result; config->mode = ProjectMode::DotCcls; LOG_IF_S(WARNING, !fs::exists(config->project_dir / ".ccls") && config->extra_flags.empty()) << "ccls has no clang arguments. Considering adding either a " "compile_commands.json or .ccls file. See the ccls README for " "more information."; std::unordered_map> folder_args; std::vector files; GetFilesInFolder(config->project_dir, true /*recursive*/, true /*add_folder_to_path*/, [&folder_args, &files](const std::string& path) { if (SourceFileLanguage(path) != LanguageId::Unknown) { files.push_back(path); } else if (fs::path(path).filename() == ".ccls") { LOG_S(INFO) << "Using .ccls arguments from " << path; folder_args.emplace( fs::path(path).parent_path().string(), ReadCompilerArgumentsFromFile(path)); } }); const std::string project_dir = config->project_dir.string(); const auto& project_dir_args = folder_args[project_dir]; LOG_IF_S(INFO, !project_dir_args.empty()) << "Using .ccls arguments " << StringJoin(project_dir_args); auto GetCompilerArgumentForFile = [&project_dir, &folder_args](fs::path cur) { while (!(cur = cur.parent_path()).empty()) { auto it = folder_args.find(cur); if (it != folder_args.end()) return it->second; std::string normalized = NormalizePath(cur); // Break if outside of the project root. if (normalized.size() <= project_dir.size() || normalized.compare(0, project_dir.size(), project_dir) != 0) break; } return folder_args[project_dir]; }; for (const std::string& file : files) { CompileCommandsEntry e; e.directory = config->project_dir; e.file = file; e.args = GetCompilerArgumentForFile(file); if (e.args.empty()) e.args.push_back("%clang"); // Add a Dummy. e.args.push_back(e.file); result.push_back(GetCompilationEntryFromCompileCommandEntry(config, e)); } return result; } std::vector LoadCompilationEntriesFromDirectory( ProjectConfig* project, const std::string& opt_compilation_db_dir) { // If there is a .ccls file always load using directory listing. if (fs::exists(project->project_dir / ".ccls")) return LoadFromDirectoryListing(project); // If |compilationDatabaseCommand| is specified, execute it to get the compdb. fs::path comp_db_dir; if (g_config->compilationDatabaseCommand.empty()) { project->mode = ProjectMode::CompileCommandsJson; // Try to load compile_commands.json, but fallback to a project listing. comp_db_dir = opt_compilation_db_dir.empty() ? project->project_dir.string() : opt_compilation_db_dir; } else { project->mode = ProjectMode::ExternalCommand; #ifdef _WIN32 // TODO #else char tmpdir[] = "/tmp/ccls-compdb-XXXXXX"; if (!mkdtemp(tmpdir)) return {}; comp_db_dir = tmpdir; rapidjson::StringBuffer input; rapidjson::Writer writer(input); JsonWriter json_writer(&writer); Reflect(json_writer, *g_config); std::string contents = GetExternalCommandOutput( std::vector{g_config->compilationDatabaseCommand, project->project_dir}, input.GetString()); FILE* fout = fopen((comp_db_dir / "compile_commands.json").c_str(), "wb"); fwrite(contents.c_str(), contents.size(), 1, fout); fclose(fout); #endif } fs::path comp_db_path = comp_db_dir / "compile_commands.json"; LOG_S(INFO) << "Trying to load " << comp_db_path.string(); CXCompilationDatabase_Error cx_db_load_error; CXCompilationDatabase cx_db = clang_CompilationDatabase_fromDirectory( comp_db_dir.c_str(), &cx_db_load_error); if (!g_config->compilationDatabaseCommand.empty()) { #ifdef _WIN32 // TODO #else unlink(comp_db_path.c_str()); rmdir(comp_db_dir.c_str()); #endif } if (cx_db_load_error == CXCompilationDatabase_CanNotLoadDatabase) { LOG_S(INFO) << "Unable to load " << comp_db_path.string() << "; using directory listing instead."; return LoadFromDirectoryListing(project); } Timer clang_time; Timer our_time; clang_time.Pause(); our_time.Pause(); clang_time.Resume(); CXCompileCommands cx_commands = clang_CompilationDatabase_getAllCompileCommands(cx_db); unsigned int num_commands = clang_CompileCommands_getSize(cx_commands); clang_time.Pause(); std::vector result; for (unsigned int i = 0; i < num_commands; i++) { clang_time.Resume(); CXCompileCommand cx_command = clang_CompileCommands_getCommand(cx_commands, i); std::string directory = ToString(clang_CompileCommand_getDirectory(cx_command)); std::string relative_filename = ToString(clang_CompileCommand_getFilename(cx_command)); unsigned num_args = clang_CompileCommand_getNumArgs(cx_command); CompileCommandsEntry entry; entry.args.reserve(num_args); for (unsigned j = 0; j < num_args; ++j) { entry.args.push_back( ToString(clang_CompileCommand_getArg(cx_command, j))); } clang_time.Pause(); // TODO: don't call ToString in this block. // LOG_S(INFO) << "Got args " << StringJoin(entry.args); our_time.Resume(); entry.directory = directory; entry.file = entry.ResolveIfRelative(relative_filename); result.push_back( GetCompilationEntryFromCompileCommandEntry(project, entry)); our_time.Pause(); } clang_time.Resume(); clang_CompileCommands_dispose(cx_commands); clang_CompilationDatabase_dispose(cx_db); clang_time.Pause(); clang_time.ResetAndPrint("compile_commands.json clang time"); our_time.ResetAndPrint("compile_commands.json our time"); return result; } // Computes a score based on how well |a| and |b| match. This is used for // argument guessing. int ComputeGuessScore(std::string_view a, std::string_view b) { // Increase score based on common prefix and suffix. Prefixes are prioritized. if (a.size() < b.size()) std::swap(a, b); size_t i = std::mismatch(a.begin(), a.end(), b.begin()).first - a.begin(); size_t j = std::mismatch(a.rbegin(), a.rend(), b.rbegin()).first - a.rbegin(); int score = 10 * i + j; if (i + j < b.size()) score -= 100 * (std::count(a.begin() + i, a.end() - j, '/') + std::count(b.begin() + i, b.end() - j, '/')); return score; } } // namespace void Project::Load(const std::string& root_directory) { // Load data. ProjectConfig project; project.extra_flags = g_config->clang.extraArgs; project.project_dir = root_directory; entries = LoadCompilationEntriesFromDirectory( &project, g_config->compilationDatabaseDirectory); // Cleanup / postprocess include directories. quote_include_directories.assign(project.quote_dirs.begin(), project.quote_dirs.end()); angle_include_directories.assign(project.angle_dirs.begin(), project.angle_dirs.end()); for (std::string& path : quote_include_directories) { EnsureEndsInSlash(path); LOG_S(INFO) << "quote_include_dir: " << path; } for (std::string& path : angle_include_directories) { EnsureEndsInSlash(path); LOG_S(INFO) << "angle_include_dir: " << path; } // Setup project entries. std::lock_guard lock(mutex_); absolute_path_to_entry_index_.reserve(entries.size()); for (size_t i = 0; i < entries.size(); ++i) { entries[i].id = i; absolute_path_to_entry_index_[entries[i].filename] = i; } } void Project::SetFlagsForFile( const std::vector& flags, const std::string& path) { std::lock_guard lock(mutex_); auto it = absolute_path_to_entry_index_.find(path); if (it != absolute_path_to_entry_index_.end()) { // The entry already exists in the project, just set the flags. this->entries[it->second].args = flags; } else { // Entry wasn't found, so we create a new one. Entry entry; entry.is_inferred = false; entry.filename = path; entry.args = flags; this->entries.emplace_back(entry); } } Project::Entry Project::FindCompilationEntryForFile( const std::string& filename) { { std::lock_guard lock(mutex_); auto it = absolute_path_to_entry_index_.find(filename); if (it != absolute_path_to_entry_index_.end()) return entries[it->second]; } // We couldn't find the file. Try to infer it. // TODO: Cache inferred file in a separate array (using a lock or similar) Entry* best_entry = nullptr; int best_score = std::numeric_limits::min(); for (Entry& entry : entries) { int score = ComputeGuessScore(filename, entry.filename); if (score > best_score) { best_score = score; best_entry = &entry; } } Project::Entry result; result.is_inferred = true; result.filename = filename; if (!best_entry) { result.args.push_back("%clang"); result.args.push_back(filename); } else { result.args = best_entry->args; // |best_entry| probably has its own path in the arguments. We need to remap // that path to the new filename. fs::path best_entry_base_name = fs::path(best_entry->filename).filename(); for (std::string& arg : result.args) { try { if (arg == best_entry->filename || fs::path(arg).filename() == best_entry_base_name) { arg = filename; } } catch (...) { } } } return result; } void Project::ForAllFilteredFiles( std::function action) { GroupMatch matcher(g_config->index.whitelist, g_config->index.blacklist); for (int i = 0; i < entries.size(); ++i) { const Project::Entry& entry = entries[i]; std::string failure_reason; if (matcher.IsMatch(entry.filename, &failure_reason)) action(i, entries[i]); else if (g_config->index.logSkippedPaths) { LOG_S(INFO) << "[" << i + 1 << "/" << entries.size() << "]: Failed " << failure_reason << "; skipping " << entry.filename; } } } void Project::Index(QueueManager* queue, WorkingFiles* wfiles, lsRequestId id) { ForAllFilteredFiles([&](int i, const Project::Entry& entry) { std::optional content = ReadContent(entry.filename); if (!content) { LOG_S(ERROR) << "When loading project, canont read file " << entry.filename; return; } bool is_interactive = wfiles->GetFileByFilename(entry.filename) != nullptr; queue->index_request.PushBack(Index_Request(entry.filename, entry.args, is_interactive, *content, id)); }); // Dummy request to indicate that project is loaded and // trigger refreshing semantic highlight for all working files. queue->index_request.PushBack(Index_Request("", {}, false, "")); } TEST_SUITE("Project") { void CheckFlags(const std::string& directory, const std::string& file, std::vector raw, std::vector expected) { g_config = std::make_unique(); g_config->clang.resourceDir = "/w/resource_dir/"; ProjectConfig project; project.project_dir = "/w/c/s/"; CompileCommandsEntry entry; entry.directory = directory; entry.args = raw; entry.file = file; Project::Entry result = GetCompilationEntryFromCompileCommandEntry(&project, entry); if (result.args != expected) { fprintf(stderr, "Raw: %s\n", StringJoin(raw).c_str()); fprintf(stderr, "Expected: %s\n", StringJoin(expected).c_str()); fprintf(stderr, "Actual: %s\n", StringJoin(result.args).c_str()); } REQUIRE(result.args == expected); } void CheckFlags(std::vector raw, std::vector expected) { CheckFlags("/dir/", "file.cc", raw, expected); } TEST_CASE("strip meta-compiler invocations") { CheckFlags( /* raw */ {"clang", "-lstdc++", "myfile.cc"}, /* expected */ {"clang", "-working-directory=/dir/", "-lstdc++", "/dir/myfile.cc", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); CheckFlags( /* raw */ {"clang.exe"}, /* expected */ {"clang.exe", "-working-directory=/dir/", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); } #ifdef _WIN32 TEST_CASE("Windows path normalization") { CheckFlags("E:/workdir", "E:/workdir/bar.cc", /* raw */ {"clang", "bar.cc"}, /* expected */ {"clang", "-working-directory=E:/workdir", "E:/workdir/bar.cc", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); CheckFlags("E:/workdir", "E:/workdir/bar.cc", /* raw */ {"clang", "E:/workdir/bar.cc"}, /* expected */ {"clang", "-working-directory=E:/workdir", "E:/workdir/bar.cc", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); CheckFlags("E:/workdir", "E:/workdir/bar.cc", /* raw */ {"clang-cl.exe", "/I./test", "E:/workdir/bar.cc"}, /* expected */ {"clang-cl.exe", "-working-directory=E:/workdir", "/I&E:/workdir/./test", "E:/workdir/bar.cc", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); CheckFlags("E:/workdir", "E:/workdir/bar.cc", /* raw */ {"cl.exe", "/I../third_party/test/include", "E:/workdir/bar.cc"}, /* expected */ {"cl.exe", "-working-directory=E:/workdir", "/I&E:/workdir/../third_party/test/include", "E:/workdir/bar.cc", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); } #endif TEST_CASE("Path in args") { CheckFlags("/home/user", "/home/user/foo/bar.c", /* raw */ {"cc", "-O0", "foo/bar.c"}, /* expected */ {"cc", "-working-directory=/home/user", "-O0", "/home/user/foo/bar.c", "-resource-dir=/w/resource_dir/", "-Wno-unknown-warning-option", "-fparse-all-comments"}); } TEST_CASE("Directory extraction") { g_config = std::make_unique(); ProjectConfig config; config.project_dir = "/w/c/s/"; CompileCommandsEntry entry; entry.directory = "/base"; entry.args = {"clang", "-I/a_absolute1", "--foobar", "-I", "/a_absolute2", "--foobar", "-Ia_relative1", "--foobar", "-isystem", "a_relative2", "--foobar", "-iquote/q_absolute1", "--foobar", "-iquote", "/q_absolute2", "--foobar", "-iquoteq_relative1", "--foobar", "-iquote", "q_relative2", "--foobar", "foo.cc"}; entry.file = "foo.cc"; Project::Entry result = GetCompilationEntryFromCompileCommandEntry(&config, entry); std::unordered_set angle_expected{ "/a_absolute1", "/a_absolute2", "/base/a_relative1", "/base/a_relative2"}; std::unordered_set quote_expected{ "/a_absolute1", "/a_absolute2", "/base/a_relative1", "/q_absolute1", "/q_absolute2", "/base/q_relative1", "/base/q_relative2"}; REQUIRE(config.angle_dirs == angle_expected); REQUIRE(config.quote_dirs == quote_expected); } TEST_CASE("Entry inference") { Project p; { Project::Entry e; e.args = {"arg1"}; e.filename = "/a/b/c/d/bar.cc"; p.entries.push_back(e); } { Project::Entry e; e.args = {"arg2"}; e.filename = "/a/b/c/baz.cc"; p.entries.push_back(e); } // Guess at same directory level, when there are parent directories. { std::optional entry = p.FindCompilationEntryForFile("/a/b/c/d/new.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg1"}); } // Guess at same directory level, when there are child directories. { std::optional entry = p.FindCompilationEntryForFile("/a/b/c/new.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg2"}); } // Guess at new directory (use the closest parent directory). { std::optional entry = p.FindCompilationEntryForFile("/a/b/c/new/new.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg2"}); } } TEST_CASE("Entry inference remaps file names") { Project p; { Project::Entry e; e.args = {"a", "b", "aaaa.cc", "d"}; e.filename = "absolute/aaaa.cc"; p.entries.push_back(e); } { std::optional entry = p.FindCompilationEntryForFile("ee.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"a", "b", "ee.cc", "d"}); } } TEST_CASE("Entry inference prefers same file endings") { Project p; { Project::Entry e; e.args = {"arg1"}; e.filename = "common/simple_browsertest.cc"; p.entries.push_back(e); } { Project::Entry e; e.args = {"arg2"}; e.filename = "common/simple_unittest.cc"; p.entries.push_back(e); } { Project::Entry e; e.args = {"arg3"}; e.filename = "common/a/simple_unittest.cc"; p.entries.push_back(e); } // Prefer files with the same ending. { std::optional entry = p.FindCompilationEntryForFile("my_browsertest.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg1"}); } { std::optional entry = p.FindCompilationEntryForFile("my_unittest.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg2"}); } { std::optional entry = p.FindCompilationEntryForFile("common/my_browsertest.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg1"}); } { std::optional entry = p.FindCompilationEntryForFile("common/my_unittest.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg2"}); } // Prefer the same directory over matching file-ending. { std::optional entry = p.FindCompilationEntryForFile("common/a/foo.cc"); REQUIRE(entry.has_value()); REQUIRE(entry->args == std::vector{"arg3"}); } } }