diff --git a/clang-tools-extra/include-cleaner/CMakeLists.txt b/clang-tools-extra/include-cleaner/CMakeLists.txt --- a/clang-tools-extra/include-cleaner/CMakeLists.txt +++ b/clang-tools-extra/include-cleaner/CMakeLists.txt @@ -1,4 +1,6 @@ +include_directories(include) add_subdirectory(lib) +add_subdirectory(tool) if(CLANG_INCLUDE_TESTS) add_subdirectory(test) add_subdirectory(unittests) diff --git a/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Hooks.h b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Hooks.h new file mode 100644 --- /dev/null +++ b/clang-tools-extra/include-cleaner/include/clang-include-cleaner/Hooks.h @@ -0,0 +1,45 @@ +//===--- Hooks.h - Record compiler events -------------------------- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// Where Analysis.h analyzes AST nodes and recorded preprocessor events, this +// file defines ways to capture AST and preprocessor information from a parse. +// +// These are the simplest way to connect include-cleaner logic to the parser, +// but other ways are possible (for example clangd records includes separately). +// +//===----------------------------------------------------------------------===// + +#ifndef CLANG_INCLUDE_CLEANER_HOOKS_H +#define CLANG_INCLUDE_CLEANER_HOOKS_H + +#include +#include + +namespace clang { +class ASTConsumer; +class ASTContext; +class Decl; +namespace include_cleaner { + +// Contains recorded parser events relevant to include-cleaner. +struct RecordedAST { + // The consumer (when installed into clang) tracks declarations in this. + std::unique_ptr record(); + + ASTContext *Ctx = nullptr; + // The set of declarations written at file scope inside the main file. + // + // These are the roots of the subtrees that should be traversed to find uses. + // (Traversing the TranslationUnitDecl would find uses inside headers!) + std::vector TopLevelDecls; +}; + +} // namespace include_cleaner +} // namespace clang + +#endif \ No newline at end of file diff --git a/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h b/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h --- a/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h +++ b/clang-tools-extra/include-cleaner/lib/AnalysisInternal.h @@ -25,6 +25,7 @@ #include "llvm/ADT/STLFunctionalExtras.h" namespace clang { +class ASTContext; class Decl; class NamedDecl; namespace include_cleaner { @@ -41,6 +42,11 @@ /// being analyzed, in order to find all references within it. void walkAST(Decl &Root, llvm::function_ref); +/// Write an HTML summary of the analysis to the given stream. +/// FIXME: Once analysis has a public API, this should be public too. +void writeHTMLReport(FileID File, llvm::ArrayRef Roots, ASTContext &Ctx, + llvm::raw_ostream &OS); + } // namespace include_cleaner } // namespace clang diff --git a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt --- a/clang-tools-extra/include-cleaner/lib/CMakeLists.txt +++ b/clang-tools-extra/include-cleaner/lib/CMakeLists.txt @@ -1,6 +1,8 @@ set(LLVM_LINK_COMPONENTS Support) add_clang_library(clangIncludeCleaner + HTMLReport.cpp + Hooks.cpp WalkAST.cpp LINK_LIBS diff --git a/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp b/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/include-cleaner/lib/HTMLReport.cpp @@ -0,0 +1,198 @@ +//===--- HTMLReport.cpp - Explain the analysis for humans -----------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// If we're debugging this tool or trying to explain its conclusions, we need to +// be able to identify specific facts about the code and the inferences made. +// +// This library prints an annotated version of the code +// +//===----------------------------------------------------------------------===// + +#include "AnalysisInternal.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/Decl.h" +#include "clang/AST/PrettyPrinter.h" +#include "clang/Basic/SourceManager.h" +#include "clang/Lex/Lexer.h" +#include "llvm/Support/raw_ostream.h" + +namespace clang::include_cleaner { +namespace { + +constexpr llvm::StringLiteral CSS = R"css( + pre { line-height: 1.5em; } + .ref { text-decoration: underline; color: #008; } + .sel { position: relative; cursor: pointer; } + #hover { + background-color: #aaccff; border: 1px solid black; + z-index: 1; + position: absolute; top: 100%; left: 0; + font-family: sans-serif; + padding: 0.5em; + } + #hover p, #hover pre { margin: 0; } + #hover section header { font-weight: bold; } + #hover section:not(:first-child) { margin-top: 1em; } +)css"; + +constexpr llvm::StringLiteral JS = R"js( + // Recreate the #hover div inside whichever target .sel element was clicked. + function select(event) { + var target = event.target.closest('.sel'); + var hover = document.getElementById('hover'); + if (hover) { + if (hover.parentElement == target) return; + hover.parentNode.removeChild(hover); + } + if (target == null) return; + hover = document.createElement('div'); + hover.id = 'hover'; + fillHover(hover, target); + target.appendChild(hover); + } + // Fill the #hover div with the templates named by data-hover in the target. + function fillHover(hover, target) { + target.dataset.hover?.split(',').forEach(function(id) { + for (c of document.getElementById(id).content.childNodes) + hover.appendChild(c.cloneNode(true)); + }) + } +)js"; + +// Print the declaration tersely, but enough to identify e.g. which overload. +std::string printDecl(const NamedDecl &ND) { + std::string S; + llvm::raw_string_ostream OS(S); + PrintingPolicy PP = ND.getASTContext().getPrintingPolicy(); + PP.FullyQualifiedName = true; + PP.TerseOutput = true; + PP.SuppressInitializers = true; + ND.print(OS, PP); + llvm::erase_value(S, '\n'); + return S; +} + +class Reporter { + llvm::raw_ostream &OS; + const ASTContext &Ctx; + const SourceManager &SM; + FileID File; + + struct Target { + const NamedDecl *D; + }; + std::vector Targets; + std::vector> Refs; + +public: + Reporter(llvm::raw_ostream &OS, ASTContext &Ctx, FileID File) + : OS(OS), Ctx(Ctx), SM(Ctx.getSourceManager()), File(File) {} + + void addRef(SourceLocation Loc, const NamedDecl &D) { + auto Coords = SM.getDecomposedLoc(SM.getFileLoc(Loc)); + if (Coords.first != File) + llvm::errs() << "Ref location outside file!\n"; + Targets.push_back({&D}); + Refs.push_back({Coords.second, Targets.size() - 1}); + } + + void write() { + OS << "\n"; + OS << "\n"; + OS << "\n"; + OS << "\n"; + OS << "\n"; + for (unsigned I = 0; I < Targets.size(); ++I) { + OS << "\n"; + } + OS << "\n"; + OS << "\n"; + writeCode(); + OS << "\n"; + OS << "\n"; + } + +private: + void escapeChar(char C) { + switch (C) { + case '<': + OS << "<"; + break; + case '&': + OS << "&"; + break; + default: + OS << C; + } + } + + void escapeString(llvm::StringRef S) { + for (char C : S) + escapeChar(C); + } + + void writeTarget(const Target &T) { + OS << "
" << T.D->getDeclKindName() << " "; + escapeString(T.D->getQualifiedNameAsString()); + OS << "
"; + + OS << "

