[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