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