[clang-tools-extra] [include-cleaner] Ignore stale IWYU export pragmas (PR #202779)
Daniil Dudkin via cfe-commits
cfe-commits at lists.llvm.org
Tue Jun 9 14:46:28 PDT 2026
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 <unterumarmung at yandex.ru>
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 <unterumarmung at yandex.ru>
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
--------------------------
More information about the cfe-commits
mailing list