[clang-tools-extra] [clang-tidy] Fix llvm-header-guard for arbitrary worktree names (PR #206000)
Jeff Bailey via cfe-commits
cfe-commits at lists.llvm.org
Fri Jun 26 08:00:34 PDT 2026
https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/206000
>From 305cfad694a8fcf86692ec5170005286bcdbfa8f Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 25 Jun 2026 15:45:14 +0100
Subject: [PATCH 1/3] [clang-tidy] Fix llvm-header-guard for arbitrary worktree
names
The llvm-header-guard check failed to find the project root when
working in git worktrees with names other than llvm-project or llvm.
When `/llvm-project/` is not in the path, we now walk up the directory
tree to find the project root by checking for a `.git` file/directory.
The path is then canonicalized to start with `/llvm-project/` before
calculating the header guard.
Assisted-by: Automated tooling, human reviewed.
---
.../clang-tidy/llvm/HeaderGuardCheck.cpp | 42 ++++++++-
.../clang-tidy/llvm/HeaderGuardCheck.h | 3 +
.../unittests/clang-tidy/LLVMModuleTest.cpp | 90 +++++++++++++++++++
3 files changed, 134 insertions(+), 1 deletion(-)
diff --git a/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.cpp b/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.cpp
index ef8b6b1dfb8f7..f34ce4e90505f 100644
--- a/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.cpp
+++ b/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.cpp
@@ -8,6 +8,7 @@
#include "HeaderGuardCheck.h"
#include "clang/Tooling/Tooling.h"
+#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
namespace clang::tidy::llvm_check {
@@ -16,9 +17,48 @@ LLVMHeaderGuardCheck::LLVMHeaderGuardCheck(StringRef Name,
ClangTidyContext *Context)
: HeaderGuardCheck(Name, Context) {}
+// Attempt to find the root of the LLVM project monorepo by walking up the
+// directory tree from Filename and looking for a ".git" file/directory.
+// This allows us to find the root even when working in git worktrees with
+// arbitrary names.
+std::string
+LLVMHeaderGuardCheck::findLLVMProjectRoot(StringRef Filename) const {
+ SmallString<256> Path = Filename;
+ SmallString<256> Parent = llvm::sys::path::parent_path(Path);
+ while (!Parent.empty() && Parent != Path) {
+ Path = Parent;
+
+ // Check for .git (file or directory) which indicates the root of the
+ // git repository or worktree.
+ SmallString<256> GitPath = Path;
+ llvm::sys::path::append(GitPath, ".git");
+ if (llvm::sys::fs::exists(GitPath))
+ return std::string(Path);
+
+ Parent = llvm::sys::path::parent_path(Path);
+ }
+ return "";
+}
+
std::string LLVMHeaderGuardCheck::getHeaderGuard(StringRef Filename,
StringRef OldGuard) {
- std::string Guard = tooling::getAbsolutePath(Filename);
+ const std::string AbsolutePath = tooling::getAbsolutePath(Filename);
+ std::string Guard = AbsolutePath;
+
+ // Check for "/llvm-project/" using a path with normalized slashes to ensure
+ // Windows paths (which use backslashes) are matched correctly.
+ const std::string CanonicalPath =
+ llvm::sys::path::convert_to_slash(AbsolutePath);
+ if (!StringRef(CanonicalPath).contains("/llvm-project/")) {
+ const std::string Root = findLLVMProjectRoot(AbsolutePath);
+ if (!Root.empty()) {
+ StringRef RelativePath = StringRef(AbsolutePath).substr(Root.size());
+ if (!RelativePath.empty() &&
+ llvm::sys::path::is_separator(RelativePath.front()))
+ RelativePath = RelativePath.drop_front();
+ Guard = ("/llvm-project/" + RelativePath).str();
+ }
+ }
// When running under Windows, need to convert the path separators from
// `\` to `/`.
diff --git a/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.h b/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.h
index cfc920bb85e23..20d8014710531 100644
--- a/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.h
+++ b/clang-tools-extra/clang-tidy/llvm/HeaderGuardCheck.h
@@ -22,6 +22,9 @@ class LLVMHeaderGuardCheck : public utils::HeaderGuardCheck {
bool shouldSuggestEndifComment(StringRef Filename) override { return false; }
std::string getHeaderGuard(StringRef Filename, StringRef OldGuard) override;
+
+protected:
+ virtual std::string findLLVMProjectRoot(StringRef Filename) const;
};
} // namespace clang::tidy::llvm_check
diff --git a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
index e1338c6ef1d5f..9638a116ec904 100644
--- a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
+++ b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
@@ -60,6 +60,24 @@ runHeaderGuardCheckWithEndif(StringRef Code, const Twine &Filename,
std::optional<StringRef> ExpectedWarning) {
return runCheck<WithEndifComment>(Code, Filename, std::move(ExpectedWarning));
}
+static std::string GlobalMockRoot;
+
+struct MockLLVMHeaderGuardCheck : public LLVMHeaderGuardCheck {
+ MockLLVMHeaderGuardCheck(StringRef Name, ClangTidyContext *Context)
+ : LLVMHeaderGuardCheck(Name, Context) {}
+ std::string findLLVMProjectRoot(StringRef Filename) const override {
+ return GlobalMockRoot;
+ }
+};
+
+static std::string
+runMockHeaderGuardCheck(StringRef Code, const Twine &Filename,
+ std::optional<StringRef> ExpectedWarning,
+ std::string MockRoot) {
+ GlobalMockRoot = MockRoot;
+ return runCheck<MockLLVMHeaderGuardCheck>(Code, Filename,
+ std::move(ExpectedWarning));
+}
} // namespace
TEST(LLVMHeaderGuardCheckTest, FixHeaderGuards) {
@@ -297,6 +315,78 @@ TEST(LLVMHeaderGuardCheckTest, FixHeaderGuards) {
"", "\\\\?\\C:\\llvm-project\\clang-tools-extra\\clangd\\foo.h",
StringRef("header is missing header guard")));
#endif
+
+ // Test path outside any git repo (real root detection should fail and not
+ // hang).
+ EXPECT_EQ("#ifndef NON_EXISTENT_DIRECTORY_123_X_H\n"
+ "#define NON_EXISTENT_DIRECTORY_123_X_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runHeaderGuardCheck("", "/non_existent_directory_123/x.h",
+ StringRef("header is missing header guard")));
+
+ // Test single-component absolute path (loop boundary, doesn't hang).
+ EXPECT_EQ("#ifndef X_H\n"
+ "#define X_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runHeaderGuardCheck("", "/x.h",
+ StringRef("header is missing header guard")));
+}
+
+TEST(LLVMHeaderGuardCheckTest, MockedProjectRoot) {
+ EXPECT_EQ("#ifndef LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "#define LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runMockHeaderGuardCheck(
+ "", "/my/mock/root/libc/src/string/strlen.h",
+ StringRef("header is missing header guard"), "/my/mock/root"));
+
+ EXPECT_EQ(
+ "#ifndef SOME_OTHER_PATH_LIBC_SRC_STRING_STRLEN_H\n"
+ "#define SOME_OTHER_PATH_LIBC_SRC_STRING_STRLEN_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runMockHeaderGuardCheck("", "/some/other/path/libc/src/string/strlen.h",
+ StringRef("header is missing header guard"), ""));
+
+ EXPECT_EQ("#ifndef LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "#define LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runMockHeaderGuardCheck(
+ "", "/path/to/llvm-project/libc/src/string/strlen.h",
+ StringRef("header is missing header guard"),
+ "/different/mock/root"));
+
+ // Test when root has a trailing slash.
+ EXPECT_EQ("#ifndef LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "#define LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runMockHeaderGuardCheck(
+ "", "/my/mock/root/libc/src/string/strlen.h",
+ StringRef("header is missing header guard"), "/my/mock/root/"));
+
+#ifdef WIN32
+ // Test Windows-style paths through the new code path (mocked root).
+ EXPECT_EQ("#ifndef LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "#define LLVM_LIBC_SRC_STRING_STRLEN_H\n"
+ "\n"
+ "\n"
+ "#endif\n",
+ runMockHeaderGuardCheck(
+ "", "C:\\my\\mock\\root\\libc\\src\\string\\strlen.h",
+ StringRef("header is missing header guard"),
+ "C:\\my\\mock\\root"));
+#endif
}
TEST(IncludeOrderCheck, GTestHeaders) {
>From 0e8ec797f94ee934255a289e5b30f226589c70cc Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 26 Jun 2026 15:17:02 +0100
Subject: [PATCH 2/3] [clang-tidy] Guard non-Windows absolute path tests in
LLVMModuleTest
The tests for paths outside any git repo and single-component absolute
paths use absolute paths without 'llvm-project' or 'llvm' in them.
On Windows, getAbsolutePath prepends the drive letter to these paths
(since they start with '/' but lack drive letter, making them
root-relative). This drive letter is not truncated by the legacy
logic (as they don't contain 'llvm-project' or 'llvm'), resulting
in guards starting with the drive letter (e.g. 'C_'), which fails the
assertions.
Guarded these tests with #ifndef WIN32 to skip them on Windows, as
we already have MockedProjectRoot tests to verify Windows-style path
handling.
---
clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp | 2 ++
1 file changed, 2 insertions(+)
diff --git a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
index 9638a116ec904..3614fdb37eef0 100644
--- a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
+++ b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
@@ -316,6 +316,7 @@ TEST(LLVMHeaderGuardCheckTest, FixHeaderGuards) {
StringRef("header is missing header guard")));
#endif
+#ifndef WIN32
// Test path outside any git repo (real root detection should fail and not
// hang).
EXPECT_EQ("#ifndef NON_EXISTENT_DIRECTORY_123_X_H\n"
@@ -334,6 +335,7 @@ TEST(LLVMHeaderGuardCheckTest, FixHeaderGuards) {
"#endif\n",
runHeaderGuardCheck("", "/x.h",
StringRef("header is missing header guard")));
+#endif
}
TEST(LLVMHeaderGuardCheckTest, MockedProjectRoot) {
>From dc1aab6ccc7b67bbf8018c091f507759c1317baa Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 26 Jun 2026 15:55:13 +0100
Subject: [PATCH 3/3] [clang-tidy] Use _WIN32 for new Windows guards (#206000)
Replaced the legacy WIN32 macro with the standard compiler-defined
_WIN32 macro for the new Windows-specific guards in
LLVMModuleTest.cpp. This ensures that the non-Windows-compatible
absolute path tests are correctly skipped on Windows CI bots.
Assisted-by: Automated tooling, human reviewed.
---
clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
index 3614fdb37eef0..d2ec4bcb145e6 100644
--- a/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
+++ b/clang-tools-extra/unittests/clang-tidy/LLVMModuleTest.cpp
@@ -316,7 +316,7 @@ TEST(LLVMHeaderGuardCheckTest, FixHeaderGuards) {
StringRef("header is missing header guard")));
#endif
-#ifndef WIN32
+#ifndef _WIN32
// Test path outside any git repo (real root detection should fail and not
// hang).
EXPECT_EQ("#ifndef NON_EXISTENT_DIRECTORY_123_X_H\n"
@@ -377,7 +377,7 @@ TEST(LLVMHeaderGuardCheckTest, MockedProjectRoot) {
"", "/my/mock/root/libc/src/string/strlen.h",
StringRef("header is missing header guard"), "/my/mock/root/"));
-#ifdef WIN32
+#ifdef _WIN32
// Test Windows-style paths through the new code path (mocked root).
EXPECT_EQ("#ifndef LLVM_LIBC_SRC_STRING_STRLEN_H\n"
"#define LLVM_LIBC_SRC_STRING_STRLEN_H\n"
More information about the cfe-commits
mailing list