[llvm-branch-commits] [clang] [llvm] [Clang][Lex] Fix __has_include_next to return false when no valid next dir (PR #173717)
Zachary Fogg via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Sat Dec 27 04:30:41 PST 2025
https://github.com/zfogg updated https://github.com/llvm/llvm-project/pull/173717
>From 77331cd6ef0bf99d3ff799048dc59a8c87e42931 Mon Sep 17 00:00:00 2001
From: Zachary Fogg <me at zfo.gg>
Date: Sat, 27 Dec 2025 06:42:21 -0500
Subject: [PATCH] [Clang][Lex] Fix __has_include_next to return false when no
valid next dir
When a header file is included using an absolute path, any __has_include_next
call within that header would search from the beginning of the include path
instead of returning false. This caused false positives and fatal errors in
LibTooling-based tools on macOS when the SDK's stdbool.h (which uses
__has_include_next) was processed.
The fix refactors the token consumption logic into a separate
ConsumeHasIncludeTokens() helper function that returns a HasIncludeResult
struct. This allows EvaluateHasIncludeNext() to:
1. Get the include-next start location from getIncludeNextStart()
2. Check if there is no valid "next" search location
3. Consume the tokens (maintaining preprocessor state)
4. Return false early without performing the file lookup
The primary file case is excluded to preserve existing behavior.
(cherry picked from commit 5cbf4646d567)
---
clang/lib/Lex/PPMacroExpansion.cpp | 77 ++++++++++++++-----
.../has-include-next-absolute/test_header.h | 10 +++
.../Preprocessor/has_include_next_absolute.c | 17 ++++
cmake/Modules/LLVMVersion.cmake | 2 +-
4 files changed, 87 insertions(+), 19 deletions(-)
create mode 100644 clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h
create mode 100644 clang/test/Preprocessor/has_include_next_absolute.c
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp b/clang/lib/Lex/PPMacroExpansion.cpp
index 890567cfd3246..0d9ff87476f6a 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1131,13 +1131,23 @@ static bool HasExtension(const Preprocessor &PP, StringRef Extension) {
#undef EXTENSION
}
-/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
+/// Result of consuming __has_include/__has_include_next tokens.
+struct HasIncludeResult {
+ bool Valid; // Whether token parsing succeeded.
+ StringRef Filename; // The parsed filename.
+ SourceLocation FileLoc; // Location of the filename token.
+ bool IsAngled; // True for <...>, false for "...".
+};
+
+/// ConsumeHasIncludeTokens - Consume the tokens for a '__has_include("path")'
/// or '__has_include_next("path")' expression.
-/// Returns true if successful.
-static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
- Preprocessor &PP,
- ConstSearchDirIterator LookupFrom,
- const FileEntry *LookupFromFile) {
+/// Returns a HasIncludeResult indicating whether parsing succeeded and
+/// providing the filename if so.
+static HasIncludeResult ConsumeHasIncludeTokens(Token &Tok, IdentifierInfo *II,
+ Preprocessor &PP,
+ SmallString<128> &FilenameBuffer) {
+ HasIncludeResult Result = {false, StringRef(), SourceLocation(), false};
+
// Save the location of the current token. If a '(' is later found, use
// that location. If not, use the end of this location instead.
SourceLocation LParenLoc = Tok.getLocation();
@@ -1148,13 +1158,13 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
// Return a valid identifier token.
assert(Tok.is(tok::identifier));
Tok.setIdentifierInfo(II);
- return false;
+ return Result;
}
// Get '('. If we don't have a '(', try to form a header-name token.
do {
if (PP.LexHeaderName(Tok))
- return false;
+ return Result;
} while (Tok.getKind() == tok::comment);
// Ensure we have a '('.
@@ -1165,25 +1175,24 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
// If the next token looks like a filename or the start of one,
// assume it is and process it as such.
if (Tok.isNot(tok::header_name))
- return false;
+ return Result;
} else {
// Save '(' location for possible missing ')' message.
LParenLoc = Tok.getLocation();
if (PP.LexHeaderName(Tok))
- return false;
+ return Result;
}
if (Tok.isNot(tok::header_name)) {
PP.Diag(Tok.getLocation(), diag::err_pp_expects_filename);
- return false;
+ return Result;
}
// Reserve a buffer to get the spelling.
- SmallString<128> FilenameBuffer;
bool Invalid = false;
StringRef Filename = PP.getSpelling(Tok, FilenameBuffer, &Invalid);
if (Invalid)
- return false;
+ return Result;
SourceLocation FilenameLoc = Tok.getLocation();
@@ -1195,13 +1204,33 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
PP.Diag(PP.getLocForEndOfToken(FilenameLoc), diag::err_pp_expected_after)
<< II << tok::r_paren;
PP.Diag(LParenLoc, diag::note_matching) << tok::l_paren;
- return false;
+ return Result;
}
- bool isAngled = PP.GetIncludeFilenameSpelling(Tok.getLocation(), Filename);
+ bool IsAngled = PP.GetIncludeFilenameSpelling(Tok.getLocation(), Filename);
// If GetIncludeFilenameSpelling set the start ptr to null, there was an
// error.
if (Filename.empty())
+ return Result;
+
+ Result.Valid = true;
+ Result.Filename = Filename;
+ Result.FileLoc = FilenameLoc;
+ Result.IsAngled = IsAngled;
+ return Result;
+}
+
+/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
+/// or '__has_include_next("path")' expression.
+/// Returns true if the file exists.
+static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
+ Preprocessor &PP,
+ ConstSearchDirIterator LookupFrom,
+ const FileEntry *LookupFromFile) {
+ SmallString<128> FilenameBuffer;
+ HasIncludeResult ParseResult =
+ ConsumeHasIncludeTokens(Tok, II, PP, FilenameBuffer);
+ if (!ParseResult.Valid)
return false;
// Passing this to LookupFile forces header search to check whether the found
@@ -1211,14 +1240,16 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
// Search include directories.
OptionalFileEntryRef File =
- PP.LookupFile(FilenameLoc, Filename, isAngled, LookupFrom, LookupFromFile,
- nullptr, nullptr, nullptr, &KH, nullptr, nullptr);
+ PP.LookupFile(ParseResult.FileLoc, ParseResult.Filename,
+ ParseResult.IsAngled, LookupFrom, LookupFromFile, nullptr,
+ nullptr, nullptr, &KH, nullptr, nullptr);
if (PPCallbacks *Callbacks = PP.getPPCallbacks()) {
SrcMgr::CharacteristicKind FileType = SrcMgr::C_User;
if (File)
FileType = PP.getHeaderSearchInfo().getFileDirFlavor(*File);
- Callbacks->HasInclude(FilenameLoc, Filename, isAngled, File, FileType);
+ Callbacks->HasInclude(ParseResult.FileLoc, ParseResult.Filename,
+ ParseResult.IsAngled, File, FileType);
}
// Get the result value. A result of true means the file exists.
@@ -1333,6 +1364,16 @@ bool Preprocessor::EvaluateHasIncludeNext(Token &Tok, IdentifierInfo *II) {
const FileEntry *LookupFromFile;
std::tie(Lookup, LookupFromFile) = getIncludeNextStart(Tok);
+ // If there's no valid "next" search location (file was found via absolute
+ // path), consume the tokens but return false - there's no "next" to find.
+ // Primary file case is excluded to preserve existing behavior.
+ if (!Lookup && !LookupFromFile && !isInPrimaryFile()) {
+ // Still need to consume the tokens to maintain preprocessor state.
+ SmallString<128> FilenameBuffer;
+ ConsumeHasIncludeTokens(Tok, II, *this, FilenameBuffer);
+ return false;
+ }
+
return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile);
}
diff --git a/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h b/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h
new file mode 100644
index 0000000000000..5142adb6dfa50
--- /dev/null
+++ b/clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h
@@ -0,0 +1,10 @@
+// Test header for __has_include_next with absolute path
+// When this header is found via absolute path (not through search directories),
+// __has_include_next should return false instead of searching from the start
+// of the include path.
+
+#if __has_include_next(<nonexistent_header.h>)
+#error "__has_include_next should return false for nonexistent header"
+#endif
+
+#define TEST_HEADER_INCLUDED 1
diff --git a/clang/test/Preprocessor/has_include_next_absolute.c b/clang/test/Preprocessor/has_include_next_absolute.c
new file mode 100644
index 0000000000000..35fd4bd6594fd
--- /dev/null
+++ b/clang/test/Preprocessor/has_include_next_absolute.c
@@ -0,0 +1,17 @@
+// RUN: %clang_cc1 -E -include %S/Inputs/has-include-next-absolute/test_header.h \
+// RUN: -verify %s
+
+// Test that __has_include_next returns false when the current file was found
+// via absolute path (not through the search directories). Previously, this
+// would incorrectly search from the start of the include path, which could
+// cause false positives or fatal errors when it tried to open non-existent
+// files.
+
+// expected-warning at Inputs/has-include-next-absolute/test_header.h:6 {{#include_next in file found relative to primary source file or found by absolute path; will search from start of include path}}
+
+// Verify the header was included correctly
+#ifndef TEST_HEADER_INCLUDED
+#error "test_header.h was not included"
+#endif
+
+int main(void) { return 0; }
diff --git a/cmake/Modules/LLVMVersion.cmake b/cmake/Modules/LLVMVersion.cmake
index 908d52f238128..1fdc014065556 100644
--- a/cmake/Modules/LLVMVersion.cmake
+++ b/cmake/Modules/LLVMVersion.cmake
@@ -7,7 +7,7 @@ if(NOT DEFINED LLVM_VERSION_MINOR)
set(LLVM_VERSION_MINOR 1)
endif()
if(NOT DEFINED LLVM_VERSION_PATCH)
- set(LLVM_VERSION_PATCH 8)
+ set(LLVM_VERSION_PATCH 9)
endif()
if(NOT DEFINED LLVM_VERSION_SUFFIX)
set(LLVM_VERSION_SUFFIX)
More information about the llvm-branch-commits
mailing list