[clang] [clang-tools-extra] [Tooling] Resolve relative "directory" against the compilation database (PR #235400)

via cfe-commits cfe-commits at lists.llvm.org
Sat Oct 24 23:02:12 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-tools-extra

Author: Fangrui Song (MaskRay)

<details>
<summary>Changes</summary>

JSONCompilationDatabase does not resolve a relative "directory", e.g.
`{"directory": ".", "command": "cc ../src/a.c", "file": "../src/a.c"}`.
getCompileCommands finds nothing, so clang-check reports "Compile
command not found" and clangd uses a fallback command.
getAllCompileCommands returns the directory verbatim, so each consumer
resolves it against its own working directory.

Resolve it against the directory containing compile_commands.json,
after following symlinks, so that a compile_commands.json symlinked from
the source root into the build directory keeps its meaning. Since
loadFromBuffer has no file path, it takes the base directory as a new
first parameter.

LLM-aided

---
Full diff: https://github.com/llvm/llvm-project/pull/235400.diff


7 Files Affected:

- (modified) clang-tools-extra/clangd/GlobalCompilationDatabase.cpp (+13-5) 
- (modified) clang/docs/JSONCompilationDatabase.md (+1) 
- (modified) clang/docs/ReleaseNotes.md (+2) 
- (modified) clang/include/clang/Tooling/JSONCompilationDatabase.h (+16-11) 
- (modified) clang/lib/Tooling/JSONCompilationDatabase.cpp (+24-13) 
- (modified) clang/unittests/Tooling/CompilationDatabaseTest.cpp (+63-5) 
- (modified) clang/unittests/Tooling/ToolingTest.cpp (+1-1) 


``````````diff
diff --git a/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp b/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
index adb771ecbbaada..7e12a040345a17 100644
--- a/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
+++ b/clang-tools-extra/clangd/GlobalCompilationDatabase.cpp
@@ -247,9 +247,15 @@ DirectoryBasedGlobalCompilationDatabase::DirectoryCache::CachedFile::load(
 
 // Adapt CDB-loading functions to a common interface for DirectoryCache::load().
 static std::unique_ptr<tooling::CompilationDatabase>
-parseJSON(PathRef Path, llvm::StringRef Data, std::string &Error) {
+parseJSON(PathRef Path, llvm::StringRef Data, llvm::vfs::FileSystem &FS,
+          std::string &Error) {
+  llvm::SmallString<256> RealPath;
+  llvm::StringRef BaseDirectory;
+  if (!FS.getRealPath(Path, RealPath))
+    BaseDirectory = llvm::sys::path::parent_path(RealPath);
   if (auto CDB = tooling::JSONCompilationDatabase::loadFromBuffer(
-          Data, Error, tooling::JSONCommandLineSyntax::AutoDetect)) {
+          BaseDirectory, Data, Error,
+          tooling::JSONCommandLineSyntax::AutoDetect)) {
     // FS used for expanding response files.
     // FIXME: ExpandResponseFilesDatabase appears not to provide the usual
     // thread-safety guarantees, as the access to FS is not locked!
@@ -264,7 +270,8 @@ parseJSON(PathRef Path, llvm::StringRef Data, std::string &Error) {
   return nullptr;
 }
 static std::unique_ptr<tooling::CompilationDatabase>
-parseFixed(PathRef Path, llvm::StringRef Data, std::string &Error) {
+parseFixed(PathRef Path, llvm::StringRef Data, llvm::vfs::FileSystem &,
+           std::string &Error) {
   return tooling::FixedCompilationDatabase::loadFromBuffer(
       llvm::sys::path::parent_path(Path), Data, Error);
 }
@@ -283,7 +290,7 @@ bool DirectoryBasedGlobalCompilationDatabase::DirectoryCache::load(
     // Wrapper for {Fixed,JSON}CompilationDatabase::loadFromBuffer.
     std::unique_ptr<tooling::CompilationDatabase> (*Parser)(
         PathRef,
-        /*Data*/ llvm::StringRef,
+        /*Data*/ llvm::StringRef, llvm::vfs::FileSystem &,
         /*ErrorMsg*/ std::string &);
   };
   for (const auto &Entry : {CDBFile{&CompileCommandsJson, parseJSON},
@@ -309,7 +316,8 @@ bool DirectoryBasedGlobalCompilationDatabase::DirectoryCache::load(
       return true;
     case CachedFile::LoadResult::FoundNewData:
       // We have a new CDB!
-      CDB = Entry.Parser(Entry.File->Path, Loaded.Buffer->getBuffer(), Error);
+      CDB =
+          Entry.Parser(Entry.File->Path, Loaded.Buffer->getBuffer(), FS, Error);
       if (CDB)
         log("{0} compilation database from {1}", Active ? "Reloaded" : "Loaded",
             Entry.File->Path);
diff --git a/clang/docs/JSONCompilationDatabase.md b/clang/docs/JSONCompilationDatabase.md
index 35fdea8fc3b1a5..649d164d2d52d3 100644
--- a/clang/docs/JSONCompilationDatabase.md
+++ b/clang/docs/JSONCompilationDatabase.md
@@ -76,6 +76,7 @@ The contracts for each field in the command object are:
 - **directory:** The working directory of the compilation. All paths
   specified in the **command** or **file** fields must be either
   absolute or relative to this directory.
+  A relative **directory** is relative to the directory containing the compilation database file, after resolving symlinks.
 
 - **file:** The main translation unit source processed by this
   compilation step. This is used by tools as the key into the
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 44f30666457f94..e7ee0001f4e4a3 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -312,6 +312,8 @@ documentation](CIR/index.md) for more information regarding ClangIR's status and
 - Clang tools now resolve tool names without a path in compilation databases
   through `PATH`.
 
+- A relative `directory` in a JSON compilation database is now resolved against the directory containing the compilation database file, after resolving symlinks.
+
 - On musl targets, the driver now links ``libssp_nonshared.a`` when stack
   protection is enabled and the library is present in the toolchain library
   paths, matching what musl distributions configure GCC to do.
diff --git a/clang/include/clang/Tooling/JSONCompilationDatabase.h b/clang/include/clang/Tooling/JSONCompilationDatabase.h
index 96582457c63d58..8350ca6a65d134 100644
--- a/clang/include/clang/Tooling/JSONCompilationDatabase.h
+++ b/clang/include/clang/Tooling/JSONCompilationDatabase.h
@@ -20,6 +20,7 @@
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/StringMap.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSet.h"
 #include "llvm/Support/MemoryBuffer.h"
 #include "llvm/Support/SourceMgr.h"
 #include "llvm/Support/YAMLParser.h"
@@ -60,7 +61,9 @@ namespace tooling {
 enum class JSONCommandLineSyntax { Windows, Gnu, AutoDetect };
 class JSONCompilationDatabase : public CompilationDatabase {
 public:
-  /// Loads a JSON compilation database from the specified file.
+  /// Loads a JSON compilation database from the specified file. A relative
+  /// "directory" is resolved against the directory containing the file, after
+  /// resolving symlinks.
   ///
   /// Returns NULL and sets ErrorMessage if the database could not be
   /// loaded from the given file.
@@ -68,12 +71,13 @@ class JSONCompilationDatabase : public CompilationDatabase {
   loadFromFile(StringRef FilePath, std::string &ErrorMessage,
                JSONCommandLineSyntax Syntax);
 
-  /// Loads a JSON compilation database from a data buffer.
+  /// Loads a JSON compilation database from a data buffer. If BaseDirectory
+  /// is non-empty, a relative "directory" is resolved against it.
   ///
   /// Returns NULL and sets ErrorMessage if the database could not be loaded.
   static std::unique_ptr<JSONCompilationDatabase>
-  loadFromBuffer(StringRef DatabaseString, std::string &ErrorMessage,
-                 JSONCommandLineSyntax Syntax);
+  loadFromBuffer(StringRef BaseDirectory, StringRef DatabaseString,
+                 std::string &ErrorMessage, JSONCommandLineSyntax Syntax);
 
   /// Returns all compile commands in which the specified file was
   /// compiled.
@@ -103,19 +107,19 @@ class JSONCompilationDatabase : public CompilationDatabase {
   ///
   /// Returns whether parsing succeeded. Sets ErrorMessage if parsing
   /// failed.
-  bool parse(std::string &ErrorMessage);
+  bool parse(StringRef BaseDirectory, std::string &ErrorMessage);
 
-  // Tuple (directory, filename, commandline, output) where 'commandline'
-  // points to the corresponding scalar nodes in the YAML stream.
+  // Tuple (directory, filename, commandline, output) where 'directory' is
+  // owned by Directories and the others point to the corresponding scalar
+  // nodes in the YAML stream.
   // If the command line contains a single argument, it is a shell-escaped
   // command line.
   // Otherwise, each entry in the command line vector is a literal
   // argument to the compiler.
   // The output field may be a nullptr.
-  using CompileCommandRef =
-      std::tuple<llvm::yaml::ScalarNode *, llvm::yaml::ScalarNode *,
-                 std::vector<llvm::yaml::ScalarNode *>,
-                 llvm::yaml::ScalarNode *>;
+  using CompileCommandRef = std::tuple<StringRef, llvm::yaml::ScalarNode *,
+                                       std::vector<llvm::yaml::ScalarNode *>,
+                                       llvm::yaml::ScalarNode *>;
 
   /// Converts the given array of CompileCommandRefs to CompileCommands.
   void getCommands(ArrayRef<CompileCommandRef> CommandsRef,
@@ -130,6 +134,7 @@ class JSONCompilationDatabase : public CompilationDatabase {
 
   FileMatchTrie MatchTrie;
 
+  llvm::StringSet<> Directories;
   std::unique_ptr<llvm::MemoryBuffer> Database;
   JSONCommandLineSyntax Syntax;
   llvm::SourceMgr SM;
diff --git a/clang/lib/Tooling/JSONCompilationDatabase.cpp b/clang/lib/Tooling/JSONCompilationDatabase.cpp
index 0efa75970d9864..5f54a74b308161 100644
--- a/clang/lib/Tooling/JSONCompilationDatabase.cpp
+++ b/clang/lib/Tooling/JSONCompilationDatabase.cpp
@@ -197,22 +197,27 @@ JSONCompilationDatabase::loadFromFile(StringRef FilePath,
     ErrorMessage = "Error while opening JSON database: " + Result.message();
     return nullptr;
   }
+  SmallString<128> RealPath;
+  StringRef BaseDirectory;
+  if (!llvm::sys::fs::real_path(FilePath, RealPath))
+    BaseDirectory = llvm::sys::path::parent_path(RealPath);
   std::unique_ptr<JSONCompilationDatabase> Database(
       new JSONCompilationDatabase(std::move(*DatabaseBuffer), Syntax));
-  if (!Database->parse(ErrorMessage))
+  if (!Database->parse(BaseDirectory, ErrorMessage))
     return nullptr;
   return Database;
 }
 
 std::unique_ptr<JSONCompilationDatabase>
-JSONCompilationDatabase::loadFromBuffer(StringRef DatabaseString,
+JSONCompilationDatabase::loadFromBuffer(StringRef BaseDirectory,
+                                        StringRef DatabaseString,
                                         std::string &ErrorMessage,
                                         JSONCommandLineSyntax Syntax) {
   std::unique_ptr<llvm::MemoryBuffer> DatabaseBuffer(
       llvm::MemoryBuffer::getMemBufferCopy(DatabaseString));
   std::unique_ptr<JSONCompilationDatabase> Database(
       new JSONCompilationDatabase(std::move(DatabaseBuffer), Syntax));
-  if (!Database->parse(ErrorMessage))
+  if (!Database->parse(BaseDirectory, ErrorMessage))
     return nullptr;
   return Database;
 }
@@ -308,19 +313,18 @@ void JSONCompilationDatabase::getCommands(
     ArrayRef<CompileCommandRef> CommandsRef,
     std::vector<CompileCommand> &Commands) const {
   for (const auto &CommandRef : CommandsRef) {
-    SmallString<8> DirectoryStorage;
     SmallString<32> FilenameStorage;
     SmallString<32> OutputStorage;
     auto Output = std::get<3>(CommandRef);
-    Commands.emplace_back(
-        std::get<0>(CommandRef)->getValue(DirectoryStorage),
-        std::get<1>(CommandRef)->getValue(FilenameStorage),
-        nodeToCommandLine(Syntax, std::get<2>(CommandRef)),
-        Output ? Output->getValue(OutputStorage) : "");
+    Commands.emplace_back(std::get<0>(CommandRef),
+                          std::get<1>(CommandRef)->getValue(FilenameStorage),
+                          nodeToCommandLine(Syntax, std::get<2>(CommandRef)),
+                          Output ? Output->getValue(OutputStorage) : "");
   }
 }
 
-bool JSONCompilationDatabase::parse(std::string &ErrorMessage) {
+bool JSONCompilationDatabase::parse(StringRef BaseDirectory,
+                                    std::string &ErrorMessage) {
   llvm::yaml::document_iterator I = YAMLStream.begin();
   if (I == YAMLStream.end()) {
     ErrorMessage = "Error while parsing YAML.";
@@ -409,19 +413,26 @@ bool JSONCompilationDatabase::parse(std::string &ErrorMessage) {
       ErrorMessage = "Missing key: \"directory\".";
       return false;
     }
+    SmallString<8> DirectoryStorage;
+    SmallString<128> DirectoryPath(Directory->getValue(DirectoryStorage));
+    if (!BaseDirectory.empty() && !DirectoryPath.empty() &&
+        !llvm::sys::path::is_absolute(DirectoryPath)) {
+      llvm::sys::path::make_absolute(BaseDirectory, DirectoryPath);
+      llvm::sys::path::remove_dots(DirectoryPath);
+    }
+    StringRef DirectoryName = Directories.insert(DirectoryPath).first->getKey();
     SmallString<8> FileStorage;
     StringRef FileName = File->getValue(FileStorage);
     SmallString<128> NativeFilePath;
     if (llvm::sys::path::is_relative(FileName)) {
-      SmallString<8> DirectoryStorage;
-      SmallString<128> AbsolutePath(Directory->getValue(DirectoryStorage));
+      SmallString<128> AbsolutePath(DirectoryName);
       llvm::sys::path::append(AbsolutePath, FileName);
       llvm::sys::path::native(AbsolutePath, NativeFilePath);
     } else {
       llvm::sys::path::native(FileName, NativeFilePath);
     }
     llvm::sys::path::remove_dots(NativeFilePath, /*remove_dot_dot=*/true);
-    auto Cmd = CompileCommandRef(Directory, File, *Command, Output);
+    auto Cmd = CompileCommandRef(DirectoryName, File, *Command, Output);
     IndexByFile[NativeFilePath].push_back(Cmd);
     AllCommands.push_back(Cmd);
     MatchTrie.insert(NativeFilePath);
diff --git a/clang/unittests/Tooling/CompilationDatabaseTest.cpp b/clang/unittests/Tooling/CompilationDatabaseTest.cpp
index d571f992e763e6..5ce5223f0a4be7 100644
--- a/clang/unittests/Tooling/CompilationDatabaseTest.cpp
+++ b/clang/unittests/Tooling/CompilationDatabaseTest.cpp
@@ -12,6 +12,7 @@
 #include "clang/Tooling/Tooling.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/TargetSelect.h"
+#include "llvm/Testing/Support/SupportHelpers.h"
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <algorithm>
@@ -27,8 +28,8 @@ using testing::UnorderedElementsAreArray;
 static void expectFailure(StringRef JSONDatabase, StringRef Explanation) {
   std::string ErrorMessage;
   EXPECT_EQ(nullptr,
-            JSONCompilationDatabase::loadFromBuffer(JSONDatabase, ErrorMessage,
-                                                    JSONCommandLineSyntax::Gnu))
+            JSONCompilationDatabase::loadFromBuffer(
+                "", JSONDatabase, ErrorMessage, JSONCommandLineSyntax::Gnu))
       << "Expected an error because of: " << Explanation.str();
 }
 
@@ -56,7 +57,7 @@ static std::vector<std::string> getAllFiles(StringRef JSONDatabase,
                                             std::string &ErrorMessage,
                                             JSONCommandLineSyntax Syntax) {
   std::unique_ptr<CompilationDatabase> Database(
-      JSONCompilationDatabase::loadFromBuffer(JSONDatabase, ErrorMessage,
+      JSONCompilationDatabase::loadFromBuffer("", JSONDatabase, ErrorMessage,
                                               Syntax));
   if (!Database) {
     ADD_FAILURE() << ErrorMessage;
@@ -71,7 +72,7 @@ static std::vector<CompileCommand>
 getAllCompileCommands(JSONCommandLineSyntax Syntax, StringRef JSONDatabase,
                       std::string &ErrorMessage) {
   std::unique_ptr<CompilationDatabase> Database(
-      JSONCompilationDatabase::loadFromBuffer(JSONDatabase, ErrorMessage,
+      JSONCompilationDatabase::loadFromBuffer("", JSONDatabase, ErrorMessage,
                                               Syntax));
   if (!Database) {
     ADD_FAILURE() << ErrorMessage;
@@ -193,7 +194,7 @@ static CompileCommand findCompileArgsInJsonDatabase(StringRef FileName,
                                                     std::string JSONDatabase,
                                                     std::string &ErrorMessage) {
   std::unique_ptr<CompilationDatabase> Database(
-      JSONCompilationDatabase::loadFromBuffer(JSONDatabase, ErrorMessage,
+      JSONCompilationDatabase::loadFromBuffer("", JSONDatabase, ErrorMessage,
                                               JSONCommandLineSyntax::Gnu));
   if (!Database)
     return CompileCommand();
@@ -225,6 +226,63 @@ TEST(JSONCompilationDatabase, ArgumentsPreferredOverCommand) {
    EXPECT_EQ(Arguments, FoundCommand.CommandLine[0]) << ErrorMessage;
 }
 
+static std::string native(StringRef Path) {
+  SmallString<32> Result;
+  llvm::sys::path::native(Path, Result);
+  return std::string(Result);
+}
+
+TEST(JSONCompilationDatabase, RelativeDirectory) {
+  StringRef JSONDatabase = R"([
+    {"directory": ".", "command": "cc a.cc", "file": "a.cc"},
+    {"directory": "../out", "command": "cc b.cc", "file": "b.cc"},
+    {"directory": "//net/abs/.", "command": "cc c.cc", "file": "c.cc"},
+    {"directory": "", "command": "cc d.cc", "file": "d.cc"}
+  ])";
+  std::string ErrorMessage;
+  std::unique_ptr<JSONCompilationDatabase> Database =
+      JSONCompilationDatabase::loadFromBuffer(
+          "//net/db", JSONDatabase, ErrorMessage, JSONCommandLineSyntax::Gnu);
+  ASSERT_TRUE(Database) << ErrorMessage;
+  std::vector<CompileCommand> Commands = Database->getAllCompileCommands();
+  ASSERT_EQ(4u, Commands.size());
+  EXPECT_EQ(native("//net/db"), Commands[0].Directory);
+  EXPECT_EQ(native("//net/db/../out"), Commands[1].Directory);
+  EXPECT_EQ("//net/abs/.", Commands[2].Directory);
+  EXPECT_EQ("", Commands[3].Directory);
+  EXPECT_EQ(1u, Database->getCompileCommands("//net/out/b.cc").size());
+
+  Database = JSONCompilationDatabase::loadFromBuffer(
+      "", JSONDatabase, ErrorMessage, JSONCommandLineSyntax::Gnu);
+  ASSERT_TRUE(Database) << ErrorMessage;
+  EXPECT_EQ(".", Database->getAllCompileCommands()[0].Directory);
+}
+
+TEST(JSONCompilationDatabase, RelativeDirectoryFromFile) {
+  llvm::unittest::TempDir Dir("relative-directory", /*Unique=*/true);
+  llvm::unittest::TempDir Build(Dir.path("build"));
+  llvm::unittest::TempFile File(
+      Build.path("compile_commands.json"), "",
+      R"([{"directory": ".", "command": "cc a.cc", "file": "../a.cc"}])");
+  auto Check = [&](StringRef DatabasePath) {
+    std::string ErrorMessage;
+    std::unique_ptr<JSONCompilationDatabase> Database =
+        JSONCompilationDatabase::loadFromFile(DatabasePath, ErrorMessage,
+                                              JSONCommandLineSyntax::Gnu);
+    ASSERT_TRUE(Database) << ErrorMessage;
+    std::vector<CompileCommand> Commands =
+        Database->getCompileCommands(Dir.path("a.cc"));
+    ASSERT_EQ(1u, Commands.size());
+    EXPECT_EQ(Build.path(), Commands[0].Directory);
+  };
+  Check(File.path());
+#ifndef _WIN32
+  // A symlink to the database does not change the base directory.
+  llvm::unittest::TempLink Link(File.path(), Dir.path("compile_commands.json"));
+  Check(Link.path());
+#endif
+}
+
 struct FakeComparator : public PathComparator {
   ~FakeComparator() override {}
   bool equivalent(StringRef FileA, StringRef FileB) const override {
diff --git a/clang/unittests/Tooling/ToolingTest.cpp b/clang/unittests/Tooling/ToolingTest.cpp
index 11a5491dd35fb1..96240f2bbcf26a 100644
--- a/clang/unittests/Tooling/ToolingTest.cpp
+++ b/clang/unittests/Tooling/ToolingTest.cpp
@@ -1096,7 +1096,7 @@ std::string runToolWithProgress(llvm::ArrayRef<TestCommand> Commands,
   OS << llvm::json::Value(std::move(Entries));
 
   std::unique_ptr<CompilationDatabase> Database(
-      JSONCompilationDatabase::loadFromBuffer(DatabaseContent, ErrorMessage,
+      JSONCompilationDatabase::loadFromBuffer("", DatabaseContent, ErrorMessage,
                                               JSONCommandLineSyntax::Gnu));
   if (!Database) {
     ADD_FAILURE() << "Failed to load compilation database: " << ErrorMessage;

``````````

</details>


https://github.com/llvm/llvm-project/pull/235400


More information about the cfe-commits mailing list