hokein updated this revision to Diff 468688.
hokein marked 4 inline comments as done.
hokein added a comment.

- rebase
- address comments
- implement shouldKeep
- add unittests


Repository:
  rG LLVM Github Monorepo

CHANGES SINCE LAST ACTION
  https://reviews.llvm.org/D136071/new/

https://reviews.llvm.org/D136071

Files:
  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

Index: clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
===================================================================
--- clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
+++ clang-tools-extra/include-cleaner/unittests/RecordTest.cpp
@@ -1,5 +1,6 @@
 #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"
@@ -80,5 +81,60 @@
   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 0
+    #include "keep1.h" // IWYU pragma: keep
+    #include "keep2.h" /* IWYU pragma: keep */
+  )cpp";
+  Inputs.ExtraFiles["keep1.h"] = Inputs.ExtraFiles["keep2.h"] = "";
+
+  TestAST Processed = build();
+  EXPECT_FALSE(PI.shouldKeep(0));
+  EXPECT_TRUE(PI.shouldKeep(1));
+  EXPECT_TRUE(PI.shouldKeep(2));
+}
+
+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 "public.h"
+    class Private {};
+  )cpp";
+  TestAST Processed = build();
+  auto PrivateFE = Processed.fileManager().getFile("private.h");
+  assert(PrivateFE);
+  EXPECT_EQ(PI.getMapping(PrivateFE.get()), "\"public.h\"");
+  auto PublicFE = Processed.fileManager().getFile("public.h");
+  assert(PublicFE);
+  EXPECT_EQ(PI.getMapping(PublicFE.get()), ""); // no mapping.
+}
+
 } // namespace
 } // namespace clang::include_cleaner
Index: clang-tools-extra/include-cleaner/lib/Record.cpp
===================================================================
--- clang-tools-extra/include-cleaner/lib/Record.cpp
+++ 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 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 (SM.isWrittenInMainFile(HashLoc)) {
+      int HashLine =
+          SM.getLineNumber(SM.getMainFileID(), SM.getFileOffset(HashLoc)) - 1;
+      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->IWYUPrivate.insert(
+            {FE->getLastRef().getUniqueID(),
+             Pragma->startswith("<") || Pragma->startswith("\"")
+                 ? Pragma->str()
+                 : ("\"" + *Pragma + "\"").str()});
+      return false;
+    }
+
+    if (SM.isWrittenInMainFile(Range.getBegin())) { // In main file.
+      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())) -
+          1;
+    }
+    return false;
+  }
+
+private:
+  const SourceManager &SM;
+  PragmaIncludes *Out;
+  // Track the last line "IWYU pragma: keep" was seen in the main file,
+  // 0-indexed.
+  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));
+}
+
+bool PragmaIncludes::shouldKeep(unsigned HashLineNumber) const {
+  return ShouldKeep.find(HashLineNumber) != ShouldKeep.end();
+}
+
+llvm::StringRef PragmaIncludes::getMapping(const FileEntry *F) const {
+  auto It = IWYUPrivate.find(F->getUniqueID());
+  if (It == IWYUPrivate.end())
+    return "";
+  return It->getSecond();
+}
+
 std::unique_ptr<ASTConsumer> RecordedAST::record() {
   class Recorder : public ASTConsumer {
     RecordedAST *Out;
Index: clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
===================================================================
--- clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
+++ clang-tools-extra/include-cleaner/include/clang-include-cleaner/Record.h
@@ -17,15 +17,60 @@
 #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.
+//
+// Used in the "Location => Header" analysis step to determine the final
+// spelling 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 # line of the main-file has the
+  // `IWYU pragma: keep`.
+  bool shouldKeep(unsigned HashLineNumber) const;
+  // Returns the mapping include for the given physical header file.
+  // Returns "" if there is no mapping.
+  llvm::StringRef getMapping(const FileEntry *File) const;
+
+  class RecordPragma;
+
+private:
+  // Line numbers for the include directives in the main file that have the
+  // `IWYU pragma: keep` right after.
+  llvm::DenseSet</*0-indexed line number*/ unsigned> ShouldKeep;
+
+  // 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*/>
+      IWYUPrivate;
+
+  // 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.
@@ -42,4 +87,4 @@
 } // namespace include_cleaner
 } // namespace clang
 
-#endif
\ No newline at end of file
+#endif
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to