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 `<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 ``` --- 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
