[clangd] Allow configuration database to be specified in config.
authorSam McCall <sam.mccall@gmail.com>
Wed, 20 Jan 2021 17:35:11 +0000 (18:35 +0100)
committerSam McCall <sam.mccall@gmail.com>
Mon, 25 Jan 2021 22:15:48 +0000 (23:15 +0100)
This allows for more flexibility than -compile-commands-dir or ancestor
discovery.

See https://github.com/clangd/clangd/issues/116

Differential Revision: https://reviews.llvm.org/D95057

clang-tools-extra/clangd/ClangdLSPServer.cpp
clang-tools-extra/clangd/Config.h
clang-tools-extra/clangd/ConfigCompile.cpp
clang-tools-extra/clangd/ConfigFragment.h
clang-tools-extra/clangd/ConfigYAML.cpp
clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
clang-tools-extra/clangd/GlobalCompilationDatabase.h
clang-tools-extra/clangd/unittests/ClangdLSPServerTests.cpp
clang-tools-extra/clangd/unittests/ConfigCompileTests.cpp
clang-tools-extra/clangd/unittests/GlobalCompilationDatabaseTests.cpp

index abfb1b2..dc89ebd 100644 (file)
@@ -523,6 +523,7 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
   if (Opts.UseDirBasedCDB) {
     DirectoryBasedGlobalCompilationDatabase::Options CDBOpts(TFS);
     CDBOpts.CompileCommandsDir = Opts.CompileCommandsDir;
+    CDBOpts.ContextProvider = Opts.ContextProvider;
     BaseCDB =
         std::make_unique<DirectoryBasedGlobalCompilationDatabase>(CDBOpts);
     BaseCDB = getQueryDriverDatabase(llvm::makeArrayRef(Opts.QueryDriverGlobs),
index 675e721..44ca283 100644 (file)
@@ -52,11 +52,19 @@ struct Config {
   Config(Config &&) = default;
   Config &operator=(Config &&) = default;
 
+  struct CDBSearchSpec {
+    enum { Ancestors, FixedDir, NoCDBSearch } Policy = Ancestors;
+    // Absolute, native slashes, no trailing slash.
+    llvm::Optional<std::string> 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<llvm::unique_function<void(std::vector<std::string> &) 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 };
index f2e6d54..e82c6e1 100644 (file)
@@ -263,6 +263,36 @@ struct FragmentCompiler {
         });
       });
     }
+
+    if (F.CompilationDatabase) {
+      llvm::Optional<Config::CDBSearchSpec> 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) {
index c491ec5..5b67c49 100644 (file)
@@ -151,6 +151,13 @@ struct Fragment {
     ///
     /// Flags added by the same CompileFlags entry will not be removed.
     std::vector<Located<std::string>> 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<Located<std::string>> CompilationDatabase;
   };
   CompileFlagsBlock CompileFlags;
 
index 20cdc0b..7aaff55 100644 (file)
@@ -95,6 +95,9 @@ private:
       if (auto Values = scalarValues(N))
         F.Remove = std::move(*Values);
     });
+    Dict.handle("CompilationDatabase", [&](Node &N) {
+      F.CompilationDatabase = scalarValue(N, "CompilationDatabase");
+    });
     Dict.parse(N);
   }
 
