brintos

brintos / llvm-project-archived public Read only

0
0
Text · 14.2 KiB · fefbfc3 Raw
415 lines · cpp
1//===--- IncludeCleaner.cpp - standalone tool for include analysis --------===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#include "AnalysisInternal.h"10#include "clang-include-cleaner/Analysis.h"11#include "clang-include-cleaner/Record.h"12#include "clang/Frontend/CompilerInstance.h"13#include "clang/Frontend/FrontendAction.h"14#include "clang/Lex/Preprocessor.h"15#include "clang/Tooling/CommonOptionsParser.h"16#include "clang/Tooling/Tooling.h"17#include "llvm/ADT/STLFunctionalExtras.h"18#include "llvm/ADT/SmallVector.h"19#include "llvm/ADT/StringMap.h"20#include "llvm/ADT/StringRef.h"21#include "llvm/Support/CommandLine.h"22#include "llvm/Support/FormatVariadic.h"23#include "llvm/Support/Regex.h"24#include "llvm/Support/Signals.h"25#include "llvm/Support/raw_ostream.h"26#include <functional>27#include <memory>28#include <string>29#include <utility>30#include <vector>31 32namespace clang {33namespace include_cleaner {34namespace {35namespace cl = llvm::cl;36 37llvm::StringRef Overview = llvm::StringLiteral(R"(38clang-include-cleaner analyzes the #include directives in source code.39 40It suggests removing headers that the code is not using.41It suggests inserting headers that the code relies on, but does not include.42These changes make the file more self-contained and (at scale) make the codebase43easier to reason about and modify.44 45The tool operates on *working* source code. This means it can suggest including46headers that are only indirectly included, but cannot suggest those that are47missing entirely. (clang-include-fixer can do this).48)")49                               .trim();50 51cl::OptionCategory IncludeCleaner("clang-include-cleaner");52 53cl::opt<std::string> HTMLReportPath{54    "html",55    cl::desc("Specify an output filename for an HTML report. "56             "This describes both recommendations and reasons for changes."),57    cl::cat(IncludeCleaner),58};59 60cl::opt<std::string> OnlyHeaders{61    "only-headers",62    cl::desc("A comma-separated list of regexes to match against suffix of a "63             "header. Only headers that match will be analyzed."),64    cl::init(""),65    cl::cat(IncludeCleaner),66};67 68cl::opt<std::string> IgnoreHeaders{69    "ignore-headers",70    cl::desc("A comma-separated list of regexes to match against suffix of a "71             "header, and disable analysis if matched."),72    cl::init(""),73    cl::cat(IncludeCleaner),74};75 76enum class PrintStyle { Changes, Final };77cl::opt<PrintStyle> Print{78    "print",79    cl::values(80        clEnumValN(PrintStyle::Changes, "changes", "Print symbolic changes"),81        clEnumValN(PrintStyle::Final, "", "Print final code")),82    cl::ValueOptional,83    cl::init(PrintStyle::Final),84    cl::desc("Print the list of headers to insert and remove"),85    cl::cat(IncludeCleaner),86};87 88cl::opt<bool> Edit{89    "edit",90    cl::desc("Apply edits to analyzed source files"),91    cl::cat(IncludeCleaner),92};93cl::opt<bool> Insert{94    "insert",95    cl::desc(96        "Allow header insertions (deprecated. Use -disable-insert instead)"),97    cl::init(true),98    cl::cat(IncludeCleaner),99};100cl::opt<bool> Remove{101    "remove",102    cl::desc("Allow header removals (deprecated. Use -disable-remove instead)"),103    cl::init(true),104    cl::cat(IncludeCleaner),105};106cl::opt<bool> DisableInsert{107    "disable-insert",108    cl::desc("Disable header insertions"),109    cl::init(false),110    cl::cat(IncludeCleaner),111};112cl::opt<bool> DisableRemove{113    "disable-remove",114    cl::desc("Disable header removals"),115    cl::init(false),116    cl::cat(IncludeCleaner),117};118 119std::atomic<unsigned> Errors = ATOMIC_VAR_INIT(0);120 121format::FormatStyle getStyle(llvm::StringRef Filename) {122  auto S = format::getStyle(format::DefaultFormatStyle, Filename,123                            format::DefaultFallbackStyle);124  if (!S || !S->isCpp()) {125    consumeError(S.takeError());126    return format::getLLVMStyle();127  }128  return std::move(*S);129}130 131class Action : public clang::ASTFrontendAction {132public:133  Action(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter,134         llvm::StringMap<std::string> &EditedFiles)135      : HeaderFilter(HeaderFilter), EditedFiles(EditedFiles) {}136 137private:138  RecordedAST AST;139  RecordedPP PP;140  PragmaIncludes PI;141  llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;142  llvm::StringMap<std::string> &EditedFiles;143 144  bool BeginInvocation(CompilerInstance &CI) override {145    // We only perform include-cleaner analysis. So we disable diagnostics that146    // won't affect our analysis to make the tool more robust against147    // in-development code.148    CI.getLangOpts().ModulesDeclUse = false;149    CI.getLangOpts().ModulesStrictDeclUse = false;150    return true;151  }152 153  void ExecuteAction() override {154    const auto &CI = getCompilerInstance();155 156    // Disable all warnings when running include-cleaner, as we are only157    // interested in include-cleaner related findings. This makes the tool both158    // more resilient around in-development code, and possibly faster as we159    // skip some extra analysis.160    auto &Diags = CI.getDiagnostics();161    Diags.setEnableAllWarnings(false);162    Diags.setSeverityForAll(clang::diag::Flavor::WarningOrError,163                            clang::diag::Severity::Ignored);164    auto &P = CI.getPreprocessor();165    P.addPPCallbacks(PP.record(P));166    PI.record(getCompilerInstance());167    ASTFrontendAction::ExecuteAction();168  }169 170  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,171                                                 StringRef File) override {172    return AST.record();173  }174 175  void EndSourceFile() override {176    const auto &SM = getCompilerInstance().getSourceManager();177    if (SM.getDiagnostics().hasUncompilableErrorOccurred()) {178      llvm::errs()179          << "Skipping file " << getCurrentFile()180          << " due to compiler errors. clang-include-cleaner expects to "181             "work on compilable source code.\n";182      return;183    }184 185    if (!HTMLReportPath.empty())186      writeHTML();187 188    // Source File's path of compiler invocation, converted to absolute path.189    llvm::SmallString<256> AbsPath(190        SM.getFileEntryRefForID(SM.getMainFileID())->getName());191    assert(!AbsPath.empty() && "Main file path not known?");192    SM.getFileManager().makeAbsolutePath(AbsPath);193    llvm::StringRef Code = SM.getBufferData(SM.getMainFileID());194 195    auto Results =196        analyze(AST.Roots, PP.MacroReferences, PP.Includes, &PI,197                getCompilerInstance().getPreprocessor(), HeaderFilter);198 199    if (!Insert) {200      llvm::errs()201          << "warning: '-insert=0' is deprecated in favor of "202             "'-disable-insert'. "203             "The old flag was confusing since it suggested that inserts "204             "were disabled by default, when they were actually enabled.\n";205    }206 207    if (!Remove) {208      llvm::errs()209          << "warning: '-remove=0' is deprecated in favor of "210             "'-disable-remove'. "211             "The old flag was confusing since it suggested that removes "212             "were disabled by default, when they were actually enabled.\n";213    }214 215    if (!Insert || DisableInsert)216      Results.Missing.clear();217    if (!Remove || DisableRemove)218      Results.Unused.clear();219    std::string Final = fixIncludes(Results, AbsPath, Code, getStyle(AbsPath));220 221    if (Print.getNumOccurrences()) {222      switch (Print) {223      case PrintStyle::Changes:224        for (const Include *I : Results.Unused)225          llvm::outs() << "- " << I->quote() << " @Line:" << I->Line << "\n";226        for (const auto &[I, _] : Results.Missing)227          llvm::outs() << "+ " << I << "\n";228        break;229      case PrintStyle::Final:230        llvm::outs() << Final;231        break;232      }233    }234 235    if (!Results.Missing.empty() || !Results.Unused.empty())236      EditedFiles.try_emplace(AbsPath, Final);237  }238 239  void writeHTML() {240    std::error_code EC;241    llvm::raw_fd_ostream OS(HTMLReportPath, EC);242    if (EC) {243      llvm::errs() << "Unable to write HTML report to " << HTMLReportPath244                   << ": " << EC.message() << "\n";245      ++Errors;246      return;247    }248    writeHTMLReport(AST.Ctx->getSourceManager().getMainFileID(), PP.Includes,249                    AST.Roots, PP.MacroReferences, *AST.Ctx,250                    getCompilerInstance().getPreprocessor(), &PI, OS);251  }252};253class ActionFactory : public tooling::FrontendActionFactory {254public:255  ActionFactory(llvm::function_ref<bool(llvm::StringRef)> HeaderFilter)256      : HeaderFilter(HeaderFilter) {}257 258  std::unique_ptr<clang::FrontendAction> create() override {259    return std::make_unique<Action>(HeaderFilter, EditedFiles);260  }261 262  const llvm::StringMap<std::string> &editedFiles() const {263    return EditedFiles;264  }265 266private:267  llvm::function_ref<bool(llvm::StringRef)> HeaderFilter;268  // Map from file name to final code with the include edits applied.269  llvm::StringMap<std::string> EditedFiles;270};271 272// Compiles a regex list into a function that return true if any match a header.273// Prints and returns nullptr if any regexes are invalid.274std::function<bool(llvm::StringRef)> matchesAny(llvm::StringRef RegexFlag) {275  auto FilterRegs = std::make_shared<std::vector<llvm::Regex>>();276  llvm::SmallVector<llvm::StringRef> Headers;277  RegexFlag.split(Headers, ',', -1, /*KeepEmpty=*/false);278  for (auto HeaderPattern : Headers) {279    std::string AnchoredPattern = "(" + HeaderPattern.str() + ")$";280    llvm::Regex CompiledRegex(AnchoredPattern);281    std::string RegexError;282    if (!CompiledRegex.isValid(RegexError)) {283      llvm::errs() << llvm::formatv("Invalid regular expression '{0}': {1}\n",284                                    HeaderPattern, RegexError);285      return nullptr;286    }287    FilterRegs->push_back(std::move(CompiledRegex));288  }289  return [FilterRegs](llvm::StringRef Path) {290    for (const auto &F : *FilterRegs) {291      if (F.match(Path))292        return true;293    }294    return false;295  };296}297 298std::function<bool(llvm::StringRef)> headerFilter() {299  auto OnlyMatches = matchesAny(OnlyHeaders);300  auto IgnoreMatches = matchesAny(IgnoreHeaders);301  if (!OnlyMatches || !IgnoreMatches)302    return nullptr;303 304  return [OnlyMatches, IgnoreMatches](llvm::StringRef Header) {305    if (!OnlyHeaders.empty() && !OnlyMatches(Header))306      return true;307    if (!IgnoreHeaders.empty() && IgnoreMatches(Header))308      return true;309    return false;310  };311}312 313// Maps absolute path of each files of each compilation commands to the314// absolute path of the input file.315llvm::Expected<std::map<std::string, std::string, std::less<>>>316mapInputsToAbsPaths(clang::tooling::CompilationDatabase &CDB,317                    llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS,318                    const std::vector<std::string> &Inputs) {319  std::map<std::string, std::string, std::less<>> CDBToAbsPaths;320  // Factory.editedFiles()` will contain the final code, along with the321  // path given in the compilation database. That path can be322  // absolute or relative, and if it is relative, it is relative to the323  // "Directory" field in the compilation database. We need to make it324  // absolute to write the final code to the correct path.325  for (auto &Source : Inputs) {326    llvm::SmallString<256> AbsPath(Source);327    if (auto Err = VFS->makeAbsolute(AbsPath)) {328      llvm::errs() << "Failed to get absolute path for " << Source << " : "329                   << Err.message() << '\n';330      return llvm::errorCodeToError(Err);331    }332    std::vector<clang::tooling::CompileCommand> Cmds =333        CDB.getCompileCommands(AbsPath);334    if (Cmds.empty()) {335      // It should be found in the compilation database, even user didn't336      // specify the compilation database, the `FixedCompilationDatabase` will337      // create an entry from the arguments. So it is an error if we can't338      // find the compile commands.339      std::string ErrorMsg =340          llvm::formatv("No compile commands found for {0}", AbsPath).str();341      llvm::errs() << ErrorMsg << '\n';342      return llvm::make_error<llvm::StringError>(343          ErrorMsg, llvm::inconvertibleErrorCode());344    }345    for (const auto &Cmd : Cmds) {346      llvm::SmallString<256> CDBPath(Cmd.Filename);347      llvm::sys::path::make_absolute(Cmd.Directory, CDBPath);348      CDBToAbsPaths[std::string(CDBPath)] = std::string(AbsPath);349    }350  }351  return CDBToAbsPaths;352}353 354} // namespace355} // namespace include_cleaner356} // namespace clang357 358int main(int argc, const char **argv) {359  using namespace clang::include_cleaner;360 361  llvm::sys::PrintStackTraceOnErrorSignal(argv[0]);362  auto OptionsParser =363      clang::tooling::CommonOptionsParser::create(argc, argv, IncludeCleaner);364  if (!OptionsParser) {365    llvm::errs() << toString(OptionsParser.takeError());366    return 1;367  }368 369  if (OptionsParser->getSourcePathList().size() != 1) {370    std::vector<cl::Option *> IncompatibleFlags = {&HTMLReportPath, &Print};371    for (const auto *Flag : IncompatibleFlags) {372      if (Flag->getNumOccurrences()) {373        llvm::errs() << "-" << Flag->ArgStr << " requires a single input file";374        return 1;375      }376    }377  }378 379  auto VFS = llvm::vfs::getRealFileSystem();380  auto &CDB = OptionsParser->getCompilations();381  // CDBToAbsPaths is a map from the path in the compilation database to the382  // writable absolute path of the file.383  auto CDBToAbsPaths =384      mapInputsToAbsPaths(CDB, VFS, OptionsParser->getSourcePathList());385  if (!CDBToAbsPaths)386    return 1;387 388  clang::tooling::ClangTool Tool(CDB, OptionsParser->getSourcePathList());389 390  auto HeaderFilter = headerFilter();391  if (!HeaderFilter)392    return 1; // error already reported.393  ActionFactory Factory(HeaderFilter);394  auto ErrorCode = Tool.run(&Factory);395  if (Edit) {396    for (const auto &NameAndContent : Factory.editedFiles()) {397      llvm::StringRef FileName = NameAndContent.first();398      if (auto It = CDBToAbsPaths->find(FileName); It != CDBToAbsPaths->end())399        FileName = It->second;400 401      const std::string &FinalCode = NameAndContent.second;402      if (auto Err = llvm::writeToOutput(403              FileName, [&](llvm::raw_ostream &OS) -> llvm::Error {404                OS << FinalCode;405                return llvm::Error::success();406              })) {407        llvm::errs() << "Failed to apply edits to " << FileName << ": "408                     << toString(std::move(Err)) << "\n";409        ++Errors;410      }411    }412  }413  return ErrorCode || Errors != 0;414}415