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
45using namespace clang::ast_matchers;
46
47namespace clang::tidy::misc {
48
49namespace {
50struct MissingIncludeInfo {
51 include_cleaner::SymbolReference SymRef;
52 include_cleaner::Header Missing;
53};
54} // namespace
55
56IncludeCleanerCheck::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 &Header : IgnoreHeaders) {
65 if (!llvm::Regex{Header}.isValid())
66 configurationDiag(Description: "Invalid ignore headers regex '%0'") << Header;
67 std::string HeaderSuffix{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
79void 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
87bool IncludeCleanerCheck::isLanguageVersionSupported(
88 const LangOptions &LangOpts) const {
89 return !LangOpts.ObjC;
90}
91
92void IncludeCleanerCheck::registerMatchers(MatchFinder *Finder) {
93 Finder->addMatcher(NodeMatch: translationUnitDecl().bind(ID: "top"), Action: this);
94}
95
96void 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
104bool IncludeCleanerCheck::shouldIgnore(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
121void 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 PHeader = 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 StdHeader = 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 HeaderIncludes(getCurrentMainFile(), Code,
224 FileStyle->IncludeStyle);
225 // Deduplicate insertions when running in bulk fix mode.
226 llvm::StringSet<> InsertedHeaders{};
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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

source code of clang-tools-extra/clang-tidy/misc/IncludeCleanerCheck.cpp