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 @@ -17,15 +17,64 @@ #ifndef CLANG_INCLUDE_CLEANER_RECORD_H #define CLANG_INCLUDE_CLEANER_RECORD_H +#include "llvm/ADT/DenseMap.h" +#include "llvm/ADT/DenseSet.h" +#include "llvm/Support/FileSystem/UniqueID.h" #include #include namespace clang { class ASTConsumer; class ASTContext; +class CompilerInstance; class Decl; +class FileEntry; + namespace include_cleaner { +/// Captures #include mapping information. It analyses IWYU Pragma comments and +/// other use-instead-like mechanisms (#pragma include_instead) on included +/// files. +/// +/// This is a low-level piece being used in the "Location => Header" analysis +/// step to determine the final public header rather than the header directly +/// defines the symbol. +class PragmaIncludes { +public: + /// Installs an analysing PPCallback and CommentHandler and populates results + /// to the structure. + void record(const CompilerInstance &CI); + + /// Returns true if the given #include of the main-file should never be + /// removed. + bool shouldKeep(unsigned HashLineNumber) const { + return ShouldKeep.find(HashLineNumber) != ShouldKeep.end(); + } + + /// Returns the public mapping include for the given physical header file. + /// Returns "" if there is none. + llvm::StringRef getPublic(const FileEntry *File) const; + +private: + class RecordPragma; + /// 1-based Line numbers for the #include directives of the main file that + /// should always keep (e.g. has the `IWYU pragma: keep` or `IWYU pragma: + /// export` right after). + llvm::DenseSet ShouldKeep; + + /// The public header mapping by the IWYU private pragma. + // + // !!NOTE: instead of using a FileEntry* to identify the physical file, we + // deliberately use the UniqueID to ensure the result is stable across + // FileManagers (for clangd's preamble and main-file builds). + llvm::DenseMap + IWYUPublic; + + // FIXME: add other IWYU supports (export etc) + // FIXME: add support for clang use_instead pragma + // FIXME: add selfcontained file. +}; + // Contains recorded parser events relevant to include-cleaner. struct RecordedAST { // The consumer (when installed into clang) tracks declarations in this. 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 @@ -11,9 +11,115 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/DeclGroup.h" #include "clang/Basic/SourceManager.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Lex/PPCallbacks.h" +#include "clang/Lex/Preprocessor.h" namespace clang::include_cleaner { +// FIXME: this is a mirror of clang::clangd::parseIWYUPragma, move to libTooling +// to share the code? +static llvm::Optional parseIWYUPragma(const char *Text) { + assert(strncmp(Text, "//", 2) || strncmp(Text, "/*", 2)); + constexpr llvm::StringLiteral IWYUPragma = " IWYU pragma: "; + Text += 2; // Skip the comment start, // or /*. + if (strncmp(Text, IWYUPragma.data(), IWYUPragma.size())) + return llvm::None; + Text += IWYUPragma.size(); + const char *End = Text; + while (*End != 0 && *End != '\n') + ++End; + return StringRef(Text, End - Text); +} + +class PragmaIncludes::RecordPragma : public PPCallbacks, public CommentHandler { +public: + RecordPragma(const CompilerInstance &CI, PragmaIncludes *Out) + : SM(CI.getSourceManager()), Out(Out) {} + + void FileChanged(SourceLocation Loc, FileChangeReason Reason, + SrcMgr::CharacteristicKind FileType, + FileID PrevFID) override { + InMainFile = SM.isWrittenInMainFile(Loc); + } + + void InclusionDirective(SourceLocation HashLoc, const Token &IncludeTok, + llvm::StringRef FileName, bool IsAngled, + CharSourceRange /*FilenameRange*/, + Optional File, + llvm::StringRef /*SearchPath*/, + llvm::StringRef /*RelativePath*/, + const clang::Module * /*Imported*/, + SrcMgr::CharacteristicKind FileKind) override { + if (!InMainFile) + return; + int HashLine = + SM.getLineNumber(SM.getMainFileID(), SM.getFileOffset(HashLoc)); + if (LastPragmaKeepInMainFileLine == HashLine) + Out->ShouldKeep.insert(HashLine); + } + + bool HandleComment(Preprocessor &PP, SourceRange Range) override { + auto &SM = PP.getSourceManager(); + auto Pragma = parseIWYUPragma(SM.getCharacterData(Range.getBegin())); + if (!Pragma) + return false; + + if (Pragma->consume_front("private, include ")) { + // We always insert using the spelling from the pragma. + if (auto *FE = SM.getFileEntryForID(SM.getFileID(Range.getBegin()))) + Out->IWYUPublic.insert( + {FE->getLastRef().getUniqueID(), + Pragma->startswith("<") || Pragma->startswith("\"") + ? Pragma->str() + : ("\"" + *Pragma + "\"").str()}); + return false; + } + + if (InMainFile) { + if (!Pragma->startswith("keep")) + return false; + // Given: + // + // #include "foo.h" + // #include "bar.h" // IWYU pragma: keep + // + // The order in which the callbacks will be triggered: + // + // 1. InclusionDirective("foo.h") + // 2. handleCommentInMainFile("// IWYU pragma: keep") + // 3. InclusionDirective("bar.h") + // + // This code stores the last location of "IWYU pragma: keep" (or export) + // comment in the main file, so that when next InclusionDirective is + // called, it will know that the next inclusion is behind the IWYU pragma. + LastPragmaKeepInMainFileLine = SM.getLineNumber( + SM.getMainFileID(), SM.getFileOffset(Range.getBegin())); + } + return false; + } + +private: + bool InMainFile = false; + const SourceManager &SM; + PragmaIncludes *Out; + // Track the last line "IWYU pragma: keep" was seen in the main file, 1-based. + int LastPragmaKeepInMainFileLine = -1; +}; + +void PragmaIncludes::record(const CompilerInstance &CI) { + auto Record = std::make_unique(CI, this); + CI.getPreprocessor().addCommentHandler(Record.get()); + CI.getPreprocessor().addPPCallbacks(std::move(Record)); +} + +llvm::StringRef PragmaIncludes::getPublic(const FileEntry *F) const { + auto It = IWYUPublic.find(F->getUniqueID()); + if (It == IWYUPublic.end()) + return ""; + return It->getSecond(); +} + 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 @@ -8,6 +8,7 @@ #include "clang-include-cleaner/Record.h" #include "clang/Frontend/FrontendAction.h" +#include "clang/Frontend/FrontendActions.h" #include "clang/Testing/TestAST.h" #include "llvm/Support/raw_ostream.h" #include "gmock/gmock.h" @@ -88,5 +89,63 @@ EXPECT_THAT(Recorded.Roots, testing::ElementsAre(Named("x"))); } +class PragmaIncludeTest : public ::testing::Test { +protected: + // We don't build an AST, we just run a preprocessor action! + TestInputs Inputs; + PragmaIncludes PI; + + PragmaIncludeTest() { + Inputs.MakeAction = [this] { + struct Hook : public PreprocessOnlyAction { + public: + Hook(PragmaIncludes *Out) : Out(Out) {} + bool BeginSourceFileAction(clang::CompilerInstance &CI) override { + Out->record(CI); + return true; + } + PragmaIncludes *Out; + }; + return std::make_unique(&PI); + }; + } + + TestAST build() { return TestAST(Inputs); } +}; + +TEST_F(PragmaIncludeTest, IWYUKeep) { + Inputs.Code = R"cpp(// Line 1 + #include "keep1.h" // IWYU pragma: keep + #include "keep2.h" /* IWYU pragma: keep */ + #include "normal.h" + )cpp"; + Inputs.ExtraFiles["keep1.h"] = Inputs.ExtraFiles["keep2.h"] = + Inputs.ExtraFiles["normal.h"] = ""; + + TestAST Processed = build(); + EXPECT_FALSE(PI.shouldKeep(1)); + EXPECT_TRUE(PI.shouldKeep(2)); + EXPECT_TRUE(PI.shouldKeep(3)); + EXPECT_FALSE(PI.shouldKeep(4)); +} + +TEST_F(PragmaIncludeTest, IWYUPrivate) { + Inputs.Code = R"cpp( + #include "public.h" + )cpp"; + Inputs.ExtraFiles["public.h"] = "#include \"private.h\""; + Inputs.ExtraFiles["private.h"] = R"cpp( + // IWYU pragma: private, include "public2.h" + class Private {}; + )cpp"; + TestAST Processed = build(); + auto PrivateFE = Processed.fileManager().getFile("private.h"); + assert(PrivateFE); + EXPECT_EQ(PI.getPublic(PrivateFE.get()), "\"public2.h\""); + auto PublicFE = Processed.fileManager().getFile("public.h"); + assert(PublicFE); + EXPECT_EQ(PI.getPublic(PublicFE.get()), ""); // no mapping. +} + } // namespace } // namespace clang::include_cleaner