[clang-tools-extra] 9ab0827 - [include-cleaner] Add a data-structure to capture IWYU pragmas.

Haojian Wu via cfe-commits cfe-commits at lists.llvm.org
Mon Oct 31 07:21:33 PDT 2022


Author: Haojian Wu
Date: 2022-10-31T15:21:24+01:00
New Revision: 9ab0827f70a524aa9f94995662d37675cd042f30

URL: https://github.com/llvm/llvm-project/commit/9ab0827f70a524aa9f94995662d37675cd042f30
DIFF: https://github.com/llvm/llvm-project/commit/9ab0827f70a524aa9f94995662d37675cd042f30.diff

LOG: [include-cleaner] Add a data-structure to capture IWYU pragmas.

PragmaIncludes captures the pragma-based header-mapping information, it is used
in the "Location => Header" step to determine the final spelling header for a
symbol (rather than the header directive).

The structure is by design to be used inside the include-cleaner library
and clangd.

Differential Revision: https://reviews.llvm.org/D136071

Added: 
    

Modified: 
    clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
    clang-tools-extra/include-cleaner/lib/Record.cpp
    clang-tools-extra/include-cleaner/unittests/RecordTest.cpp

Removed: 
    


################################################################################
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
index c5449b68b1f79..258d36141e5b0 100644
--- 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 <memory>
 #include <vector>
 
 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</*LineNumber*/ unsigned> 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<llvm::sys::fs::UniqueID, std::string /*VerbatimSpelling*/>
+      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
index 88f9268eeb93c..ba691f46460a3 100644
--- 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<StringRef> 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<FileEntryRef> 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<RecordPragma>(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<ASTConsumer> 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
index 8eaa3ffb43672..c1f15623021de 100644
--- 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 @@ TEST_F(RecordASTTest, Macros) {
   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<Hook>(&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


        


More information about the cfe-commits mailing list