[clang] [Clang][Lex] Fix __has_include_next to return false when no valid next dir (PR #173715)
Zachary Fogg via cfe-commits
cfe-commits at lists.llvm.org
Sat Dec 27 03:36:33 PST 2025
https://github.com/zfogg created https://github.com/llvm/llvm-project/pull/173715
## Summary
Fix `__has_include_next` to return `false` when the current file was found via absolute path rather than incorrectly searching from the start of the include path.
### Problem
When a header file is included using an absolute path (e.g., via `-include /full/path/to/header.h`), any `__has_include_next` call within that header would search from the **beginning** of the include path instead of returning `false`. This caused:
1. **False positives**: Headers that shouldn't be "found" were found because the search started from the beginning
2. **Fatal errors in LibTooling**: Tools like `clang-tidy` and custom LibTooling-based source transformers would crash when parsing files included via absolute path
### Real-World Impact (macOS + Clang 21/22)
This bug was discovered when building LibTooling-based tools on macOS. The macOS SDK's `<stdbool.h>` uses `__has_include_next`:
```c
#if __has_include_next(<stdbool.h>)
#include_next <stdbool.h>
#endif
```
When source files were passed to a LibTooling tool with include paths like `-I/path/to/project/lib`, clang would:
1. Look for `<stdbool.h>` in `/path/to/project/lib/stdbool.h` (due to the bug searching from start)
2. Fail with: `fatal error: cannot open file '/path/to/project/lib/stdbool.h': No such file or directory`
The error path shows clang was looking for the system header in a user include directory, which should never happen.
### Solution
Add a `SkipLookup` parameter to `EvaluateHasIncludeCommon()`. When `EvaluateHasIncludeNext()` detects there's no valid "next" search location (i.e., `!Lookup && !LookupFromFile`) and we're not in the primary file, we consume the tokens but skip the actual file lookup, returning `false`.
The primary file case is excluded to preserve existing behavior.
### Test
Added `has_include_next_absolute.c` which tests that `__has_include_next` returns `false` for a nonexistent header when the current file was found via absolute path.
## Test Plan
```bash
ninja check-clang-lex
```
>From 5cbf4646d567fdec603925f26b25e40086852b59 Mon Sep 17 00:00:00 2001
From: Zachary Fogg <me at zfo.gg>
Date: Sat, 27 Dec 2025 05:57:53 -0500
Subject: [PATCH 1/2] [Clang][Lex] Fix __has_include_next to return false when
no valid next dir
When __has_include_next is evaluated in a file that was found via absolute
path (not through the search directories), getIncludeNextStart returns
{nullptr, nullptr}. Previously, EvaluateHasIncludeCommon would then search
from the start of the include path, which is incorrect for include_next
semantics and could cause false positives or fatal errors when attempting
to open non-existent files.
This fix adds a SkipLookup parameter to EvaluateHasIncludeCommon. When
there's no valid 'next' directory to search from (absolute path case),
we skip the file lookup entirely and return false, which is the correct
behavior - there is no 'next' header to find.
The primary file case is handled separately and preserves existing behavior
where __has_include_next can still find headers (with a warning).
Fixes issues encountered when using LibTooling/ClangTool where source files
are processed with their full paths, causing __has_include_next in system
headers (like clang's stdbool.h) to fail with fatal errors.
---
clang/lib/Lex/PPMacroExpansion.cpp | 25 +++++++++++++++++--
.../has-include-next-absolute/test_header.h | 10 ++++++++
.../Preprocessor/has_include_next_absolute.c | 17 +++++++++++++
3 files changed, 50 insertions(+), 2 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 5efa4b5b3f872..f4453f189ad78 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1134,10 +1134,13 @@ static bool HasExtension(const Preprocessor &PP, StringRef Extension) {
/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
/// or '__has_include_next("path")' expression.
/// Returns true if successful.
+/// If SkipLookup is true, only consume the tokens without performing the
+/// actual file lookup (used when we know the result should be false anyway).
static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
Preprocessor &PP,
ConstSearchDirIterator LookupFrom,
- const FileEntry *LookupFromFile) {
+ const FileEntry *LookupFromFile,
+ bool SkipLookup = 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();
@@ -1204,6 +1207,11 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
if (Filename.empty())
return false;
+ // If SkipLookup is set, we've already consumed the tokens - just return false
+ // without performing the actual file lookup.
+ if (SkipLookup)
+ return false;
+
// Passing this to LookupFile forces header search to check whether the found
// file belongs to a module. Skipping that check could incorrectly mark
// modular header as textual, causing issues down the line.
@@ -1333,7 +1341,20 @@ bool Preprocessor::EvaluateHasIncludeNext(Token &Tok, IdentifierInfo *II) {
const FileEntry *LookupFromFile;
std::tie(Lookup, LookupFromFile) = getIncludeNextStart(Tok);
- return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile);
+ // If getIncludeNextStart returns {nullptr, nullptr} AND we're not in the
+ // primary file, the current file was found via absolute path or relative to
+ // such a file. In this case, there's no valid "next" directory to search
+ // from, so __has_include_next should return false. We pass SkipLookup=true
+ // to consume the tokens without performing the file lookup (which would
+ // incorrectly search from the start of the include path and potentially
+ // find the wrong file or cause errors).
+ //
+ // Note: When in the primary file, we still allow the search to proceed
+ // (with a warning emitted by getIncludeNextStart). This preserves existing
+ // behavior where __has_include_next in primary files can still find headers.
+ bool SkipLookup = !Lookup && !LookupFromFile && !isInPrimaryFile();
+ return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile,
+ SkipLookup);
}
/// Process single-argument builtin feature-like macros that return
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; }
>From cc5f7a7ae96b7193df3461390c366712df334e5b Mon Sep 17 00:00:00 2001
From: Zachary Fogg <me at zfo.gg>
Date: Sat, 27 Dec 2025 06:19:23 -0500
Subject: [PATCH 2/2] style: Match LLVM comment conventions
---
clang/lib/Lex/PPMacroExpansion.cpp | 22 ++++++----------------
1 file changed, 6 insertions(+), 16 deletions(-)
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp b/clang/lib/Lex/PPMacroExpansion.cpp
index f4453f189ad78..b5c65041837b9 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1133,9 +1133,8 @@ static bool HasExtension(const Preprocessor &PP, StringRef Extension) {
/// EvaluateHasIncludeCommon - Process a '__has_include("path")'
/// or '__has_include_next("path")' expression.
-/// Returns true if successful.
-/// If SkipLookup is true, only consume the tokens without performing the
-/// actual file lookup (used when we know the result should be false anyway).
+/// Returns true if successful. If \p SkipLookup is true, only consume the
+/// tokens without performing the file lookup.
static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
Preprocessor &PP,
ConstSearchDirIterator LookupFrom,
@@ -1207,8 +1206,7 @@ static bool EvaluateHasIncludeCommon(Token &Tok, IdentifierInfo *II,
if (Filename.empty())
return false;
- // If SkipLookup is set, we've already consumed the tokens - just return false
- // without performing the actual file lookup.
+ // Tokens consumed; skip the lookup if requested.
if (SkipLookup)
return false;
@@ -1341,17 +1339,9 @@ bool Preprocessor::EvaluateHasIncludeNext(Token &Tok, IdentifierInfo *II) {
const FileEntry *LookupFromFile;
std::tie(Lookup, LookupFromFile) = getIncludeNextStart(Tok);
- // If getIncludeNextStart returns {nullptr, nullptr} AND we're not in the
- // primary file, the current file was found via absolute path or relative to
- // such a file. In this case, there's no valid "next" directory to search
- // from, so __has_include_next should return false. We pass SkipLookup=true
- // to consume the tokens without performing the file lookup (which would
- // incorrectly search from the start of the include path and potentially
- // find the wrong file or cause errors).
- //
- // Note: When in the primary file, we still allow the search to proceed
- // (with a warning emitted by getIncludeNextStart). This preserves existing
- // behavior where __has_include_next in primary files can still find headers.
+ // If there's no valid "next" search location, skip the lookup and return
+ // false. This happens when the file was found via absolute path.
+ // Primary file case is excluded to preserve existing behavior.
bool SkipLookup = !Lookup && !LookupFromFile && !isInPrimaryFile();
return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile,
SkipLookup);
More information about the cfe-commits
mailing list