declared at "; + escapeString(SM.getFileLoc(T.D->getLocation()).printToString(SM)); + OS << "

";
+    escapeString(printDecl(*T.D));
+    OS << "
"; + } + + void writeCode() { + llvm::sort(Refs); + llvm::StringRef Code = SM.getBufferData(File); + + OS << "
";
+    auto Rest = llvm::makeArrayRef(Refs);
+    unsigned End = 0;
+    for (unsigned I = 0; I < Code.size(); ++I) {
+      if (End == I && I > 0) {
+        OS << "";
+        End = 0;
+      }
+      std::string TargetList;
+      Rest = Rest.drop_while([&](auto &R) {
+        if (R.first != I)
+          return false;
+        if (!TargetList.empty())
+          TargetList.push_back(',');
+        TargetList.push_back('t');
+        TargetList.append(std::to_string(R.second));
+        return true;
+      });
+      if (!TargetList.empty()) {
+        assert(End == 0 && "Overlapping tokens!");
+        OS << "";
+        End = I + Lexer::MeasureTokenLength(SM.getComposedLoc(File, I), SM,
+                                            Ctx.getLangOpts());
+      }
+      escapeChar(Code[I]);
+    }
+    OS << "
\n"; + } +}; + +} // namespace + +void writeHTMLReport(FileID File, llvm::ArrayRef Roots, ASTContext &Ctx, + llvm::raw_ostream &OS) { + Reporter R(OS, Ctx, File); + for (Decl *Root : Roots) + walkAST(*Root, + [&](SourceLocation Loc, const NamedDecl &D) { R.addRef(Loc, D); }); + R.write(); +} + +} // namespace clang::include_cleaner diff --git a/clang-tools-extra/include-cleaner/lib/Hooks.cpp b/clang-tools-extra/include-cleaner/lib/Hooks.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/include-cleaner/lib/Hooks.cpp @@ -0,0 +1,39 @@ +//===--- Hooks.h - Record compiler events ---------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang-include-cleaner/Hooks.h" +#include "clang/AST/ASTConsumer.h" +#include "clang/AST/ASTContext.h" +#include "clang/AST/DeclGroup.h" +#include "clang/Basic/SourceManager.h" + +namespace clang::include_cleaner { + +std::unique_ptr RecordedAST::record() { + class Recorder : public ASTConsumer { + RecordedAST *Out; + + public: + Recorder(RecordedAST *Out) : Out(Out) {} + void Initialize(ASTContext &Ctx) override { Out->Ctx = &Ctx; } + bool HandleTopLevelDecl(DeclGroupRef DG) override { + const auto &SM = Out->Ctx->getSourceManager(); + for (Decl *D : DG) { + if (!SM.isWrittenInMainFile(SM.getExpansionLoc(D->getLocation()))) + continue; + // FIXME: Filter out certain Obj-C and template-related decls. + Out->TopLevelDecls.push_back(D); + } + return ASTConsumer::HandleTopLevelDecl(DG); + } + }; + + return std::make_unique(this); +} + +} // namespace clang::include_cleaner diff --git a/clang-tools-extra/include-cleaner/tool/CMakeLists.txt b/clang-tools-extra/include-cleaner/tool/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/clang-tools-extra/include-cleaner/tool/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LLVM_LINK_COMPONENTS Support) + +include_directories("../lib") # FIXME: use public APIs instead. +add_clang_tool(clang-include-cleaner IncludeCleaner.cpp) +clang_target_link_libraries(clang-include-cleaner PRIVATE + clangBasic + clangTooling + ) +target_link_libraries(clang-include-cleaner PRIVATE + clangIncludeCleaner + ) + diff --git a/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp new file mode 100644 --- /dev/null +++ b/clang-tools-extra/include-cleaner/tool/IncludeCleaner.cpp @@ -0,0 +1,100 @@ +//===--- IncludeCleaner.cpp - standalone tool for include analysis --------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "AnalysisInternal.h" +#include "clang-include-cleaner/Hooks.h" +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Frontend/FrontendAction.h" +#include "clang/Tooling/CommonOptionsParser.h" +#include "clang/Tooling/Tooling.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Signals.h" +#include "llvm/Support/raw_ostream.h" + +namespace clang { +namespace include_cleaner { +namespace { +namespace cl = llvm::cl; + +llvm::StringRef Overview = llvm::StringLiteral(R"( +clang-include-cleaner analyzes the #include directives in source code. + +It suggests removing headers that the code is not using. +It suggests inserting headers that the code relies on, but does not include. +These changes make the file more self-contained and (at scale) make the codebase +easier to reason about and modify. + +The tool operates on *working* source code. This means it can suggest including +headers that are only indirectly included, but cannot suggest those that are +missing entirely. (clang-include-fixer can do this). +)") + .trim(); + +cl::OptionCategory IncludeCleaner("clang-include-cleaner"); + +cl::opt HTMLReportPath{ + "html", + cl::desc("Specify an output filename for an HTML report. " + "This describes both recommendations and reasons for changes."), + cl::cat(IncludeCleaner), +}; + +class HTMLReportAction : public clang::ASTFrontendAction { + RecordedAST AST; + + std::unique_ptr CreateASTConsumer(CompilerInstance &CI, + StringRef File) override { + return AST.record(); + } + + void EndSourceFile() override { + std::error_code EC; + llvm::raw_fd_ostream OS(HTMLReportPath, EC); + if (EC) { + llvm::errs() << "Unable to write HTML report to " << HTMLReportPath + << ": " << EC.message() << "\n"; + exit(1); + } + writeHTMLReport(AST.Ctx->getSourceManager().getMainFileID(), + AST.TopLevelDecls, *AST.Ctx, OS); + } +}; + +} // namespace +} // namespace include_cleaner +} // namespace clang + +int main(int argc, const char **argv) { + using namespace clang::include_cleaner; + + llvm::sys::PrintStackTraceOnErrorSignal(argv[0]); + auto OptionsParser = + clang::tooling::CommonOptionsParser::create(argc, argv, IncludeCleaner); + if (!OptionsParser) { + llvm::errs() << toString(OptionsParser.takeError()); + return 1; + } + + std::unique_ptr Factory; + if (HTMLReportPath.getNumOccurrences()) { + if (OptionsParser->getSourcePathList().size() != 1) { + llvm::errs() << "-" << HTMLReportPath.ArgStr + << " requires a single input file"; + return 1; + } + Factory = clang::tooling::newFrontendActionFactory(); + } else { + llvm::errs() << "Unimplemented\n"; + return 1; + } + + return clang::tooling::ClangTool(OptionsParser->getCompilations(), + OptionsParser->getSourcePathList()) + .run(Factory.get()); +}