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 | |