index 3ee2d2d..1a5379a 100644 (file)
@@ -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 @@ bool DirectoryBasedGlobalCompilationDatabase::DirectoryCache::load(
 DirectoryBasedGlobalCompilationDatabase::
     DirectoryBasedGlobalCompilationDatabase(const Options &Opts)
     : Opts(Opts), Broadcaster(std::make_unique<BroadcastThread>(*this)) {
-  if (Opts.CompileCommandsDir)
-    OnlyDirCache = std::make_unique<DirectoryCache>(*Opts.CompileCommandsDir);
+  if (!this->Opts.ContextProvider)
+    this->Opts.ContextProvider = [](llvm::StringRef) {
+      return Context::current().clone();
+    };
 }
 
 DirectoryBasedGlobalCompilationDatabase::
@@ -405,14 +409,6 @@ static std::string maybeCaseFoldPath(PathRef Path) {
 #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::DirectoryCache *>
 DirectoryBasedGlobalCompilationDatabase::getDirectoryCaches(
     llvm::ArrayRef<llvm::StringRef> Dirs) const {
@@ -441,31 +437,42 @@ DirectoryBasedGlobalCompilationDatabase::lookupCDB(
   assert(llvm::sys::path::is_absolute(Request.FileName) &&
          "path must be absolute");
 
+  std::string Storage;
+  std::vector<llvm::StringRef> 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<const tooling::CompilationDatabase> CDB = nullptr;
   bool ShouldBroadcast = false;
   DirectoryCache *DirCache = nullptr;
-  std::shared_ptr<const tooling::CompilationDatabase> 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<llvm::StringRef> 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 @@ public:
   }
 };
 
-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<std::string> 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<DirInfo> Dirs;
+
+  // A search path starts at a directory, and either includes ancestors or not.
+  using SearchPath = llvm::PointerIntPair<DirInfo *, 1>;
+
+  // 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<bool> DirectoryHasCDB;
-  std::vector<llvm::StringRef> 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<llvm::StringRef> DirKeys;
+    std::vector<DirInfo *> 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<std::string> 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<std::string> filter(std::vector<std::string> AllFiles,
+                                  std::atomic<bool> &ShouldStop) {
+    std::vector<std::string> 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<SearchPath> 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<std::string> GovernedFiles =
+      Filter(T.PI.SourceRoot, Parent).filter(T.CDB->getAllFiles(), ShouldStop);
+  if (!GovernedFiles.empty())
+    Parent.OnCommandChanged.broadcast(std::move(GovernedFiles));
 }
 
 void DirectoryBasedGlobalCompilationDatabase::broadcastCDB(
index e519ff6..b6e2fa3 100644 (file)
@@ -103,7 +103,10 @@ public:
     // (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<Context(llvm::StringRef)> ContextProvider;
     // Only look for a compilation database in this one fixed directory.
+    // FIXME: fold this into config/context mechanism.
     llvm::Optional<Path> CompileCommandsDir;
   };
 
@@ -126,14 +129,9 @@ private:
   Options Opts;
 
   class DirectoryCache;
-  // If there's an explicit CompileCommandsDir, cache of the CDB found there.
-  mutable std::unique_ptr<DirectoryCache> 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<DirectoryCache> DirCaches;
-  // DirCaches access must be locked (unlike OnlyDirCache, which is threadsafe).
   mutable std::mutex DirCachesMutex;
 
   std::vector<DirectoryCache *>
index 8cd8764..561ef71 100644 (file)
@@ -194,6 +194,33 @@ TEST_F(LSPTest, IncomingCalls) {
   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
index dde8e64..ef24b5d 100644 (file)
@@ -112,6 +112,46 @@ TEST_F(ConfigCompileTests, CompileCommands) {
   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());
index 409e495..7c62955 100644 (file)
@@ -8,6 +8,7 @@
 
 #include "GlobalCompilationDatabase.h"
 
+#include "Config.h"
 #include "Matchers.h"
 #include "TestFS.h"
 #include "support/Path.h"
@@ -205,10 +206,12 @@ TEST(GlobalCompilationDatabaseTest, DiscoveryWithNestedCDBs) {
       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<std::string> DiscoveredFiles;
     auto Sub =
@@ -227,8 +230,53 @@ TEST(GlobalCompilationDatabaseTest, DiscoveryWithNestedCDBs) {
     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<std::string> DiscoveredFiles;
+    auto Sub =
+        DB.watch([&DiscoveredFiles](const std::vector<std::string> 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 @@ TEST(GlobalCompilationDatabaseTest, CompileFlagsDirectory) {
   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<std::string> DiscoveredFiles;