[clang] [clang][Lex] warn on backslashes in include paths (PR #186770)
Rose Hudson via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 16 03:36:44 PDT 2026
https://github.com/rosefromthedead created https://github.com/llvm/llvm-project/pull/186770
As part of -Wnonportable-include-path, warn when an include path contains a backslash, and emit a fixit that changes all backslashes to forward slashes.
>From 48a57de3fbd67e14ccd478c26f8db53c2c2eb530 Mon Sep 17 00:00:00 2001
From: Rose Hudson <rose.hudson at sony.com>
Date: Fri, 6 Feb 2026 11:14:37 +0000
Subject: [PATCH] [clang][Lex] warn on backslashes in include paths
As part of -Wnonportable-include-path, warn when an include path contains
a backslash, and emit a fixit that changes all backslashes to forward
slashes.
---
.../include/clang/Basic/DiagnosticLexKinds.td | 4 +--
clang/include/clang/Basic/SourceManager.h | 18 ++++++++++
clang/lib/InstallAPI/Frontend.cpp | 8 +++--
clang/lib/Lex/PPDirectives.cpp | 35 +++++++++++++++++--
clang/test/Lexer/backslash-include-win.c | 14 ++++++++
.../test/Lexer/case-insensitive-include-ms.c | 26 +++++++-------
.../test/Lexer/case-insensitive-include-win.c | 5 +--
7 files changed, 88 insertions(+), 22 deletions(-)
create mode 100644 clang/test/Lexer/backslash-include-win.c
diff --git a/clang/include/clang/Basic/DiagnosticLexKinds.td b/clang/include/clang/Basic/DiagnosticLexKinds.td
index 5eceeced311f2..17789a515d593 100644
--- a/clang/include/clang/Basic/DiagnosticLexKinds.td
+++ b/clang/include/clang/Basic/DiagnosticLexKinds.td
@@ -373,9 +373,7 @@ def ext_missing_whitespace_after_macro_name : ExtWarn<
def warn_missing_whitespace_after_macro_name : Warning<
"whitespace recommended after macro name">;
-class NonportablePath : Warning<
- "non-portable path to file '%0'; specified path differs in case from file"
- " name on disk">;
+class NonportablePath : Warning<"non-portable path to file '%0'; %1">;
def pp_nonportable_path : NonportablePath,
InGroup<DiagGroup<"nonportable-include-path">>;
def pp_nonportable_system_path : NonportablePath, DefaultIgnore,
diff --git a/clang/include/clang/Basic/SourceManager.h b/clang/include/clang/Basic/SourceManager.h
index bc9e97863556d..e097e8c14d331 100644
--- a/clang/include/clang/Basic/SourceManager.h
+++ b/clang/include/clang/Basic/SourceManager.h
@@ -1526,6 +1526,24 @@ class SourceManager : public RefCountedBase<SourceManager> {
return Filename == "<scratch space>";
}
+ /// Returns whether \p Loc is located in a <module-includes> file.
+ bool isWrittenInModuleIncludes(SourceLocation Loc) const {
+ PresumedLoc Presumed = getPresumedLoc(Loc);
+ if (Presumed.isInvalid())
+ return false;
+ StringRef Filename(Presumed.getFilename());
+ return Filename == "<module-includes>";
+ }
+
+ /// Returns whether \p Loc is located in a <extract-api-includes> file.
+ bool isWrittenInExtractAPIIncludes(SourceLocation Loc) const {
+ PresumedLoc Presumed = getPresumedLoc(Loc);
+ if (Presumed.isInvalid())
+ return false;
+ StringRef Filename(Presumed.getFilename());
+ return Filename == "<extract-api-includes>";
+ }
+
/// Returns whether \p Loc is located in a built-in or command line source.
bool isInPredefinedFile(SourceLocation Loc) const {
PresumedLoc Presumed = getPresumedLoc(Loc);
diff --git a/clang/lib/InstallAPI/Frontend.cpp b/clang/lib/InstallAPI/Frontend.cpp
index cce0b19b50619..3168d6143e168 100644
--- a/clang/lib/InstallAPI/Frontend.cpp
+++ b/clang/lib/InstallAPI/Frontend.cpp
@@ -148,8 +148,12 @@ std::unique_ptr<MemoryBuffer> createInputBuffer(InstallAPIContext &Ctx) {
OS << "#import ";
if (H.useIncludeName())
OS << "<" << H.getIncludeName() << ">\n";
- else
- OS << "\"" << H.getPath() << "\"\n";
+ else {
+ // Don't trigger -Wnonportable-include-path
+ std::string Path = H.getPath().str();
+ std::replace(Path.begin(), Path.end(), '\\', '/');
+ OS << "\"" << Path << "\"\n";
+ }
Ctx.addKnownHeader(H);
}
diff --git a/clang/lib/Lex/PPDirectives.cpp b/clang/lib/Lex/PPDirectives.cpp
index 4a854c213926b..0fba46fd90fa0 100644
--- a/clang/lib/Lex/PPDirectives.cpp
+++ b/clang/lib/Lex/PPDirectives.cpp
@@ -2675,6 +2675,8 @@ Preprocessor::ImportAction Preprocessor::HandleHeaderIncludeOrImport(
}
#endif
+ std::optional<SmallString<128>> SuggestedPath;
+ bool HasCaseDifference = false;
if (trySimplifyPath(Components, RealPathName, BackslashStyle)) {
SmallString<128> Path;
Path.reserve(Name.size()+2);
@@ -2719,14 +2721,43 @@ Preprocessor::ImportAction Preprocessor::HandleHeaderIncludeOrImport(
Path = (Path.substr(0, 1) + "\\\\?\\" + Path.substr(1)).str();
#endif
+ SuggestedPath = std::make_optional(std::move(Path));
+ HasCaseDifference = true;
+ }
+
+ bool HasReplaceableBackslash = false;
+ if (Name.contains('\\')) {
+ if (!SuggestedPath.has_value()) {
+ SuggestedPath = std::make_optional(OriginalFilename);
+ }
+ std::replace(SuggestedPath->begin(), SuggestedPath->end(), '\\', '/');
+ HasReplaceableBackslash = true;
+ }
+
+ SourceLocation FilenameLoc = FilenameTok.getLocation();
+ bool SuppressDiag = FilenameLoc.isMacroID() ||
+ SourceMgr.isWrittenInBuiltinFile(FilenameLoc) ||
+ SourceMgr.isWrittenInModuleIncludes(FilenameLoc) ||
+ SourceMgr.isWrittenInExtractAPIIncludes(FilenameLoc);
+ if (SuggestedPath.has_value() && !SuppressDiag) {
// For user files and known standard headers, issue a diagnostic.
// For other system headers, don't. They can be controlled separately.
auto DiagId =
(FileCharacter == SrcMgr::C_User || warnByDefaultOnWrongCase(Name))
? diag::pp_nonportable_path
: diag::pp_nonportable_system_path;
- Diag(FilenameTok, DiagId) << Path <<
- FixItHint::CreateReplacement(FilenameRange, Path);
+ auto Diagnostic =
+ Diag(FilenameTok, DiagId)
+ << SuggestedPath.value()
+ << FixItHint::CreateReplacement(FilenameRange, SuggestedPath.value());
+ if (HasCaseDifference && HasReplaceableBackslash) {
+ Diagnostic << "specified path contains backslashes and differs in case "
+ "from file name on disk";
+ } else if (HasReplaceableBackslash) {
+ Diagnostic << "specified path contains backslashes";
+ } else if (HasCaseDifference) {
+ Diagnostic << "specified path differs in case from file name on disk";
+ }
}
}
diff --git a/clang/test/Lexer/backslash-include-win.c b/clang/test/Lexer/backslash-include-win.c
new file mode 100644
index 0000000000000..e95098a06e1e4
--- /dev/null
+++ b/clang/test/Lexer/backslash-include-win.c
@@ -0,0 +1,14 @@
+// REQUIRES: system-windows
+// RUN: mkdir -p %t/backslash
+// RUN: cp %S/Inputs/case-insensitive-include.h %t/backslash/case-insensitive-include.h
+// RUN: %clang -fsyntax-only -I%t %s 2>&1 | FileCheck %s
+
+#include "backslash\case-insensitive-include.h"
+// CHECK: non-portable path to file
+// CHECK: specified path contains backslashes
+// CHECK: "backslash/case-insensitive-include.h"
+
+#include "backslash\CASE-insensitive-include.h"
+// CHECK: non-portable path to file
+// CHECK: specified path contains backslashes and differs in case from file name on disk
+// CHECK: "backslash/case-insensitive-include.h"
diff --git a/clang/test/Lexer/case-insensitive-include-ms.c b/clang/test/Lexer/case-insensitive-include-ms.c
index f7af1fef8b4e6..8fa73010750d7 100644
--- a/clang/test/Lexer/case-insensitive-include-ms.c
+++ b/clang/test/Lexer/case-insensitive-include-ms.c
@@ -6,17 +6,17 @@
// RUN: %clang_cc1 -fsyntax-only -fms-compatibility %s -include %s -I %t/Output -verify
// RUN: %clang_cc1 -fsyntax-only -fms-compatibility -fdiagnostics-parseable-fixits %s -include %s -I %t/Output 2>&1 | FileCheck %s
-#include "..\Output\.\case-insensitive-include.h"
-#include "..\Output\.\Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
-// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:50}:"\"..\\Output\\.\\case-insensitive-include.h\""
-#include "..\\Output\.\\Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
-// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:52}:"\"..\\\\Output\\.\\\\case-insensitive-include.h\""
-#include "..\output\.\case-insensitive-include.h" // expected-warning {{non-portable path}}
-// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:50}:"\"..\\Output\\.\\case-insensitive-include.h\""
+#include "../Output/./case-insensitive-include.h"
+#include "../Output/./Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
+// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:50}:"\"../Output/./case-insensitive-include.h\""
+#include "..//Output/.//Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
+// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:52}:"\"..//Output/.//case-insensitive-include.h\""
+#include "../output/./case-insensitive-include.h" // expected-warning {{non-portable path}}
+// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:50}:"\"../Output/./case-insensitive-include.h\""
-#include "apath\..\.\case-insensitive-include.h"
-#include "apath\..\.\Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
-// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:49}:"\"apath\\..\\.\\case-insensitive-include.h\""
-#include "apath\\..\\.\\Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
-// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:52}:"\"apath\\\\..\\\\.\\\\case-insensitive-include.h\""
-#include "APath\..\.\case-insensitive-include.h" // For the sake of efficiency, this case is not diagnosed. :-(
+#include "apath/.././case-insensitive-include.h"
+#include "apath/.././Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
+// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:49}:"\"apath/.././case-insensitive-include.h\""
+#include "apath//..//.//Case-Insensitive-Include.h" // expected-warning {{non-portable path}}
+// CHECK: fix-it:"{{.*}}":{[[@LINE-1]]:10-[[@LINE-1]]:52}:"\"apath//..//.//case-insensitive-include.h\""
+#include "APath/.././case-insensitive-include.h" // For the sake of efficiency, this case is not diagnosed. :-(
diff --git a/clang/test/Lexer/case-insensitive-include-win.c b/clang/test/Lexer/case-insensitive-include-win.c
index ed722feb3d994..9a8c3abf16b32 100644
--- a/clang/test/Lexer/case-insensitive-include-win.c
+++ b/clang/test/Lexer/case-insensitive-include-win.c
@@ -11,6 +11,7 @@
// REQUIRES: system-windows
// RUN: mkdir -p %t.dir
// RUN: touch %t.dir/foo.h
-// RUN: not %clang_cl /FI \\?\%{t:real}.dir\FOO.h /WX -fsyntax-only %s 2>&1 | FileCheck %s
+// RUN: echo #include "\"\\\\?\%{t:real}.dir\FOO.h\"" > %t.dir/test.c
+// RUN: not %clang_cl /WX -fsyntax-only %t.dir\test.c 2>&1 | FileCheck %s
-// CHECK: non-portable path to file '"\\?\
+// CHECK: non-portable path to file '"//?/
More information about the cfe-commits
mailing list