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