[llvm-branch-commits] [clang] [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 03:57:45 PST 2025


https://github.com/zfogg created https://github.com/llvm/llvm-project/pull/173717

## 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)

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`

### Solution

Refactor 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.

### 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-preprocessor
# or
llvm-lit clang/test/Preprocessor/has_include_next_absolute.c
```

>From 6125b4571d7046e7eae55201b0fc0ee350c92b1f 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 ++++
 3 files changed, 86 insertions(+), 18 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; }



More information about the llvm-branch-commits mailing list