diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h --- a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h +++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h @@ -19,6 +19,7 @@ #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/SmallVector.h" #include "llvm/Support/FileSystem/UniqueID.h" #include #include @@ -29,6 +30,7 @@ class CompilerInstance; class Decl; class FileEntry; +class FileManager; namespace include_cleaner { @@ -55,6 +57,11 @@ /// Returns "" if there is none. llvm::StringRef getPublic(const FileEntry *File) const; + /// Returns all direct exporter headers for the given header file. + /// Returns empty if there is none. + llvm::SmallVector getExporters(const FileEntry *File, + FileManager &FM) const; + private: class RecordPragma; /// 1-based Line numbers for the #include directives of the main file that @@ -70,7 +77,15 @@ llvm::DenseMap IWYUPublic; - // FIXME: add other IWYU supports (export etc) + /// A reverse map from the underlying header to its exporter headers. + llvm::DenseMap> + IWYUExportBy; + /// A mapping from FileEntry to RealPathNames for all opened files during the + /// parsing. + llvm::DenseMap + RealPathNamesByUID; + // FIXME: add support for clang use_instead pragma // FIXME: add selfcontained file. }; diff --git a/clang-tools-extra/include-cleaner/lib/Record.cpp b/clang-tools-extra/include-cleaner/lib/Record.cpp --- a/clang-tools-extra/include-cleaner/lib/Record.cpp +++ b/clang-tools-extra/include-cleaner/lib/Record.cpp @@ -43,6 +43,12 @@ InMainFile = SM.isWrittenInMainFile(Loc); } + void EndOfMainFile() override { + const auto *MainFile = SM.getFileEntryForID(SM.getMainFileID()); + Out->RealPathNamesByUID.try_emplace(MainFile->getUniqueID(), + MainFile->tryGetRealPathName()); + } + void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, llvm::StringRef FileName, bool IsAngled, CharSourceRange /*FilenameRange*/, @@ -51,6 +57,22 @@ llvm::StringRef /*RelativePath*/, const clang::Module * /*Imported*/, SrcMgr::CharacteristicKind FileKind) override { + Out->RealPathNamesByUID.try_emplace( + File->getUniqueID(), File->getFileEntry().tryGetRealPathName()); + + if (!ExportStack.empty() && + ExportStack.back().Exporter == SM.getFileID(HashLoc)) { + auto Top = ExportStack.back(); + Out->IWYUExportBy[File->getFileEntry().getUniqueID()].insert( + SM.getFileEntryForID(Top.Exporter)->getUniqueID()); + // main-file #include with export pragma should never be removed. + if (Top.Exporter == SM.getMainFileID()) + Out->ShouldKeep.insert( + SM.getLineNumber(SM.getMainFileID(), SM.getFileOffset(HashLoc))); + if (!Top.Block) // Pop immediately for sing-line export pragma. + ExportStack.pop_back(); + } + if (!InMainFile) return; int HashLine = @@ -76,6 +98,20 @@ return false; } + // Handle export pragma. + if (Pragma->startswith("export")) { + ExportStack.push_back({SM.getFileID(Range.getBegin()), false}); + } else if (Pragma->startswith("begin_exports")) { + ExportStack.push_back({SM.getFileID(Range.getBegin()), true}); + } else if (Pragma->startswith("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(); + } + } + if (InMainFile) { if (!Pragma->startswith("keep")) return false; @@ -105,6 +141,15 @@ PragmaIncludes *Out; // Track the last line "IWYU pragma: keep" was seen in the main file, 1-based. int LastPragmaKeepInMainFileLine = -1; + struct State { + // The file where we saw the export pragma. + FileID Exporter; + // true if it is a block begin/end_exports pragma; false if it is a + // single-line export pragma + bool Block = false; + }; + // A stack for tracking all open begin_exports or single-line export. + std::vector ExportStack; }; void PragmaIncludes::record(const CompilerInstance &CI) { @@ -120,6 +165,24 @@ return It->getSecond(); } +llvm::SmallVector +PragmaIncludes::getExporters(const FileEntry *File, FileManager &FM) const { + auto It = IWYUExportBy.find(File->getUniqueID()); + if (It == IWYUExportBy.end()) + return {}; + + llvm::SmallVector Results; + for (auto Export : It->getSecond()) { + if (auto RealExport = RealPathNamesByUID.find(Export); + RealExport != RealPathNamesByUID.end()) { + auto FE = FM.getFileRef(RealExport->getSecond()); + assert(FE); + Results.push_back(FE.get()); + } + } + return Results; +} + std::unique_ptr RecordedAST::record() { class Recorder : public ASTConsumer { RecordedAST *Out; diff --git a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp --- a/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp +++ b/clang-tools-extra/include-cleaner/unittests/RecordTest.cpp @@ -30,6 +30,13 @@ return false; } +MATCHER_P(FileNamed, N, "") { + if (arg->tryGetRealPathName() == N) + return true; + *result_listener << arg->tryGetRealPathName().str(); + return false; +} + class RecordASTTest : public ::testing::Test { protected: TestInputs Inputs; @@ -117,13 +124,31 @@ Inputs.Code = R"cpp(// Line 1 #include "keep1.h" // IWYU pragma: keep #include "keep2.h" /* IWYU pragma: keep */ + + #include "export1.h" // IWYU pragma: export // line 5 + // IWYU pragma: begin_exports + #include "export2.h" // Line 7 + #include "export3.h" + // IWYU pragma: end_exports + + #include "normal.h" // Line 11 )cpp"; - Inputs.ExtraFiles["keep1.h"] = Inputs.ExtraFiles["keep2.h"] = ""; + Inputs.ExtraFiles["keep1.h"] = Inputs.ExtraFiles["keep2.h"] = + Inputs.ExtraFiles["export1.h"] = Inputs.ExtraFiles["export2.h"] = + Inputs.ExtraFiles["export3.h"] = Inputs.ExtraFiles["normal.h"] = ""; TestAST Processed = build(); EXPECT_FALSE(PI.shouldKeep(1)); + // Keep EXPECT_TRUE(PI.shouldKeep(2)); EXPECT_TRUE(PI.shouldKeep(3)); + + // Exports + EXPECT_TRUE(PI.shouldKeep(5)); + EXPECT_TRUE(PI.shouldKeep(7)); + EXPECT_TRUE(PI.shouldKeep(8)); + + EXPECT_FALSE(PI.shouldKeep(11)); } TEST_F(PragmaIncludeTest, IWYUPrivate) { @@ -144,5 +169,69 @@ EXPECT_EQ(PI.getPublic(PublicFE.get()), ""); // no mapping. } +TEST_F(PragmaIncludeTest, IWYUExport) { + Inputs.Code = R"cpp(// Line 1 + #include "export1.h" + #include "export2.h" + )cpp"; + Inputs.ExtraFiles["export1.h"] = R"cpp( + #include "private.h" // IWYU pragma: export + )cpp"; + Inputs.ExtraFiles["export2.h"] = R"cpp( + #include "export3.h" + )cpp"; + Inputs.ExtraFiles["export3.h"] = R"cpp( + #include "private.h" // IWYU pragma: export + )cpp"; + Inputs.ExtraFiles["private.h"] = ""; + TestAST Processed = build(); + auto &FM = Processed.fileManager(); + + EXPECT_THAT(PI.getExporters(FM.getFile("private.h").get(), FM), + testing::UnorderedElementsAre(FileNamed("export1.h"), + FileNamed("export3.h"))); + EXPECT_TRUE(PI.getExporters(FM.getFile("export3.h").get(), FM).empty()); +} + +TEST_F(PragmaIncludeTest, IWYUExportBlock) { + Inputs.Code = R"cpp(// Line 1 + #include "normal.h" + )cpp"; + Inputs.ExtraFiles["normal.h"] = R"cpp( + #include "foo.h" + + // IWYU pragma: begin_exports + #include "export1.h" + #include "private1.h" + // IWYU pragma: end_exports + )cpp"; + Inputs.ExtraFiles["export1.h"] = R"cpp( + // IWYU pragma: begin_exports + #include "private1.h" + #include "private2.h" + // IWYU pragma: end_exports + + #include "bar.h" + #include "private3.h" // IWYU pragma: export + )cpp"; + + Inputs.ExtraFiles["private1.h"] = Inputs.ExtraFiles["private2.h"] = + Inputs.ExtraFiles["private3.h"] = ""; + Inputs.ExtraFiles["foo.h"] = Inputs.ExtraFiles["bar.h"] = ""; + TestAST Processed = build(); + auto &FM = Processed.fileManager(); + + EXPECT_THAT(PI.getExporters(FM.getFile("private1.h").get(), FM), + testing::UnorderedElementsAre(FileNamed("export1.h"), + FileNamed("normal.h"))); + EXPECT_THAT(PI.getExporters(FM.getFile("private2.h").get(), FM), + testing::UnorderedElementsAre(FileNamed("export1.h"))); + EXPECT_THAT(PI.getExporters(FM.getFile("private3.h").get(), FM), + testing::UnorderedElementsAre(FileNamed("export1.h"))); + + EXPECT_TRUE(PI.getExporters(FM.getFile("foo.h").get(), FM).empty()); + EXPECT_TRUE(PI.getExporters(FM.getFile("bar.h").get(), FM).empty()); +} + } // namespace } // namespace clang::include_cleaner