Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@zfogg
Copy link
Contributor

@zfogg zfogg commented Dec 27, 2025

Summary

Fix __has_include_next to return false when the current file was found via an absolute path (not through a search directory).

When a file is included via absolute path, getIncludeNextStart() returns {nullptr, LookupFromFile} because there's no search directory iterator. In this case, __has_include_next() should return false since there is no "next" directory to search in.

Problem

Previously, EvaluateHasIncludeCommon would still attempt the file lookup even when Lookup was null. This caused issues with:

  • VFS overlays in LibTooling applications
  • Headers that use __has_include_next in non-taken preprocessor branches

Solution

  1. Add early return in EvaluateHasIncludeNext when Lookup is null
  2. Refactor token consumption into ConsumeHasIncludeTokens helper to properly consume tokens before returning
  3. Add test case for the absolute path scenario

Test Plan

Added clang/test/Preprocessor/has_include_next_absolute.c which verifies:

  • __has_include_next returns 0 when file is included via absolute path
  • Regular __has_include still works correctly

cc @jansvoboda11 @AaronBallman

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Dec 27, 2025
@llvmbot
Copy link
Member

llvmbot commented Dec 27, 2025

@llvm/pr-subscribers-clang

Author: Zachary Fogg (zfogg)

Changes

Summary

Fix __has_include_next to return false when the current file was found via an absolute path (not through a search directory).

When a file is included via absolute path, getIncludeNextStart() returns {nullptr, LookupFromFile} because there's no search directory iterator. In this case, __has_include_next() should return false since there is no "next" directory to search in.

Problem

Previously, EvaluateHasIncludeCommon would still attempt the file lookup even when Lookup was null. This caused issues with:

  • VFS overlays in LibTooling applications
  • Headers that use __has_include_next in non-taken preprocessor branches

Solution

  1. Add early return in EvaluateHasIncludeNext when Lookup is null
  2. Refactor token consumption into ConsumeHasIncludeTokens helper to properly consume tokens before returning
  3. Add test case for the absolute path scenario

Test Plan

Added clang/test/Preprocessor/has_include_next_absolute.c which verifies:

  • __has_include_next returns 0 when file is included via absolute path
  • Regular __has_include still works correctly

cc @jansvoboda11 @AaronBallman


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

3 Files Affected:

  • (modified) clang/lib/Lex/PPMacroExpansion.cpp (+59-18)
  • (added) clang/test/Preprocessor/Inputs/has-include-next-absolute/test_header.h (+10)
  • (added) clang/test/Preprocessor/has_include_next_absolute.c (+17)
diff --git a/clang/lib/Lex/PPMacroExpansion.cpp b/clang/lib/Lex/PPMacroExpansion.cpp
index 5efa4b5b3f872..16a3d9e6665a6 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@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; }

@github-actions
Copy link

github-actions bot commented Dec 27, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@zfogg zfogg force-pushed the fix-has-include-next-main branch from e8fc481 to 8201169 Compare December 27, 2025 12:56
…t dir

When a file is found via an absolute path (not through a search directory),
`getIncludeNextStart()` returns `{nullptr, LookupFromFile}`. In this case,
`__has_include_next()` should return false since there is no "next" directory
to search in.

Previously, `EvaluateHasIncludeCommon` would still attempt the file lookup,
which could cause issues with VFS overlays when headers check for the existence
of other headers using `__has_include_next` in non-taken preprocessor branches.

This fix:
1. Adds early return in `EvaluateHasIncludeNext` when Lookup is null
2. Refactors token consumption into `ConsumeHasIncludeTokens` helper
3. Adds a test case for the absolute path scenario
@zfogg zfogg force-pushed the fix-has-include-next-main branch from 8201169 to 1203ee4 Compare December 27, 2025 14:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants