diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -523,6 +523,7 @@ if (Opts.UseDirBasedCDB) { DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS); CDBOpts.CompileCommandsDir = Opts.CompileCommandsDir; + CDBOpts.ContextProvider = Opts.ContextProvider; BaseCDB = std::make_unique(CDBOpts); BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs), diff --git a/clang-tools-extra/clangd/Config.h b/clang-tools-extra/clangd/Config.h --- a/clang-tools-extra/clangd/Config.h +++ b/clang-tools-extra/clangd/Config.h @@ -52,11 +52,19 @@ Config(Config &&) = default; Config &operator=(Config &&) = default; + struct CDBSearchSpec { + enum { Ancestors, FixedDir, NoCDBSearch } Policy = Ancestors; + // Absolute, native slashes, no trailing slash. + llvm::Optional FixedCDBPath; + }; + /// Controls how the compile command for the current file is determined. struct { - // Edits to apply to the compile command, in sequence. + /// Edits to apply to the compile command, in sequence. std::vector &) const>> Edits; + /// Where to search for compilation databases for this file's flags. + CDBSearchSpec CDBSearch = {CDBSearchSpec::Ancestors, llvm::None}; } CompileFlags; enum class BackgroundPolicy { Build, Skip }; diff --git a/clang-tools-extra/clangd/ConfigCompile.cpp b/clang-tools-extra/clangd/ConfigCompile.cpp --- a/clang-tools-extra/clangd/ConfigCompile.cpp +++ b/clang-tools-extra/clangd/ConfigCompile.cpp @@ -263,6 +263,36 @@ }); }); } + + if (F.CompilationDatabase) { + llvm::Optional Spec; + if (**F.CompilationDatabase == "Ancestors") { + Spec.emplace(); + Spec->Policy = Config::CDBSearchSpec::Ancestors; + } else if (**F.CompilationDatabase == "None") { + Spec.emplace(); + Spec->Policy = Config::CDBSearchSpec::NoCDBSearch; + } else { + if (auto Path = + makeAbsolute(*F.CompilationDatabase, "CompilationDatabase", + llvm::sys::path::Style::native)) { + // Drop trailing slash to put the path in canonical form. + // Should makeAbsolute do this? + llvm::StringRef Rel = llvm::sys::path::relative_path(*Path); + if (!Rel.empty() && llvm::sys::path::is_separator(Rel.back())) + Path->pop_back(); + + Spec.emplace(); + Spec->Policy = Config::CDBSearchSpec::FixedDir; + Spec->FixedCDBPath = std::move(Path); + } + } + if (Spec) + Out.Apply.push_back( + [Spec(std::move(*Spec))](const Params &, Config &C) { + C.CompileFlags.CDBSearch = Spec; + }); + } } void compile(Fragment::IndexBlock &&F) { diff --git a/clang-tools-extra/clangd/ConfigFragment.h b/clang-tools-extra/clangd/ConfigFragment.h --- a/clang-tools-extra/clangd/ConfigFragment.h +++ b/clang-tools-extra/clangd/ConfigFragment.h @@ -151,6 +151,13 @@ /// /// Flags added by the same CompileFlags entry will not be removed. std::vector> Remove; + + /// Directory to search for compilation database (compile_comands.json etc). + /// Valid values are: + /// - A single path to a directory (absolute, or relative to the fragment) + /// - Ancestors: search all parent directories (the default) + /// - None: do not use a compilation database, just default flags. + llvm::Optional> CompilationDatabase; }; CompileFlagsBlock CompileFlags; diff --git a/clang-tools-extra/clangd/ConfigYAML.cpp b/clang-tools-extra/clangd/ConfigYAML.cpp --- a/clang-tools-extra/clangd/ConfigYAML.cpp +++ b/clang-tools-extra/clangd/ConfigYAML.cpp @@ -95,6 +95,9 @@ if (auto Values = scalarValues(N)) F.Remove = std::move(*Values); }); + Dict.handle("CompilationDatabase", [&](Node &N) { + F.CompilationDatabase = scalarValue(N, "CompilationDatabase"); + }); Dict.parse(N); } diff --git a/clang-tools-extra/clangd/GlobalCompilationDatabase.h b/clang-tools-extra/clangd/GlobalCompilationDatabase.h --- a/clang-tools-extra/clangd/GlobalCompilationDatabase.h +++ b/clang-tools-extra/clangd/GlobalCompilationDatabase.h @@ -103,7 +103,10 @@ // (This is more expensive to check frequently, as we check many locations). std::chrono::steady_clock::duration RevalidateMissingAfter = std::chrono::seconds(30); + // Used to provide per-file configuration. + std::function ContextProvider; // Only look for a compilation database in this one fixed directory. + // FIXME: fold this into config/context mechanism. llvm::Optional CompileCommandsDir; }; @@ -126,14 +129,9 @@ Options Opts; class DirectoryCache; - // If there's an explicit CompileCommandsDir, cache of the CDB found there. - mutable std::unique_ptr OnlyDirCache; - // Keyed by possibly-case-folded directory path. // We can hand out pointers as they're stable and entries are never removed. - // Empty if CompileCommandsDir is given (OnlyDirCache is used instead). mutable llvm::StringMap DirCaches; - // DirCaches access must be locked (unlike OnlyDirCache, which is threadsafe). mutable std::mutex DirCachesMutex; std::vector diff --git a/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp b/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp --- a/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp +++ b/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp @@ -7,6 +7,7 @@ //===----------------------------------------------------------------------===// #include "GlobalCompilationDatabase.h" +#include "Config.h" #include "FS.h" #include "SourceCode.h" #include "support/Logger.h" @@ -20,6 +21,7 @@ #include "clang/Tooling/JSONCompilationDatabase.h" #include "llvm/ADT/None.h" #include "llvm/ADT/Optional.h" +#include "llvm/ADT/PointerIntPair.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/SmallString.h" @@ -362,8 +364,10 @@ DirectoryBasedGlobalCompilationDatabase:: DirectoryBasedGlobalCompilationDatabase(const Options &Opts) : Opts(Opts), Broadcaster(std::make_unique(*this)) { - if (Opts.CompileCommandsDir) - OnlyDirCache = std::make_unique(*Opts.CompileCommandsDir); + if (!this->Opts.ContextProvider) + this->Opts.ContextProvider = [](llvm::StringRef) { + return Context::current().clone(); + }; } DirectoryBasedGlobalCompilationDatabase:: @@ -405,14 +409,6 @@ #endif } -static bool pathEqual(PathRef A, PathRef B) { -#if defined(_WIN32) || defined(__APPLE__) - return A.equals_lower(B); -#else - return A == B; -#endif -} - std::vector DirectoryBasedGlobalCompilationDatabase::getDirectoryCaches( llvm::ArrayRef Dirs) const { @@ -441,31 +437,42 @@ assert(llvm::sys::path::is_absolute(Request.FileName) && "path must be absolute"); + std::string Storage; + std::vector SearchDirs; + if (Opts.CompileCommandsDir) // FIXME: unify this case with config. + SearchDirs = {Opts.CompileCommandsDir.getValue()}; + else { + WithContext WithProvidedContext(Opts.ContextProvider(Request.FileName)); + const auto &Spec = Config::current().CompileFlags.CDBSearch; + switch (Spec.Policy) { + case Config::CDBSearchSpec::NoCDBSearch: + return llvm::None; + case Config::CDBSearchSpec::FixedDir: + Storage = Spec.FixedCDBPath.getValue(); + SearchDirs = {Storage}; + break; + case Config::CDBSearchSpec::Ancestors: + // Traverse the canonical version to prevent false positives. i.e.: + // src/build/../a.cc can detect a CDB in /src/build if not + // canonicalized. + Storage = removeDots(Request.FileName); + actOnAllParentDirectories(Storage, [&](llvm::StringRef Dir) { + SearchDirs.push_back(Dir); + return false; + }); + } + } + + std::shared_ptr CDB = nullptr; bool ShouldBroadcast = false; DirectoryCache *DirCache = nullptr; - std::shared_ptr CDB = nullptr; - if (OnlyDirCache) { - DirCache = OnlyDirCache.get(); - ShouldBroadcast = Request.ShouldBroadcast; - CDB = DirCache->get(Opts.TFS, ShouldBroadcast, Request.FreshTime, - Request.FreshTimeMissing); - } else { - // Traverse the canonical version to prevent false positives. i.e.: - // src/build/../a.cc can detect a CDB in /src/build if not canonicalized. - std::string CanonicalPath = removeDots(Request.FileName); - std::vector SearchDirs; - actOnAllParentDirectories(CanonicalPath, [&](PathRef Path) { - SearchDirs.push_back(Path); - return false; - }); - for (DirectoryCache *Candidate : getDirectoryCaches(SearchDirs)) { - bool CandidateShouldBroadcast = Request.ShouldBroadcast; - if ((CDB = Candidate->get(Opts.TFS, CandidateShouldBroadcast, - Request.FreshTime, Request.FreshTimeMissing))) { - DirCache = Candidate; - ShouldBroadcast = CandidateShouldBroadcast; - break; - } + for (DirectoryCache *Candidate : getDirectoryCaches(SearchDirs)) { + bool CandidateShouldBroadcast = Request.ShouldBroadcast; + if ((CDB = Candidate->get(Opts.TFS, CandidateShouldBroadcast, + Request.FreshTime, Request.FreshTimeMissing))) { + DirCache = Candidate; + ShouldBroadcast = CandidateShouldBroadcast; + break; } } @@ -566,69 +573,176 @@ } }; -void DirectoryBasedGlobalCompilationDatabase::BroadcastThread::process( - const CDBLookupResult &T) { - vlog("Broadcasting compilation database from {0}", T.PI.SourceRoot); +// The DirBasedCDB associates each file with a specific CDB. +// When a CDB is discovered, it may claim to describe files that we associate +// with a different CDB. We do not want to broadcast discovery of these, and +// trigger background indexing of them. +// +// We must filter the list, and check whether they are associated with this CDB. +// This class attempts to do so efficiently. +// +// Roughly, it: +// - loads the config for each file, and determines the relevant search path +// - gathers all directories that are part of any search path +// - (lazily) checks for a CDB in each such directory at most once +// - walks the search path for each file and determines whether to include it. +class DirectoryBasedGlobalCompilationDatabase::BroadcastThread::Filter { + llvm::StringRef ThisDir; + DirectoryBasedGlobalCompilationDatabase &Parent; - std::vector AllFiles = T.CDB->getAllFiles(); - // We assume CDB in CompileCommandsDir owns all of its entries, since we don't - // perform any search in parent paths whenever it is set. - if (Parent.OnlyDirCache) { - assert(Parent.OnlyDirCache->Path == T.PI.SourceRoot && - "Trying to broadcast a CDB outside of CompileCommandsDir!"); - Parent.OnCommandChanged.broadcast(std::move(AllFiles)); - return; + // Keep track of all directories we might check for CDBs. + struct DirInfo { + DirectoryCache *Cache = nullptr; + enum { Unknown, Missing, TargetCDB, OtherCDB } State = Unknown; + DirInfo *Parent = nullptr; + }; + llvm::StringMap Dirs; + + // A search path starts at a directory, and either includes ancestors or not. + using SearchPath = llvm::PointerIntPair; + + // Add all ancestor directories of FilePath to the tracked set. + // Returns the immediate parent of the file. + DirInfo *addParents(llvm::StringRef FilePath) { + DirInfo *Leaf = nullptr; + DirInfo *Child = nullptr; + actOnAllParentDirectories(FilePath, [&](llvm::StringRef Dir) { + auto &Info = Dirs[Dir]; + // If this is the first iteration, then this node is the overall result. + if (!Leaf) + Leaf = &Info; + // Fill in the parent link from the previous iteration to this parent. + if (Child) + Child->Parent = &Info; + // Keep walking, whether we inserted or not, if parent link is missing. + // (If it's present, parent links must be present up to the root, so stop) + Child = &Info; + return Info.Parent != nullptr; + }); + return Leaf; } - // Uniquify all parent directories of all files. - llvm::StringMap DirectoryHasCDB; - std::vector FileAncestors; - for (llvm::StringRef File : AllFiles) { - actOnAllParentDirectories(File, [&](PathRef Path) { - auto It = DirectoryHasCDB.try_emplace(Path); - // Already seen this path, and all of its parents. - if (!It.second) - return true; + // Populates DirInfo::Cache (and State, if it is TargetCDB). + void grabCaches() { + // Fast path out if there were no files, or CDB loading is off. + if (Dirs.empty()) + return; - FileAncestors.push_back(It.first->getKey()); - return pathEqual(Path, T.PI.SourceRoot); - }); + std::vector DirKeys; + std::vector DirValues; + DirKeys.reserve(Dirs.size() + 1); + DirValues.reserve(Dirs.size()); + for (auto &E : Dirs) { + DirKeys.push_back(E.first()); + DirValues.push_back(&E.second); + } + + // Also look up the cache entry for the CDB we're broadcasting. + // Comparing DirectoryCache pointers is more robust than checking string + // equality, e.g. reuses the case-sensitivity handling. + DirKeys.push_back(ThisDir); + auto DirCaches = Parent.getDirectoryCaches(DirKeys); + const DirectoryCache *ThisCache = DirCaches.back(); + DirCaches.pop_back(); + DirKeys.pop_back(); + + for (unsigned I = 0; I < DirKeys.size(); ++I) { + DirValues[I]->Cache = DirCaches[I]; + if (DirCaches[I] == ThisCache) + DirValues[I]->State = DirInfo::TargetCDB; + } } - // Work out which ones have CDBs in them. - // Given that we know that CDBs have been moved/generated, don't trust caches. - // (This should be rare, so it's OK to add a little latency). - constexpr auto IgnoreCache = std::chrono::steady_clock::time_point::max(); - auto DirectoryCaches = Parent.getDirectoryCaches(FileAncestors); - assert(DirectoryCaches.size() == FileAncestors.size()); - for (unsigned I = 0; I < DirectoryCaches.size(); ++I) { - bool ShouldBroadcast = false; - if (ShouldStop.load(std::memory_order_acquire)) { - log("Giving up on broadcasting CDB, as we're shutting down"); - return; + + // Should we include a file from this search path? + bool shouldInclude(SearchPath P) { + DirInfo *Info = P.getPointer(); + if (!Info) + return false; + if (Info->State == DirInfo::Unknown) { + assert(Info->Cache && "grabCaches() should have filled this"); + // Given that we know that CDBs have been moved/generated, don't trust + // caches. (This should be rare, so it's OK to add a little latency). + constexpr auto IgnoreCache = std::chrono::steady_clock::time_point::max(); + // Don't broadcast CDBs discovered while broadcasting! + bool ShouldBroadcast = false; + bool Exists = + nullptr != Info->Cache->get(Parent.Opts.TFS, ShouldBroadcast, + /*FreshTime=*/IgnoreCache, + /*FreshTimeMissing=*/IgnoreCache); + Info->State = Exists ? DirInfo::OtherCDB : DirInfo::Missing; } - if (DirectoryCaches[I]->get(Parent.Opts.TFS, ShouldBroadcast, - /*FreshTime=*/IgnoreCache, - /*FreshTimeMissing=*/IgnoreCache)) - DirectoryHasCDB.find(FileAncestors[I])->setValue(true); + // If we have a CDB, include the file if it's the target CDB only. + if (Info->State != DirInfo::Missing) + return Info->State == DirInfo::TargetCDB; + // If we have no CDB and no relevant parent, don't include the file. + if (!P.getInt() || !Info->Parent) + return false; + // Walk up to the next parent. + return shouldInclude(SearchPath(Info->Parent, 1)); } - std::vector GovernedFiles; - for (llvm::StringRef File : AllFiles) { - // A file is governed by this CDB if lookup for the file would find it. - // Independent of whether it has an entry for that file or not. - actOnAllParentDirectories(File, [&](PathRef Path) { - if (DirectoryHasCDB.lookup(Path)) { - if (pathEqual(Path, T.PI.SourceRoot)) - // Make sure listeners always get a canonical path for the file. - GovernedFiles.push_back(removeDots(File)); - // Stop as soon as we hit a CDB. +public: + Filter(llvm::StringRef ThisDir, + DirectoryBasedGlobalCompilationDatabase &Parent) + : ThisDir(ThisDir), Parent(Parent) {} + + std::vector filter(std::vector AllFiles, + std::atomic &ShouldStop) { + std::vector Filtered; + // Allow for clean early-exit of the slow parts. + auto ExitEarly = [&] { + if (ShouldStop.load(std::memory_order_acquire)) { + log("Giving up on broadcasting CDB, as we're shutting down"); + Filtered.clear(); return true; } return false; - }); + }; + // Compute search path for each file. + std::vector SearchPaths(AllFiles.size()); + for (unsigned I = 0; I < AllFiles.size(); ++I) { + if (Parent.Opts.CompileCommandsDir) { // FIXME: unify with config + SearchPaths[I].setPointer( + &Dirs[Parent.Opts.CompileCommandsDir.getValue()]); + continue; + } + if (ExitEarly()) // loading config may be slow + return Filtered; + WithContext WithProvidedContent(Parent.Opts.ContextProvider(AllFiles[I])); + const Config::CDBSearchSpec &Spec = + Config::current().CompileFlags.CDBSearch; + switch (Spec.Policy) { + case Config::CDBSearchSpec::NoCDBSearch: + break; + case Config::CDBSearchSpec::Ancestors: + SearchPaths[I].setInt(/*Recursive=*/1); + SearchPaths[I].setPointer(addParents(AllFiles[I])); + break; + case Config::CDBSearchSpec::FixedDir: + SearchPaths[I].setPointer(&Dirs[Spec.FixedCDBPath.getValue()]); + break; + } + } + // Get the CDB cache for each dir on the search path, but don't load yet. + grabCaches(); + // Now work out which files we want to keep, loading CDBs where needed. + for (unsigned I = 0; I < AllFiles.size(); ++I) { + if (ExitEarly()) // loading CDBs may be slow + return Filtered; + if (shouldInclude(SearchPaths[I])) + Filtered.push_back(std::move(AllFiles[I])); + } + return Filtered; } +}; - Parent.OnCommandChanged.broadcast(std::move(GovernedFiles)); +void DirectoryBasedGlobalCompilationDatabase::BroadcastThread::process( + const CDBLookupResult &T) { + vlog("Broadcasting compilation database from {0}", T.PI.SourceRoot); + std::vector GovernedFiles = + Filter(T.PI.SourceRoot, Parent).filter(T.CDB->getAllFiles(), ShouldStop); + if (!GovernedFiles.empty()) + Parent.OnCommandChanged.broadcast(std::move(GovernedFiles)); } void DirectoryBasedGlobalCompilationDatabase::broadcastCDB( diff --git a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp --- a/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp +++ b/clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp @@ -194,6 +194,33 @@ EXPECT_EQ(From["name"], "caller1"); } +TEST_F(LSPTest, CDBConfigIntegration) { + auto CfgProvider = + config::Provider::fromAncestorRelativeYAMLFiles(".clangd", FS); + Opts.ConfigProvider = CfgProvider.get(); + + // Map bar.cpp to a different compilation database which defines FOO->BAR. + FS.Files[".clangd"] = R"yaml( +If: + PathMatch: bar.cpp +CompileFlags: + CompilationDatabase: bar +)yaml"; + FS.Files["bar/compile_flags.txt"] = "-DFOO=BAR"; + + auto &Client = start(); + // foo.cpp gets parsed as normal. + Client.didOpen("foo.cpp", "int x = FOO;"); + EXPECT_THAT(Client.diagnostics("foo.cpp"), + llvm::ValueIs(testing::ElementsAre( + DiagMessage("Use of undeclared identifier 'FOO'")))); + // bar.cpp shows the configured compile command. + Client.didOpen("bar.cpp", "int x = FOO;"); + EXPECT_THAT(Client.diagnostics("bar.cpp"), + llvm::ValueIs(testing::ElementsAre( + DiagMessage("Use of undeclared identifier 'BAR'")))); +} + } // namespace } // namespace clangd } // namespace clang diff --git a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp --- a/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp +++ b/clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp @@ -112,6 +112,46 @@ EXPECT_THAT(Argv, ElementsAre("clang", "a.cc", "-foo")); } +TEST_F(ConfigCompileTests, CompilationDatabase) { + Frag.CompileFlags.CompilationDatabase.emplace("None"); + EXPECT_TRUE(compileAndApply()); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, + Config::CDBSearchSpec::NoCDBSearch); + + Frag.CompileFlags.CompilationDatabase.emplace("Ancestors"); + EXPECT_TRUE(compileAndApply()); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, + Config::CDBSearchSpec::Ancestors); + + // Relative path not allowed without directory set. + Frag.CompileFlags.CompilationDatabase.emplace("Something"); + EXPECT_TRUE(compileAndApply()); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, + Config::CDBSearchSpec::Ancestors) + << "default value"; + EXPECT_THAT(Diags.Diagnostics, + ElementsAre(DiagMessage( + "CompilationDatabase must be an absolute path, because this " + "fragment is not associated with any directory."))); + + // Relative path allowed if directory is set. + Frag.Source.Directory = testRoot(); + EXPECT_TRUE(compileAndApply()); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, + Config::CDBSearchSpec::FixedDir); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something")); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); + + // Absolute path allowed. + Frag.Source.Directory.clear(); + Frag.CompileFlags.CompilationDatabase.emplace(testPath("Something2")); + EXPECT_TRUE(compileAndApply()); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.Policy, + Config::CDBSearchSpec::FixedDir); + EXPECT_EQ(Conf.CompileFlags.CDBSearch.FixedCDBPath, testPath("Something2")); + EXPECT_THAT(Diags.Diagnostics, IsEmpty()); +} + TEST_F(ConfigCompileTests, Index) { Frag.Index.Background.emplace("Skip"); EXPECT_TRUE(compileAndApply()); diff --git a/clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp b/clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp --- a/clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp +++ b/clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp @@ -8,6 +8,7 @@ #include "GlobalCompilationDatabase.h" +#include "Config.h" #include "Matchers.h" #include "TestFS.h" #include "support/Path.h" @@ -205,10 +206,12 @@ llvm::formatv(CDBOuter, llvm::sys::path::convert_to_slash(testRoot())); FS.Files[testPath("build/compile_commands.json")] = llvm::formatv(CDBInner, llvm::sys::path::convert_to_slash(testRoot())); + FS.Files[testPath("foo/compile_flags.txt")] = "-DFOO"; // Note that gen2.cc goes missing with our following model, not sure this // happens in practice though. { + SCOPED_TRACE("Default ancestor scanning"); DirectoryBasedGlobalCompilationDatabase DB(FS); std::vector DiscoveredFiles; auto Sub = @@ -227,8 +230,53 @@ EXPECT_THAT(DiscoveredFiles, UnorderedElementsAre(EndsWith("gen.cc"))); } - // With a custom compile commands dir. { + SCOPED_TRACE("With config"); + DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); + Opts.ContextProvider = [&](llvm::StringRef Path) { + Config Cfg; + if (Path.endswith("a.cc")) { + // a.cc uses another directory's CDB, so it won't be discovered. + Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::FixedDir; + Cfg.CompileFlags.CDBSearch.FixedCDBPath = testPath("foo"); + } else if (Path.endswith("gen.cc")) { + // gen.cc has CDB search disabled, so it won't be discovered. + Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::NoCDBSearch; + } else if (Path.endswith("gen2.cc")) { + // gen2.cc explicitly lists this directory, so it will be discovered. + Cfg.CompileFlags.CDBSearch.Policy = Config::CDBSearchSpec::FixedDir; + Cfg.CompileFlags.CDBSearch.FixedCDBPath = testRoot(); + } + return Context::current().derive(Config::Key, std::move(Cfg)); + }; + DirectoryBasedGlobalCompilationDatabase DB(Opts); + std::vector DiscoveredFiles; + auto Sub = + DB.watch([&DiscoveredFiles](const std::vector Changes) { + DiscoveredFiles = Changes; + }); + + // Does not use the root CDB, so no broadcast. + auto Cmd = DB.getCompileCommand(testPath("build/../a.cc")); + ASSERT_TRUE(Cmd.hasValue()); + EXPECT_THAT(Cmd->CommandLine, Contains("-DFOO")) << "a.cc uses foo/ CDB"; + ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); + EXPECT_THAT(DiscoveredFiles, IsEmpty()) << "Root CDB not discovered yet"; + + // No special config for b.cc, so we trigger broadcast of the root CDB. + DB.getCompileCommand(testPath("b.cc")); + ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); + EXPECT_THAT(DiscoveredFiles, ElementsAre(testPath("build/gen2.cc"))); + DiscoveredFiles.clear(); + + // No CDB search so no discovery/broadcast triggered for build/ CDB. + DB.getCompileCommand(testPath("build/gen.cc")); + ASSERT_TRUE(DB.blockUntilIdle(timeoutSeconds(10))); + EXPECT_THAT(DiscoveredFiles, IsEmpty()); + } + + { + SCOPED_TRACE("With custom compile commands dir"); DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); Opts.CompileCommandsDir = testRoot(); DirectoryBasedGlobalCompilationDatabase DB(Opts); @@ -294,6 +342,58 @@ EXPECT_EQ(testPath("x"), Commands.getValue().Directory); } +MATCHER_P(hasArg, Flag, "") { + if (!arg.hasValue()) { + *result_listener << "command is null"; + return false; + } + if (!llvm::is_contained(arg->CommandLine, Flag)) { + *result_listener << "flags are " << llvm::join(arg->CommandLine, " "); + return false; + } + return true; +} + +TEST(GlobalCompilationDatabaseTest, Config) { + MockFS FS; + FS.Files[testPath("x/compile_flags.txt")] = "-DX"; + FS.Files[testPath("x/y/z/compile_flags.txt")] = "-DZ"; + + Config::CDBSearchSpec Spec; + DirectoryBasedGlobalCompilationDatabase::Options Opts(FS); + Opts.ContextProvider = [&](llvm::StringRef Path) { + Config C; + C.CompileFlags.CDBSearch = Spec; + return Context::current().derive(Config::Key, std::move(C)); + }; + DirectoryBasedGlobalCompilationDatabase CDB(Opts); + + // Default ancestor behavior. + EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc"))); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/foo.cc")), hasArg("-DX")); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/foo.cc")), hasArg("-DX")); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/z/foo.cc")), hasArg("-DZ")); + + Spec.Policy = Config::CDBSearchSpec::NoCDBSearch; + EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/z/foo.cc"))); + + Spec.Policy = Config::CDBSearchSpec::FixedDir; + Spec.FixedCDBPath = testPath("w"); // doesn't exist + EXPECT_FALSE(CDB.getCompileCommand(testPath("foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/foo.cc"))); + EXPECT_FALSE(CDB.getCompileCommand(testPath("x/y/z/foo.cc"))); + + Spec.FixedCDBPath = testPath("x/y/z"); + EXPECT_THAT(CDB.getCompileCommand(testPath("foo.cc")), hasArg("-DZ")); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/foo.cc")), hasArg("-DZ")); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/foo.cc")), hasArg("-DZ")); + EXPECT_THAT(CDB.getCompileCommand(testPath("x/y/z/foo.cc")), hasArg("-DZ")); +} + TEST(GlobalCompilationDatabaseTest, NonCanonicalFilenames) { OverlayCDB DB(nullptr); std::vector DiscoveredFiles;