llvmbot wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang

Author: Zachary Fogg (zfogg)

<details>
<summary>Changes</summary>

## 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 `&lt;stdbool.h&gt;` uses `__has_include_next`:

```c
#if __has_include_next(&lt;stdbool.h&gt;)
#include_next &lt;stdbool.h&gt;
#endif
```

When source files were passed to a LibTooling tool with include paths like 
`-I/path/to/project/lib`, clang would:
1. Look for `&lt;stdbool.h&gt;` 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 &amp;&amp; !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
```

---
Full diff: https://github.com/llvm/llvm-project/pull/173715.diff


3 Files Affected:

- (modified) clang/lib/Lex/PPMacroExpansion.cpp (+14-3) 
- (added) 
clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h (+10) 
- (added) clang/test/Preprocessor/has_include_next_absolute.c (+17) 


``````````diff
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp 
b/clang/lib/Lex/PPMacroExpansion.cpp
index 5efa4b5b3f872..b5c65041837b9 100644
--- a/clang/lib/Lex/PPMacroExpansion.cpp
+++ b/clang/lib/Lex/PPMacroExpansion.cpp
@@ -1133,11 +1133,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.
+/// 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,
-                                     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 +1206,10 @@ static bool EvaluateHasIncludeCommon(Token &Tok, 
IdentifierInfo *II,
   if (Filename.empty())
     return false;
 
+  // Tokens consumed; skip the lookup if requested.
+  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 +1339,12 @@ bool Preprocessor::EvaluateHasIncludeNext(Token &Tok, 
IdentifierInfo *II) {
   const FileEntry *LookupFromFile;
   std::tie(Lookup, LookupFromFile) = getIncludeNextStart(Tok);
 
-  return EvaluateHasIncludeCommon(Tok, II, *this, Lookup, LookupFromFile);
+  // 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);
 }
 
 /// 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; }

``````````

</details>


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

Reply via email to