| 1 | //===--- IncludeCleanerCheck.cpp - clang-tidy -----------------------------===// |
| 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-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | |
| 9 | #include "IncludeCleanerCheck.h" |
| 10 | #include "../ClangTidyCheck.h" |
| 11 | #include "../ClangTidyDiagnosticConsumer.h" |
| 12 | #include "../ClangTidyOptions.h" |
| 13 | #include "../utils/OptionsUtils.h" |
| 14 | #include "clang-include-cleaner/Analysis.h" |
| 15 | #include "clang-include-cleaner/IncludeSpeller.h" |
| 16 | #include "clang-include-cleaner/Record.h" |
| 17 | #include "clang-include-cleaner/Types.h" |
| 18 | #include "clang/AST/ASTContext.h" |
| 19 | #include "clang/AST/Decl.h" |
| 20 | #include "clang/AST/DeclBase.h" |
| 21 | #include "clang/ASTMatchers/ASTMatchFinder.h" |
| 22 | #include "clang/ASTMatchers/ASTMatchers.h" |
| 23 | #include "clang/Basic/Diagnostic.h" |
| 24 | #include "clang/Basic/FileEntry.h" |
| 25 | #include "clang/Basic/LLVM.h" |
| 26 | #include "clang/Basic/LangOptions.h" |
| 27 | #include "clang/Basic/SourceLocation.h" |
| 28 | #include "clang/Format/Format.h" |
| 29 | #include "clang/Lex/Preprocessor.h" |
| 30 | #include "clang/Tooling/Core/Replacement.h" |
| 31 | #include "clang/Tooling/Inclusions/HeaderIncludes.h" |
| 32 | #include "clang/Tooling/Inclusions/StandardLibrary.h" |
| 33 | #include "llvm/ADT/DenseSet.h" |
| 34 | #include "llvm/ADT/STLExtras.h" |
| 35 | #include "llvm/ADT/SmallVector.h" |
| 36 | #include "llvm/ADT/StringRef.h" |
| 37 | #include "llvm/ADT/StringSet.h" |
| 38 | #include "llvm/Support/ErrorHandling.h" |
| 39 | #include "llvm/Support/Path.h" |
| 40 | #include "llvm/Support/Regex.h" |
| 41 | #include <optional> |
| 42 | #include <string> |
| 43 | #include <vector> |
| 44 | |
| 45 | using namespace clang::ast_matchers; |
| 46 | |
| 47 | namespace clang::tidy::misc { |
| 48 | |
| 49 | namespace { |
| 50 | struct MissingIncludeInfo { |
| 51 | include_cleaner::SymbolReference SymRef; |
| 52 | include_cleaner::Header Missing; |
| 53 | }; |
| 54 | } // namespace |
| 55 | |
| 56 | IncludeCleanerCheck::IncludeCleanerCheck(StringRef Name, |
| 57 | ClangTidyContext *Context) |
| 58 | : ClangTidyCheck(Name, Context), |
| 59 | IgnoreHeaders( |
| 60 | utils::options::parseStringList(Option: Options.get(LocalName: "IgnoreHeaders" , Default: "" ))), |
| 61 | DeduplicateFindings(Options.get(LocalName: "DeduplicateFindings" , Default: true)), |
| 62 | UnusedIncludes(Options.get(LocalName: "UnusedIncludes" , Default: true)), |
| 63 | MissingIncludes(Options.get(LocalName: "MissingIncludes" , Default: true)) { |
| 64 | for (const auto & : IgnoreHeaders) { |
| 65 | if (!llvm::Regex{Header}.isValid()) |
| 66 | configurationDiag(Description: "Invalid ignore headers regex '%0'" ) << Header; |
| 67 | std::string {Header.str()}; |
| 68 | if (!Header.ends_with(Suffix: "$" )) |
| 69 | HeaderSuffix += "$" ; |
| 70 | IgnoreHeadersRegex.emplace_back(Args&: HeaderSuffix); |
| 71 | } |
| 72 | |
| 73 | if (UnusedIncludes == false && MissingIncludes == false) |
| 74 | this->configurationDiag(Description: "The check 'misc-include-cleaner' will not " |
| 75 | "perform any analysis because 'UnusedIncludes' and " |
| 76 | "'MissingIncludes' are both false." ); |
| 77 | } |
| 78 | |
| 79 | void IncludeCleanerCheck::storeOptions(ClangTidyOptions::OptionMap &Opts) { |
| 80 | Options.store(Options&: Opts, LocalName: "IgnoreHeaders" , |
| 81 | Value: utils::options::serializeStringList(Strings: IgnoreHeaders)); |
| 82 | Options.store(Options&: Opts, LocalName: "DeduplicateFindings" , Value: DeduplicateFindings); |
| 83 | Options.store(Options&: Opts, LocalName: "UnusedIncludes" , Value: UnusedIncludes); |
| 84 | Options.store(Options&: Opts, LocalName: "MissingIncludes" , Value: MissingIncludes); |
| 85 | } |
| 86 | |
| 87 | bool IncludeCleanerCheck::isLanguageVersionSupported( |
| 88 | const LangOptions &LangOpts) const { |
| 89 | return !LangOpts.ObjC; |
| 90 | } |
| 91 | |
| 92 | void IncludeCleanerCheck::registerMatchers(MatchFinder *Finder) { |
| 93 | Finder->addMatcher(NodeMatch: translationUnitDecl().bind(ID: "top" ), Action: this); |
| 94 | } |
| 95 | |
| 96 | void IncludeCleanerCheck::registerPPCallbacks(const SourceManager &SM, |
| 97 | Preprocessor *PP, |
| 98 | Preprocessor *ModuleExpanderPP) { |
| 99 | PP->addPPCallbacks(C: RecordedPreprocessor.record(PP: *PP)); |
| 100 | this->PP = PP; |
| 101 | RecordedPI.record(P&: *PP); |
| 102 | } |
| 103 | |
| 104 | bool IncludeCleanerCheck::(const include_cleaner::Header &H) { |
| 105 | return llvm::any_of(Range&: IgnoreHeadersRegex, P: [&H](const llvm::Regex &R) { |
| 106 | switch (H.kind()) { |
| 107 | case include_cleaner::Header::Standard: |
| 108 | // We don't trim angle brackets around standard library headers |
| 109 | // deliberately, so that they are only matched as <vector>, otherwise |
| 110 | // having just `.*/vector` might yield false positives. |
| 111 | return R.match(String: H.standard().name()); |
| 112 | case include_cleaner::Header::Verbatim: |
| 113 | return R.match(String: H.verbatim().trim(Chars: "<>\"" )); |
| 114 | case include_cleaner::Header::Physical: |
| 115 | return R.match(String: H.physical().getFileEntry().tryGetRealPathName()); |
| 116 | } |
| 117 | llvm_unreachable("Unknown Header kind." ); |
| 118 | }); |
| 119 | } |
| 120 | |
| 121 | void IncludeCleanerCheck::check(const MatchFinder::MatchResult &Result) { |
| 122 | const SourceManager *SM = Result.SourceManager; |
| 123 | const FileEntry *MainFile = SM->getFileEntryForID(FID: SM->getMainFileID()); |
| 124 | llvm::DenseSet<const include_cleaner::Include *> Used; |
| 125 | std::vector<MissingIncludeInfo> Missing; |
| 126 | llvm::SmallVector<Decl *> MainFileDecls; |
| 127 | for (Decl *D : Result.Nodes.getNodeAs<TranslationUnitDecl>("top" )->decls()) { |
| 128 | if (!SM->isWrittenInMainFile(SM->getExpansionLoc(D->getLocation()))) |
| 129 | continue; |
| 130 | // FIXME: Filter out implicit template specializations. |
| 131 | MainFileDecls.push_back(D); |
| 132 | } |
| 133 | llvm::DenseSet<include_cleaner::Symbol> SeenSymbols; |
| 134 | OptionalDirectoryEntryRef ResourceDir = |
| 135 | PP->getHeaderSearchInfo().getModuleMap().getBuiltinDir(); |
| 136 | // FIXME: Find a way to have less code duplication between include-cleaner |
| 137 | // analysis implementation and the below code. |
| 138 | walkUsed(ASTRoots: MainFileDecls, MacroRefs: RecordedPreprocessor.MacroReferences, PI: &RecordedPI, |
| 139 | PP: *PP, |
| 140 | CB: [&](const include_cleaner::SymbolReference &Ref, |
| 141 | llvm::ArrayRef<include_cleaner::Header> Providers) { |
| 142 | // Process each symbol once to reduce noise in the findings. |
| 143 | // Tidy checks are used in two different workflows: |
| 144 | // - Ones that show all the findings for a given file. For such |
| 145 | // workflows there is not much point in showing all the occurences, |
| 146 | // as one is enough to indicate the issue. |
| 147 | // - Ones that show only the findings on changed pieces. For such |
| 148 | // workflows it's useful to show findings on every reference of a |
| 149 | // symbol as otherwise tools might give incosistent results |
| 150 | // depending on the parts of the file being edited. But it should |
| 151 | // still help surface findings for "new violations" (i.e. |
| 152 | // dependency did not exist in the code at all before). |
| 153 | if (DeduplicateFindings && !SeenSymbols.insert(V: Ref.Target).second) |
| 154 | return; |
| 155 | bool Satisfied = false; |
| 156 | for (const include_cleaner::Header &H : Providers) { |
| 157 | if (H.kind() == include_cleaner::Header::Physical && |
| 158 | (H.physical() == MainFile || |
| 159 | H.physical().getDir() == ResourceDir)) { |
| 160 | Satisfied = true; |
| 161 | continue; |
| 162 | } |
| 163 | |
| 164 | for (const include_cleaner::Include *I : |
| 165 | RecordedPreprocessor.Includes.match(H)) { |
| 166 | Used.insert(V: I); |
| 167 | Satisfied = true; |
| 168 | } |
| 169 | } |
| 170 | if (!Satisfied && !Providers.empty() && |
| 171 | Ref.RT == include_cleaner::RefType::Explicit && |
| 172 | !shouldIgnore(H: Providers.front())) |
| 173 | Missing.push_back(x: {.SymRef: Ref, .Missing: Providers.front()}); |
| 174 | }); |
| 175 | |
| 176 | std::vector<const include_cleaner::Include *> Unused; |
| 177 | for (const include_cleaner::Include &I : |
| 178 | RecordedPreprocessor.Includes.all()) { |
| 179 | if (Used.contains(V: &I) || !I.Resolved || I.Resolved->getDir() == ResourceDir) |
| 180 | continue; |
| 181 | if (RecordedPI.shouldKeep(FE: *I.Resolved)) |
| 182 | continue; |
| 183 | // Check if main file is the public interface for a private header. If so |
| 184 | // we shouldn't diagnose it as unused. |
| 185 | if (auto = RecordedPI.getPublic(File: *I.Resolved); !PHeader.empty()) { |
| 186 | PHeader = PHeader.trim(Chars: "<>\"" ); |
| 187 | // Since most private -> public mappings happen in a verbatim way, we |
| 188 | // check textually here. This might go wrong in presence of symlinks or |
| 189 | // header mappings. But that's not different than rest of the places. |
| 190 | if (getCurrentMainFile().ends_with(Suffix: PHeader)) |
| 191 | continue; |
| 192 | } |
| 193 | auto = tooling::stdlib::Header::named( |
| 194 | Name: I.quote(), Language: PP->getLangOpts().CPlusPlus ? tooling::stdlib::Lang::CXX |
| 195 | : tooling::stdlib::Lang::C); |
| 196 | if (StdHeader && shouldIgnore(H: *StdHeader)) |
| 197 | continue; |
| 198 | if (shouldIgnore(H: *I.Resolved)) |
| 199 | continue; |
| 200 | Unused.push_back(x: &I); |
| 201 | } |
| 202 | |
| 203 | llvm::StringRef Code = SM->getBufferData(FID: SM->getMainFileID()); |
| 204 | auto FileStyle = |
| 205 | format::getStyle(StyleName: format::DefaultFormatStyle, FileName: getCurrentMainFile(), |
| 206 | FallbackStyle: format::DefaultFallbackStyle, Code, |
| 207 | FS: &SM->getFileManager().getVirtualFileSystem()); |
| 208 | if (!FileStyle) |
| 209 | FileStyle = format::getLLVMStyle(); |
| 210 | |
| 211 | if (UnusedIncludes) { |
| 212 | for (const auto *Inc : Unused) { |
| 213 | diag(Loc: Inc->HashLocation, Description: "included header %0 is not used directly" ) |
| 214 | << llvm::sys::path::filename(path: Inc->Spelled, |
| 215 | style: llvm::sys::path::Style::posix) |
| 216 | << FixItHint::CreateRemoval(RemoveRange: CharSourceRange::getCharRange( |
| 217 | B: SM->translateLineCol(FID: SM->getMainFileID(), Line: Inc->Line, Col: 1), |
| 218 | E: SM->translateLineCol(FID: SM->getMainFileID(), Line: Inc->Line + 1, Col: 1))); |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | if (MissingIncludes) { |
| 223 | tooling::HeaderIncludes (getCurrentMainFile(), Code, |
| 224 | FileStyle->IncludeStyle); |
| 225 | // Deduplicate insertions when running in bulk fix mode. |
| 226 | llvm::StringSet<> {}; |
| 227 | for (const auto &Inc : Missing) { |
| 228 | std::string Spelling = include_cleaner::spellHeader( |
| 229 | Input: {.H: Inc.Missing, .HS: PP->getHeaderSearchInfo(), .Main: MainFile}); |
| 230 | bool Angled = llvm::StringRef{Spelling}.starts_with(Prefix: "<" ); |
| 231 | // We might suggest insertion of an existing include in edge cases, e.g., |
| 232 | // include is present in a PP-disabled region, or spelling of the header |
| 233 | // turns out to be the same as one of the unresolved includes in the |
| 234 | // main file. |
| 235 | if (auto Replacement = HeaderIncludes.insert( |
| 236 | Header: llvm::StringRef{Spelling}.trim(Chars: "\"<>" ), IsAngled: Angled, |
| 237 | Directive: tooling::IncludeDirective::Include)) { |
| 238 | DiagnosticBuilder DB = |
| 239 | diag(Loc: SM->getSpellingLoc(Loc: Inc.SymRef.RefLocation), |
| 240 | Description: "no header providing \"%0\" is directly included" ) |
| 241 | << Inc.SymRef.Target.name(); |
| 242 | if (areDiagsSelfContained() || |
| 243 | InsertedHeaders.insert(key: Replacement->getReplacementText()).second) { |
| 244 | DB << FixItHint::CreateInsertion( |
| 245 | InsertionLoc: SM->getComposedLoc(FID: SM->getMainFileID(), Offset: Replacement->getOffset()), |
| 246 | Code: Replacement->getReplacementText()); |
| 247 | } |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | } // namespace clang::tidy::misc |
| 254 | |