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 <[email protected]>
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@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 <[email protected]>
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);

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to