//===--- IncludeCleanerCheck.cpp - clang-tidy -----------------------------===// // // 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 "IncludeCleanerCheck.h" #include "../ClangTidyCheck.h" #include "../ClangTidyDiagnosticConsumer.h" #include "../ClangTidyOptions.h" #include "../utils/OptionsUtils.h" #include "clang-include-cleaner/Analysis.h" #include "clang-include-cleaner/IncludeSpeller.h" #include "clang-include-cleaner/Record.h" #include "clang-include-cleaner/Types.h" #include "clang/AST/ASTContext.h" #include "clang/AST/Decl.h" #include "clang/AST/DeclBase.h" #include "clang/ASTMatchers/ASTMatchFinder.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/FileEntry.h" #include "clang/Basic/LLVM.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/SourceLocation.h" #include "clang/Format/Format.h" #include "clang/Lex/HeaderSearchOptions.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Inclusions/HeaderIncludes.h" #include "clang/Tooling/Inclusions/StandardLibrary.h" #include "llvm/ADT/DenseSet.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include "llvm/Support/ErrorHandling.h" #include "llvm/Support/Path.h" #include "llvm/Support/Regex.h" #include #include #include using namespace clang::ast_matchers; namespace clang::tidy::misc { namespace { struct MissingIncludeInfo { include_cleaner::SymbolReference SymRef; include_cleaner::Header Missing; }; } // namespace IncludeCleanerCheck::IncludeCleanerCheck(StringRef Name, ClangTidyContext *Context) : ClangTidyCheck(Name, Context), IgnoreHeaders(utils::options::parseStringList( Options.getLocalOrGlobal("IgnoreHeaders", ""))), DeduplicateFindings( Options.getLocalOrGlobal("DeduplicateFindings", true)) { for (const auto &Header : IgnoreHeaders) { if (!llvm::Regex{Header}.isValid()) configurationDiag("Invalid ignore headers regex '%0'") << Header; std::string HeaderSuffix{Header.str()}; if (!Header.ends_with("$")) HeaderSuffix += "$"; IgnoreHeadersRegex.emplace_back(HeaderSuffix); } } void IncludeCleanerCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { Options.store(Opts, "IgnoreHeaders", utils::options::serializeStringList(IgnoreHeaders)); Options.store(Opts, "DeduplicateFindings", DeduplicateFindings); } bool IncludeCleanerCheck::isLanguageVersionSupported( const LangOptions &LangOpts) const { return !LangOpts.ObjC; } void IncludeCleanerCheck::registerMatchers(MatchFinder *Finder) { Finder->addMatcher(translationUnitDecl().bind("top"), this); } void IncludeCleanerCheck::registerPPCallbacks(const SourceManager &SM, Preprocessor *PP, Preprocessor *ModuleExpanderPP) { PP->addPPCallbacks(RecordedPreprocessor.record(*PP)); this->PP = PP; RecordedPI.record(*PP); } bool IncludeCleanerCheck::shouldIgnore(const include_cleaner::Header &H) { return llvm::any_of(IgnoreHeadersRegex, [&H](const llvm::Regex &R) { switch (H.kind()) { case include_cleaner::Header::Standard: // We don't trim angle brackets around standard library headers // deliberately, so that they are only matched as , otherwise // having just `.*/vector` might yield false positives. return R.match(H.standard().name()); case include_cleaner::Header::Verbatim: return R.match(H.verbatim().trim("<>\"")); case include_cleaner::Header::Physical: return R.match(H.physical().getFileEntry().tryGetRealPathName()); } llvm_unreachable("Unknown Header kind."); }); } void IncludeCleanerCheck::check(const MatchFinder::MatchResult &Result) { const SourceManager *SM = Result.SourceManager; const FileEntry *MainFile = SM->getFileEntryForID(SM->getMainFileID()); llvm::DenseSet Used; std::vector Missing; llvm::SmallVector MainFileDecls; for (Decl *D : Result.Nodes.getNodeAs("top")->decls()) { if (!SM->isWrittenInMainFile(SM->getExpansionLoc(D->getLocation()))) continue; // FIXME: Filter out implicit template specializations. MainFileDecls.push_back(D); } llvm::DenseSet SeenSymbols; OptionalDirectoryEntryRef ResourceDir = PP->getHeaderSearchInfo().getModuleMap().getBuiltinDir(); // FIXME: Find a way to have less code duplication between include-cleaner // analysis implementation and the below code. walkUsed(MainFileDecls, RecordedPreprocessor.MacroReferences, &RecordedPI, *PP, [&](const include_cleaner::SymbolReference &Ref, llvm::ArrayRef Providers) { // Process each symbol once to reduce noise in the findings. // Tidy checks are used in two different workflows: // - Ones that show all the findings for a given file. For such // workflows there is not much point in showing all the occurences, // as one is enough to indicate the issue. // - Ones that show only the findings on changed pieces. For such // workflows it's useful to show findings on every reference of a // symbol as otherwise tools might give incosistent results // depending on the parts of the file being edited. But it should // still help surface findings for "new violations" (i.e. // dependency did not exist in the code at all before). if (DeduplicateFindings && !SeenSymbols.insert(Ref.Target).second) return; bool Satisfied = false; for (const include_cleaner::Header &H : Providers) { if (H.kind() == include_cleaner::Header::Physical && (H.physical() == MainFile || H.physical().getDir() == ResourceDir)) { Satisfied = true; continue; } for (const include_cleaner::Include *I : RecordedPreprocessor.Includes.match(H)) { Used.insert(I); Satisfied = true; } } if (!Satisfied && !Providers.empty() && Ref.RT == include_cleaner::RefType::Explicit && !shouldIgnore(Providers.front())) Missing.push_back({Ref, Providers.front()}); }); std::vector Unused; for (const include_cleaner::Include &I : RecordedPreprocessor.Includes.all()) { if (Used.contains(&I) || !I.Resolved || I.Resolved->getDir() == ResourceDir) continue; if (RecordedPI.shouldKeep(*I.Resolved)) continue; // Check if main file is the public interface for a private header. If so // we shouldn't diagnose it as unused. if (auto PHeader = RecordedPI.getPublic(*I.Resolved); !PHeader.empty()) { PHeader = PHeader.trim("<>\""); // Since most private -> public mappings happen in a verbatim way, we // check textually here. This might go wrong in presence of symlinks or // header mappings. But that's not different than rest of the places. if (getCurrentMainFile().ends_with(PHeader)) continue; } auto StdHeader = tooling::stdlib::Header::named( I.quote(), PP->getLangOpts().CPlusPlus ? tooling::stdlib::Lang::CXX : tooling::stdlib::Lang::C); if (StdHeader && shouldIgnore(*StdHeader)) continue; if (shouldIgnore(*I.Resolved)) continue; Unused.push_back(&I); } llvm::StringRef Code = SM->getBufferData(SM->getMainFileID()); auto FileStyle = format::getStyle(format::DefaultFormatStyle, getCurrentMainFile(), format::DefaultFallbackStyle, Code, &SM->getFileManager().getVirtualFileSystem()); if (!FileStyle) FileStyle = format::getLLVMStyle(); for (const auto *Inc : Unused) { diag(Inc->HashLocation, "included header %0 is not used directly") << llvm::sys::path::filename(Inc->Spelled, llvm::sys::path::Style::posix) << FixItHint::CreateRemoval(CharSourceRange::getCharRange( SM->translateLineCol(SM->getMainFileID(), Inc->Line, 1), SM->translateLineCol(SM->getMainFileID(), Inc->Line + 1, 1))); } tooling::HeaderIncludes HeaderIncludes(getCurrentMainFile(), Code, FileStyle->IncludeStyle); // Deduplicate insertions when running in bulk fix mode. llvm::StringSet<> InsertedHeaders{}; for (const auto &Inc : Missing) { std::string Spelling = include_cleaner::spellHeader( {Inc.Missing, PP->getHeaderSearchInfo(), MainFile}); bool Angled = llvm::StringRef{Spelling}.starts_with("<"); // We might suggest insertion of an existing include in edge cases, e.g., // include is present in a PP-disabled region, or spelling of the header // turns out to be the same as one of the unresolved includes in the // main file. if (auto Replacement = HeaderIncludes.insert(llvm::StringRef{Spelling}.trim("\"<>"), Angled, tooling::IncludeDirective::Include)) { DiagnosticBuilder DB = diag(SM->getSpellingLoc(Inc.SymRef.RefLocation), "no header providing \"%0\" is directly included") << Inc.SymRef.Target.name(); if (areDiagsSelfContained() || InsertedHeaders.insert(Replacement->getReplacementText()).second) { DB << FixItHint::CreateInsertion( SM->getComposedLoc(SM->getMainFileID(), Replacement->getOffset()), Replacement->getReplacementText()); } } } } } // namespace clang::tidy::misc