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
