https://github.com/unterumarmung updated 
https://github.com/llvm/llvm-project/pull/202779

>From 1f85b3e4af0dbe0b753549c164c9e72076895f42 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <[email protected]>
Date: Wed, 10 Jun 2026 00:43:19 +0300
Subject: [PATCH 1/2] [include-cleaner] Ignore stale IWYU export pragmas

A single-line `IWYU pragma: export` can be attached to a non-include
line, such as a forward declaration. Previously include-cleaner still
pushed such pragmas onto the export stack, where they could hide an
enclosing `begin_exports` block or interfere with `end_exports`.

Discard stale single-line export pragmas before processing includes and
before closing export blocks. Instead of detecting includes directly,
keep a single-line export only when its file and line match the include
currently being processed.

Fixes #200036

Assisted by Codex
---
 .../include-cleaner/lib/Record.cpp            | 30 +++++--
 .../include-cleaner/unittests/RecordTest.cpp  | 85 +++++++++++++++++++
 2 files changed, 108 insertions(+), 7 deletions(-)

diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp 
b/clang-tools-extra/include-cleaner/lib/Record.cpp
index 0284d6842e2d2..4a2c27e959260 100644
--- a/clang-tools-extra/include-cleaner/lib/Record.cpp
+++ b/clang-tools-extra/include-cleaner/lib/Record.cpp
@@ -240,8 +240,10 @@ class PragmaIncludes::RecordPragma : public PPCallbacks, 
public CommentHandler {
   void checkForExport(FileID IncludingFile, int HashLine,
                       std::optional<Header> IncludedHeader,
                       OptionalFileEntryRef IncludedFile) {
+    discardStaleExports(IncludingFile, HashLine);
     if (ExportStack.empty())
       return;
+
     auto &Top = ExportStack.back();
     if (Top.SeenAtFile != IncludingFile)
       return;
@@ -255,9 +257,28 @@ class PragmaIncludes::RecordPragma : public PPCallbacks, 
public CommentHandler {
       // main-file #include with export pragma should never be removed.
       if (Top.SeenAtFile == SM.getMainFileID() && IncludedFile)
         Out->ShouldKeep.insert(IncludedFile->getUniqueID());
+      if (!Top.Block)
+        ExportStack.pop_back();
     }
-    if (!Top.Block) // Pop immediately for single-line export pragma.
+  }
+
+  void discardStaleExports(FileID IncludingFile, int HashLine) {
+    while (!ExportStack.empty() && !ExportStack.back().Block) {
+      auto &Top = ExportStack.back();
+      if (Top.SeenAtFile == IncludingFile && Top.SeenAtLine == HashLine)
+        return;
       ExportStack.pop_back();
+    }
+  }
+
+  void checkForEndExport(FileID CommentFID, int CommentLine) {
+    discardStaleExports(CommentFID, CommentLine);
+    if (!ExportStack.empty()) {
+      // FIXME: be robust on unmatching cases. We should only pop the stack if
+      // the begin_exports and end_exports is in the same file.
+      assert(ExportStack.back().Block);
+      ExportStack.pop_back();
+    }
   }
 
   void checkForKeep(int HashLine, OptionalFileEntryRef IncludedFile) {
@@ -350,12 +371,7 @@ class PragmaIncludes::RecordPragma : public PPCallbacks, 
public CommentHandler {
     } else if (Pragma->starts_with("begin_exports")) {
       ExportStack.push_back({CommentLine, CommentFID, save(Filename), true});
     } else if (Pragma->starts_with("end_exports")) {
-      // FIXME: be robust on unmatching cases. We should only pop the stack if
-      // the begin_exports and end_exports is in the same file.
-      if (!ExportStack.empty()) {
-        assert(ExportStack.back().Block);
-        ExportStack.pop_back();
-      }
+      checkForEndExport(CommentFID, CommentLine);
     }
     return false;
   }
diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp 
b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
index cbf7bae23b365..43bfa8f93324d 100644
--- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -589,6 +589,91 @@ TEST_F(PragmaIncludeTest, IWYUExportBlock) {
   EXPECT_TRUE(Exporters.empty()) << GetNames(Exporters);
 }
 
+TEST_F(PragmaIncludeTest, IWYUExportOnForwardDeclDoesNotEndBlock) {
+  Inputs.Code = R"cpp(
+    #include "normal.h"
+  )cpp";
+  Inputs.ExtraFiles["normal.h"] = R"cpp(
+    // IWYU pragma: begin_exports
+    #include "export1.h"
+    #include "private1.h"
+    // IWYU pragma: end_exports
+  )cpp";
+  Inputs.ExtraFiles["export1.h"] = R"cpp(
+    class foo; // IWYU pragma: export
+  )cpp";
+  createEmptyFiles({"private1.h"});
+
+  TestAST Processed = build();
+  auto &FM = Processed.fileManager();
+
+  EXPECT_THAT(PI.getExporters(*FM.getOptionalFileRef("private1.h"), FM),
+              testing::UnorderedElementsAre(FileNamed("normal.h")));
+  EXPECT_THAT(PI.getExporters(*FM.getOptionalFileRef("export1.h"), FM),
+              testing::UnorderedElementsAre(FileNamed("normal.h")));
+}
+
+TEST_F(PragmaIncludeTest, IWYUExportOnForwardDeclDoesNotBreakEndBlock) {
+  Inputs.Code = R"cpp(
+    #include "normal.h"
+  )cpp";
+  Inputs.ExtraFiles["normal.h"] = R"cpp(
+    // IWYU pragma: begin_exports
+    #include "export1.h"
+    // IWYU pragma: end_exports
+    #include "ordinary.h"
+  )cpp";
+  Inputs.ExtraFiles["export1.h"] = R"cpp(
+    class foo; // IWYU pragma: export
+  )cpp";
+  createEmptyFiles({"ordinary.h"});
+
+  TestAST Processed = build();
+  auto &FM = Processed.fileManager();
+
+  EXPECT_THAT(PI.getExporters(*FM.getOptionalFileRef("export1.h"), FM),
+              testing::UnorderedElementsAre(FileNamed("normal.h")));
+  EXPECT_TRUE(
+      PI.getExporters(*FM.getOptionalFileRef("ordinary.h"), FM).empty());
+}
+
+TEST_F(PragmaIncludeTest, IWYUExportOnForwardDeclDoesNotAffectNextInclude) {
+  Inputs.Code = R"cpp(
+    #include "normal.h"
+  )cpp";
+  Inputs.ExtraFiles["normal.h"] = R"cpp(
+    #include "export1.h"
+    #include "ordinary.h"
+  )cpp";
+  Inputs.ExtraFiles["export1.h"] = R"cpp(
+    class foo; // IWYU pragma: export
+  )cpp";
+  createEmptyFiles({"ordinary.h"});
+
+  TestAST Processed = build();
+  auto &FM = Processed.fileManager();
+
+  EXPECT_TRUE(
+      PI.getExporters(*FM.getOptionalFileRef("ordinary.h"), FM).empty());
+}
+
+TEST_F(PragmaIncludeTest, IWYUExportOnSameFileForwardDeclDoesNotApply) {
+  Inputs.Code = R"cpp(
+    #include "normal.h"
+  )cpp";
+  Inputs.ExtraFiles["normal.h"] = R"cpp(
+    class foo; // IWYU pragma: export
+    #include "ordinary.h"
+  )cpp";
+  createEmptyFiles({"ordinary.h"});
+
+  TestAST Processed = build();
+  auto &FM = Processed.fileManager();
+
+  EXPECT_TRUE(
+      PI.getExporters(*FM.getOptionalFileRef("ordinary.h"), FM).empty());
+}
+
 TEST_F(PragmaIncludeTest, SelfContained) {
   Inputs.Code = R"cpp(
   #include "guarded.h"

>From d4c73e9b7f1a07951b301b38b076909e6b113dd6 Mon Sep 17 00:00:00 2001
From: Daniil Dudkin <[email protected]>
Date: Wed, 10 Jun 2026 00:46:07 +0300
Subject: [PATCH 2/2] add a release note

---
 clang-tools-extra/docs/ReleaseNotes.rst | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/clang-tools-extra/docs/ReleaseNotes.rst 
b/clang-tools-extra/docs/ReleaseNotes.rst
index c369b1fd8b373..2ecf3d11b3f40 100644
--- a/clang-tools-extra/docs/ReleaseNotes.rst
+++ b/clang-tools-extra/docs/ReleaseNotes.rst
@@ -189,6 +189,12 @@ Improvements to clang-doc
 Improvements to clang-query
 ---------------------------
 
+Improvements to clang-include-cleaner
+-------------------------------------
+
+- ``IWYU pragma: export`` on a forward declaration no longer interferes with
+  surrounding ``begin_exports`` blocks.
+
 Improvements to clang-tidy
 --------------------------
 

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

Reply via email to