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
