|  | //===--- 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 <optional> | 
|  | #include <string> | 
|  | #include <vector> | 
|  |  | 
|  | 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 <vector>, 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<const include_cleaner::Include *> Used; | 
|  | std::vector<MissingIncludeInfo> Missing; | 
|  | llvm::SmallVector<Decl *> MainFileDecls; | 
|  | for (Decl *D : Result.Nodes.getNodeAs<TranslationUnitDecl>("top")->decls()) { | 
|  | if (!SM->isWrittenInMainFile(SM->getExpansionLoc(D->getLocation()))) | 
|  | continue; | 
|  | // FIXME: Filter out implicit template specializations. | 
|  | MainFileDecls.push_back(D); | 
|  | } | 
|  | llvm::DenseSet<include_cleaner::Symbol> 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<include_cleaner::Header> 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<const include_cleaner::Include *> 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 |