File: | build/source/clang-tools-extra/clangd/IncludeCleaner.cpp |
Warning: | line 160, column 12 Called C++ object pointer is null |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
1 | //===--- IncludeCleaner.cpp - Unused/Missing Headers Analysis ---*- C++ -*-===// | |||
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 "IncludeCleaner.h" | |||
10 | #include "Config.h" | |||
11 | #include "Diagnostics.h" | |||
12 | #include "Headers.h" | |||
13 | #include "ParsedAST.h" | |||
14 | #include "Preamble.h" | |||
15 | #include "Protocol.h" | |||
16 | #include "SourceCode.h" | |||
17 | #include "URI.h" | |||
18 | #include "clang-include-cleaner/Analysis.h" | |||
19 | #include "clang-include-cleaner/Record.h" | |||
20 | #include "clang-include-cleaner/Types.h" | |||
21 | #include "support/Logger.h" | |||
22 | #include "support/Path.h" | |||
23 | #include "support/Trace.h" | |||
24 | #include "clang/AST/ASTContext.h" | |||
25 | #include "clang/AST/DeclCXX.h" | |||
26 | #include "clang/AST/Expr.h" | |||
27 | #include "clang/AST/ExprCXX.h" | |||
28 | #include "clang/AST/TemplateName.h" | |||
29 | #include "clang/AST/Type.h" | |||
30 | #include "clang/Basic/Diagnostic.h" | |||
31 | #include "clang/Basic/LLVM.h" | |||
32 | #include "clang/Basic/SourceLocation.h" | |||
33 | #include "clang/Basic/SourceManager.h" | |||
34 | #include "clang/Format/Format.h" | |||
35 | #include "clang/Lex/HeaderSearch.h" | |||
36 | #include "clang/Lex/Preprocessor.h" | |||
37 | #include "clang/Tooling/Core/Replacement.h" | |||
38 | #include "clang/Tooling/Inclusions/HeaderIncludes.h" | |||
39 | #include "clang/Tooling/Inclusions/StandardLibrary.h" | |||
40 | #include "clang/Tooling/Syntax/Tokens.h" | |||
41 | #include "llvm/ADT/ArrayRef.h" | |||
42 | #include "llvm/ADT/DenseSet.h" | |||
43 | #include "llvm/ADT/GenericUniformityImpl.h" | |||
44 | #include "llvm/ADT/STLExtras.h" | |||
45 | #include "llvm/ADT/STLFunctionalExtras.h" | |||
46 | #include "llvm/ADT/SmallString.h" | |||
47 | #include "llvm/ADT/SmallVector.h" | |||
48 | #include "llvm/ADT/StringMap.h" | |||
49 | #include "llvm/ADT/StringRef.h" | |||
50 | #include "llvm/ADT/StringSet.h" | |||
51 | #include "llvm/Support/Casting.h" | |||
52 | #include "llvm/Support/Error.h" | |||
53 | #include "llvm/Support/ErrorHandling.h" | |||
54 | #include "llvm/Support/FormatVariadic.h" | |||
55 | #include "llvm/Support/Path.h" | |||
56 | #include "llvm/Support/Regex.h" | |||
57 | #include <cassert> | |||
58 | #include <iterator> | |||
59 | #include <optional> | |||
60 | #include <string> | |||
61 | #include <utility> | |||
62 | #include <vector> | |||
63 | ||||
64 | namespace clang { | |||
65 | namespace clangd { | |||
66 | ||||
67 | static bool AnalyzeStdlib = false; | |||
68 | void setIncludeCleanerAnalyzesStdlib(bool B) { AnalyzeStdlib = B; } | |||
69 | ||||
70 | namespace { | |||
71 | ||||
72 | // Returns the range starting at '#' and ending at EOL. Escaped newlines are not | |||
73 | // handled. | |||
74 | clangd::Range getDiagnosticRange(llvm::StringRef Code, unsigned HashOffset) { | |||
75 | clangd::Range Result; | |||
76 | Result.end = Result.start = offsetToPosition(Code, HashOffset); | |||
77 | ||||
78 | // Span the warning until the EOL or EOF. | |||
79 | Result.end.character += | |||
80 | lspLength(Code.drop_front(HashOffset).take_until([](char C) { | |||
81 | return C == '\n' || C == '\r'; | |||
82 | })); | |||
83 | return Result; | |||
84 | } | |||
85 | ||||
86 | bool isFilteredByConfig(const Config &Cfg, llvm::StringRef HeaderPath) { | |||
87 | // Convert the path to Unix slashes and try to match against the filter. | |||
88 | llvm::SmallString<64> NormalizedPath(HeaderPath); | |||
89 | llvm::sys::path::native(NormalizedPath, llvm::sys::path::Style::posix); | |||
90 | for (auto &Filter : Cfg.Diagnostics.Includes.IgnoreHeader) { | |||
91 | if (Filter(NormalizedPath)) | |||
92 | return true; | |||
93 | } | |||
94 | return false; | |||
95 | } | |||
96 | ||||
97 | static bool mayConsiderUnused(const Inclusion &Inc, ParsedAST &AST, | |||
98 | const Config &Cfg, | |||
99 | const include_cleaner::PragmaIncludes *PI) { | |||
100 | // FIXME(kirillbobyrev): We currently do not support the umbrella headers. | |||
101 | // System headers are likely to be standard library headers. | |||
102 | // Until we have good support for umbrella headers, don't warn about them. | |||
103 | if (Inc.Written.front() == '<') { | |||
104 | if (AnalyzeStdlib && tooling::stdlib::Header::named(Inc.Written)) | |||
105 | return true; | |||
106 | return false; | |||
107 | } | |||
108 | assert(Inc.HeaderID)(static_cast <bool> (Inc.HeaderID) ? void (0) : __assert_fail ("Inc.HeaderID", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 108, __extension__ __PRETTY_FUNCTION__)); | |||
109 | auto HID = static_cast<IncludeStructure::HeaderID>(*Inc.HeaderID); | |||
110 | auto FE = AST.getSourceManager().getFileManager().getFileRef( | |||
111 | AST.getIncludeStructure().getRealPath(HID)); | |||
112 | assert(FE)(static_cast <bool> (FE) ? void (0) : __assert_fail ("FE" , "clang-tools-extra/clangd/IncludeCleaner.cpp", 112, __extension__ __PRETTY_FUNCTION__)); | |||
113 | if (PI) { | |||
114 | if (PI->shouldKeep(Inc.HashLine + 1)) | |||
115 | return false; | |||
116 | // Check if main file is the public interface for a private header. If so we | |||
117 | // shouldn't diagnose it as unused. | |||
118 | if (auto PHeader = PI->getPublic(*FE); !PHeader.empty()) { | |||
119 | PHeader = PHeader.trim("<>\""); | |||
120 | // Since most private -> public mappings happen in a verbatim way, we | |||
121 | // check textually here. This might go wrong in presence of symlinks or | |||
122 | // header mappings. But that's not different than rest of the places. | |||
123 | if (AST.tuPath().endswith(PHeader)) | |||
124 | return false; | |||
125 | } | |||
126 | } | |||
127 | // Headers without include guards have side effects and are not | |||
128 | // self-contained, skip them. | |||
129 | if (!AST.getPreprocessor().getHeaderSearchInfo().isFileMultipleIncludeGuarded( | |||
130 | &FE->getFileEntry())) { | |||
131 | dlog("{0} doesn't have header guard and will not be considered unused",do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} doesn't have header guard and will not be considered unused" , FE->getName()); } } while (false) | |||
132 | FE->getName())do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} doesn't have header guard and will not be considered unused" , FE->getName()); } } while (false); | |||
133 | return false; | |||
134 | } | |||
135 | ||||
136 | if (isFilteredByConfig(Cfg, Inc.Resolved)) { | |||
137 | dlog("{0} header is filtered out by the configuration", FE->getName())do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} header is filtered out by the configuration" , FE->getName()); } } while (false); | |||
138 | return false; | |||
139 | } | |||
140 | return true; | |||
141 | } | |||
142 | ||||
143 | llvm::StringRef getResolvedPath(const include_cleaner::Header &SymProvider) { | |||
144 | switch (SymProvider.kind()) { | |||
145 | case include_cleaner::Header::Physical: | |||
146 | return SymProvider.physical()->tryGetRealPathName(); | |||
147 | case include_cleaner::Header::Standard: | |||
148 | return SymProvider.standard().name().trim("<>\""); | |||
149 | case include_cleaner::Header::Verbatim: | |||
150 | return SymProvider.verbatim().trim("<>\""); | |||
151 | } | |||
152 | llvm_unreachable("Unknown header kind")::llvm::llvm_unreachable_internal("Unknown header kind", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 152); | |||
153 | } | |||
154 | ||||
155 | std::string getSymbolName(const include_cleaner::Symbol &Sym) { | |||
156 | switch (Sym.kind()) { | |||
157 | case include_cleaner::Symbol::Macro: | |||
158 | return Sym.macro().Name->getName().str(); | |||
159 | case include_cleaner::Symbol::Declaration: | |||
160 | return llvm::dyn_cast<NamedDecl>(&Sym.declaration()) | |||
| ||||
161 | ->getQualifiedNameAsString(); | |||
162 | } | |||
163 | llvm_unreachable("Unknown symbol kind")::llvm::llvm_unreachable_internal("Unknown symbol kind", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 163); | |||
164 | } | |||
165 | ||||
166 | std::vector<Diag> generateMissingIncludeDiagnostics( | |||
167 | ParsedAST &AST, llvm::ArrayRef<MissingIncludeDiagInfo> MissingIncludes, | |||
168 | llvm::StringRef Code) { | |||
169 | std::vector<Diag> Result; | |||
170 | const Config &Cfg = Config::current(); | |||
171 | if (Cfg.Diagnostics.MissingIncludes != Config::IncludesPolicy::Strict || | |||
172 | Cfg.Diagnostics.SuppressAll || | |||
173 | Cfg.Diagnostics.Suppress.contains("missing-includes")) { | |||
174 | return Result; | |||
175 | } | |||
176 | ||||
177 | const SourceManager &SM = AST.getSourceManager(); | |||
178 | const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID()); | |||
179 | ||||
180 | auto FileStyle = format::getStyle( | |||
181 | format::DefaultFormatStyle, AST.tuPath(), format::DefaultFallbackStyle, | |||
182 | Code, &SM.getFileManager().getVirtualFileSystem()); | |||
183 | if (!FileStyle) { | |||
184 | elog("Couldn't infer style", FileStyle.takeError()); | |||
185 | FileStyle = format::getLLVMStyle(); | |||
186 | } | |||
187 | ||||
188 | tooling::HeaderIncludes HeaderIncludes(AST.tuPath(), Code, | |||
189 | FileStyle->IncludeStyle); | |||
190 | for (const auto &SymbolWithMissingInclude : MissingIncludes) { | |||
191 | llvm::StringRef ResolvedPath = | |||
192 | getResolvedPath(SymbolWithMissingInclude.Providers.front()); | |||
193 | if (isFilteredByConfig(Cfg, ResolvedPath)) { | |||
194 | dlog("IncludeCleaner: not diagnosing missing include {0}, filtered by "do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "IncludeCleaner: not diagnosing missing include {0}, filtered by " "config", ResolvedPath); } } while (false) | |||
195 | "config",do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "IncludeCleaner: not diagnosing missing include {0}, filtered by " "config", ResolvedPath); } } while (false) | |||
196 | ResolvedPath)do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "IncludeCleaner: not diagnosing missing include {0}, filtered by " "config", ResolvedPath); } } while (false); | |||
197 | continue; | |||
198 | } | |||
199 | ||||
200 | std::string Spelling = | |||
201 | spellHeader(AST, MainFile, SymbolWithMissingInclude.Providers.front()); | |||
202 | llvm::StringRef HeaderRef{Spelling}; | |||
203 | bool Angled = HeaderRef.starts_with("<"); | |||
204 | // We might suggest insertion of an existing include in edge cases, e.g., | |||
205 | // include is present in a PP-disabled region, or spelling of the header | |||
206 | // turns out to be the same as one of the unresolved includes in the | |||
207 | // main file. | |||
208 | std::optional<tooling::Replacement> Replacement = HeaderIncludes.insert( | |||
209 | HeaderRef.trim("\"<>"), Angled, tooling::IncludeDirective::Include); | |||
210 | if (!Replacement.has_value()) | |||
211 | continue; | |||
212 | ||||
213 | Diag &D = Result.emplace_back(); | |||
214 | D.Message = | |||
215 | llvm::formatv("No header providing \"{0}\" is directly included", | |||
216 | getSymbolName(SymbolWithMissingInclude.Symbol)); | |||
217 | D.Name = "missing-includes"; | |||
218 | D.Source = Diag::DiagSource::Clangd; | |||
219 | D.File = AST.tuPath(); | |||
220 | D.InsideMainFile = true; | |||
221 | D.Severity = DiagnosticsEngine::Warning; | |||
222 | D.Range = clangd::Range{ | |||
223 | offsetToPosition(Code, | |||
224 | SymbolWithMissingInclude.SymRefRange.beginOffset()), | |||
225 | offsetToPosition(Code, | |||
226 | SymbolWithMissingInclude.SymRefRange.endOffset())}; | |||
227 | auto &F = D.Fixes.emplace_back(); | |||
228 | F.Message = "#include " + Spelling; | |||
229 | TextEdit Edit = replacementToEdit(Code, *Replacement); | |||
230 | F.Edits.emplace_back(std::move(Edit)); | |||
231 | } | |||
232 | return Result; | |||
233 | } | |||
234 | ||||
235 | std::vector<Diag> generateUnusedIncludeDiagnostics( | |||
236 | PathRef FileName, llvm::ArrayRef<const Inclusion *> UnusedIncludes, | |||
237 | llvm::StringRef Code) { | |||
238 | std::vector<Diag> Result; | |||
239 | const Config &Cfg = Config::current(); | |||
240 | if (Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::None || | |||
241 | Cfg.Diagnostics.SuppressAll || | |||
242 | Cfg.Diagnostics.Suppress.contains("unused-includes")) { | |||
243 | return Result; | |||
244 | } | |||
245 | for (const auto *Inc : UnusedIncludes) { | |||
246 | Diag &D = Result.emplace_back(); | |||
247 | D.Message = | |||
248 | llvm::formatv("included header {0} is not used directly", | |||
249 | llvm::sys::path::filename( | |||
250 | Inc->Written.substr(1, Inc->Written.size() - 2), | |||
251 | llvm::sys::path::Style::posix)); | |||
252 | D.Name = "unused-includes"; | |||
253 | D.Source = Diag::DiagSource::Clangd; | |||
254 | D.File = FileName; | |||
255 | D.InsideMainFile = true; | |||
256 | D.Severity = DiagnosticsEngine::Warning; | |||
257 | D.Tags.push_back(Unnecessary); | |||
258 | D.Range = getDiagnosticRange(Code, Inc->HashOffset); | |||
259 | // FIXME(kirillbobyrev): Removing inclusion might break the code if the | |||
260 | // used headers are only reachable transitively through this one. Suggest | |||
261 | // including them directly instead. | |||
262 | // FIXME(kirillbobyrev): Add fix suggestion for adding IWYU pragmas | |||
263 | // (keep/export) remove the warning once we support IWYU pragmas. | |||
264 | auto &F = D.Fixes.emplace_back(); | |||
265 | F.Message = "remove #include directive"; | |||
266 | F.Edits.emplace_back(); | |||
267 | F.Edits.back().range.start.line = Inc->HashLine; | |||
268 | F.Edits.back().range.end.line = Inc->HashLine + 1; | |||
269 | } | |||
270 | return Result; | |||
271 | } | |||
272 | } // namespace | |||
273 | ||||
274 | std::vector<include_cleaner::SymbolReference> | |||
275 | collectMacroReferences(ParsedAST &AST) { | |||
276 | const auto &SM = AST.getSourceManager(); | |||
277 | // FIXME: !!this is a hacky way to collect macro references. | |||
278 | std::vector<include_cleaner::SymbolReference> Macros; | |||
279 | auto &PP = AST.getPreprocessor(); | |||
280 | for (const syntax::Token &Tok : | |||
281 | AST.getTokens().spelledTokens(SM.getMainFileID())) { | |||
282 | auto Macro = locateMacroAt(Tok, PP); | |||
283 | if (!Macro) | |||
284 | continue; | |||
285 | if (auto DefLoc = Macro->Info->getDefinitionLoc(); DefLoc.isValid()) | |||
286 | Macros.push_back( | |||
287 | {Tok.location(), | |||
288 | include_cleaner::Macro{/*Name=*/PP.getIdentifierInfo(Tok.text(SM)), | |||
289 | DefLoc}, | |||
290 | include_cleaner::RefType::Explicit}); | |||
291 | } | |||
292 | return Macros; | |||
293 | } | |||
294 | ||||
295 | include_cleaner::Includes | |||
296 | convertIncludes(const SourceManager &SM, | |||
297 | const llvm::ArrayRef<Inclusion> Includes) { | |||
298 | include_cleaner::Includes ConvertedIncludes; | |||
299 | for (const Inclusion &Inc : Includes) { | |||
300 | include_cleaner::Include TransformedInc; | |||
301 | llvm::StringRef WrittenRef = llvm::StringRef(Inc.Written); | |||
302 | TransformedInc.Spelled = WrittenRef.trim("\"<>"); | |||
303 | TransformedInc.HashLocation = | |||
304 | SM.getComposedLoc(SM.getMainFileID(), Inc.HashOffset); | |||
305 | TransformedInc.Line = Inc.HashLine + 1; | |||
306 | TransformedInc.Angled = WrittenRef.starts_with("<"); | |||
307 | auto FE = SM.getFileManager().getFile(Inc.Resolved); | |||
308 | if (!FE) { | |||
309 | elog("IncludeCleaner: Failed to get an entry for resolved path {0}: {1}", | |||
310 | Inc.Resolved, FE.getError().message()); | |||
311 | continue; | |||
312 | } | |||
313 | TransformedInc.Resolved = *FE; | |||
314 | ConvertedIncludes.add(std::move(TransformedInc)); | |||
315 | } | |||
316 | return ConvertedIncludes; | |||
317 | } | |||
318 | ||||
319 | std::string spellHeader(ParsedAST &AST, const FileEntry *MainFile, | |||
320 | include_cleaner::Header Provider) { | |||
321 | if (Provider.kind() == include_cleaner::Header::Physical) { | |||
322 | if (auto CanonicalPath = getCanonicalPath(Provider.physical()->getLastRef(), | |||
323 | AST.getSourceManager())) { | |||
324 | std::string SpelledHeader = | |||
325 | llvm::cantFail(URI::includeSpelling(URI::create(*CanonicalPath))); | |||
326 | if (!SpelledHeader.empty()) | |||
327 | return SpelledHeader; | |||
328 | } | |||
329 | } | |||
330 | return include_cleaner::spellHeader( | |||
331 | Provider, AST.getPreprocessor().getHeaderSearchInfo(), MainFile); | |||
332 | } | |||
333 | ||||
334 | std::vector<const Inclusion *> | |||
335 | getUnused(ParsedAST &AST, | |||
336 | const llvm::DenseSet<IncludeStructure::HeaderID> &ReferencedFiles, | |||
337 | const llvm::StringSet<> &ReferencedPublicHeaders) { | |||
338 | trace::Span Tracer("IncludeCleaner::getUnused"); | |||
339 | const Config &Cfg = Config::current(); | |||
340 | std::vector<const Inclusion *> Unused; | |||
341 | for (const Inclusion &MFI : AST.getIncludeStructure().MainFileIncludes) { | |||
342 | if (!MFI.HeaderID) | |||
343 | continue; | |||
344 | if (ReferencedPublicHeaders.contains(MFI.Written)) | |||
345 | continue; | |||
346 | auto IncludeID = static_cast<IncludeStructure::HeaderID>(*MFI.HeaderID); | |||
347 | bool Used = ReferencedFiles.contains(IncludeID); | |||
348 | if (!Used && !mayConsiderUnused(MFI, AST, Cfg, AST.getPragmaIncludes())) { | |||
349 | dlog("{0} was not used, but is not eligible to be diagnosed as unused",do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} was not used, but is not eligible to be diagnosed as unused" , MFI.Written); } } while (false) | |||
350 | MFI.Written)do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} was not used, but is not eligible to be diagnosed as unused" , MFI.Written); } } while (false); | |||
351 | continue; | |||
352 | } | |||
353 | if (!Used) | |||
354 | Unused.push_back(&MFI); | |||
355 | dlog("{0} is {1}", MFI.Written, Used ? "USED" : "UNUSED")do { if (::llvm::DebugFlag && ::llvm::isCurrentDebugType (::clang::clangd::detail::debugType("clang-tools-extra/clangd/IncludeCleaner.cpp" ))) { ::clang::clangd::detail::log(Logger::Debug, "{0} is {1}" , MFI.Written, Used ? "USED" : "UNUSED"); } } while (false); | |||
356 | } | |||
357 | return Unused; | |||
358 | } | |||
359 | ||||
360 | IncludeCleanerFindings computeIncludeCleanerFindings(ParsedAST &AST) { | |||
361 | const auto &SM = AST.getSourceManager(); | |||
362 | const auto &Includes = AST.getIncludeStructure(); | |||
363 | include_cleaner::Includes ConvertedIncludes = | |||
364 | convertIncludes(SM, Includes.MainFileIncludes); | |||
365 | const FileEntry *MainFile = SM.getFileEntryForID(SM.getMainFileID()); | |||
366 | auto *PreamblePatch = PreamblePatch::getPatchEntry(AST.tuPath(), SM); | |||
367 | ||||
368 | std::vector<include_cleaner::SymbolReference> Macros = | |||
369 | collectMacroReferences(AST); | |||
370 | std::vector<MissingIncludeDiagInfo> MissingIncludes; | |||
371 | llvm::DenseSet<IncludeStructure::HeaderID> Used; | |||
372 | trace::Span Tracer("include_cleaner::walkUsed"); | |||
373 | include_cleaner::walkUsed( | |||
374 | AST.getLocalTopLevelDecls(), /*MacroRefs=*/Macros, | |||
375 | AST.getPragmaIncludes(), SM, | |||
376 | [&](const include_cleaner::SymbolReference &Ref, | |||
377 | llvm::ArrayRef<include_cleaner::Header> Providers) { | |||
378 | bool Satisfied = false; | |||
379 | for (const auto &H : Providers) { | |||
380 | if (H.kind() == include_cleaner::Header::Physical && | |||
381 | (H.physical() == MainFile || H.physical() == PreamblePatch)) { | |||
382 | Satisfied = true; | |||
383 | continue; | |||
384 | } | |||
385 | for (auto *Inc : ConvertedIncludes.match(H)) { | |||
386 | Satisfied = true; | |||
387 | auto HeaderID = Includes.getID(Inc->Resolved); | |||
388 | assert(HeaderID.has_value() &&(static_cast <bool> (HeaderID.has_value() && "ConvertedIncludes only contains resolved includes." ) ? void (0) : __assert_fail ("HeaderID.has_value() && \"ConvertedIncludes only contains resolved includes.\"" , "clang-tools-extra/clangd/IncludeCleaner.cpp", 389, __extension__ __PRETTY_FUNCTION__)) | |||
389 | "ConvertedIncludes only contains resolved includes.")(static_cast <bool> (HeaderID.has_value() && "ConvertedIncludes only contains resolved includes." ) ? void (0) : __assert_fail ("HeaderID.has_value() && \"ConvertedIncludes only contains resolved includes.\"" , "clang-tools-extra/clangd/IncludeCleaner.cpp", 389, __extension__ __PRETTY_FUNCTION__)); | |||
390 | Used.insert(*HeaderID); | |||
391 | } | |||
392 | } | |||
393 | ||||
394 | if (Satisfied || Providers.empty() || | |||
395 | Ref.RT != include_cleaner::RefType::Explicit) | |||
396 | return; | |||
397 | ||||
398 | // We actually always want to map usages to their spellings, but | |||
399 | // spelling locations can point into preamble section. Using these | |||
400 | // offsets could lead into crashes in presence of stale preambles. Hence | |||
401 | // we use "getFileLoc" instead to make sure it always points into main | |||
402 | // file. | |||
403 | // FIXME: Use presumed locations to map such usages back to patched | |||
404 | // locations safely. | |||
405 | auto Loc = SM.getFileLoc(Ref.RefLocation); | |||
406 | // File locations can be outside of the main file if macro is expanded | |||
407 | // through an #include. | |||
408 | while (SM.getFileID(Loc) != SM.getMainFileID()) | |||
409 | Loc = SM.getIncludeLoc(SM.getFileID(Loc)); | |||
410 | auto TouchingTokens = | |||
411 | syntax::spelledTokensTouching(Loc, AST.getTokens()); | |||
412 | assert(!TouchingTokens.empty())(static_cast <bool> (!TouchingTokens.empty()) ? void (0 ) : __assert_fail ("!TouchingTokens.empty()", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 412, __extension__ __PRETTY_FUNCTION__)); | |||
413 | // Loc points to the start offset of the ref token, here we use the last | |||
414 | // element of the TouchingTokens, e.g. avoid getting the "::" for | |||
415 | // "ns::^abc". | |||
416 | MissingIncludeDiagInfo DiagInfo{ | |||
417 | Ref.Target, TouchingTokens.back().range(SM), Providers}; | |||
418 | MissingIncludes.push_back(std::move(DiagInfo)); | |||
419 | }); | |||
420 | // Put possibly equal diagnostics together for deduplication. | |||
421 | // The duplicates might be from macro arguments that get expanded multiple | |||
422 | // times. | |||
423 | llvm::stable_sort(MissingIncludes, [](const MissingIncludeDiagInfo &LHS, | |||
424 | const MissingIncludeDiagInfo &RHS) { | |||
425 | // First sort by reference location. | |||
426 | if (LHS.SymRefRange != RHS.SymRefRange) { | |||
427 | // We can get away just by comparing the offsets as all the ranges are in | |||
428 | // main file. | |||
429 | return LHS.SymRefRange.beginOffset() < RHS.SymRefRange.beginOffset(); | |||
430 | } | |||
431 | // For the same location, break ties using the symbol. Note that this won't | |||
432 | // be stable across runs. | |||
433 | using MapInfo = llvm::DenseMapInfo<include_cleaner::Symbol>; | |||
434 | return MapInfo::getHashValue(LHS.Symbol) < | |||
435 | MapInfo::getHashValue(RHS.Symbol); | |||
436 | }); | |||
437 | MissingIncludes.erase(llvm::unique(MissingIncludes), MissingIncludes.end()); | |||
438 | std::vector<const Inclusion *> UnusedIncludes = | |||
439 | getUnused(AST, Used, /*ReferencedPublicHeaders*/ {}); | |||
440 | return {std::move(UnusedIncludes), std::move(MissingIncludes)}; | |||
441 | } | |||
442 | ||||
443 | Fix removeAllUnusedIncludes(llvm::ArrayRef<Diag> UnusedIncludes) { | |||
444 | assert(!UnusedIncludes.empty())(static_cast <bool> (!UnusedIncludes.empty()) ? void (0 ) : __assert_fail ("!UnusedIncludes.empty()", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 444, __extension__ __PRETTY_FUNCTION__)); | |||
445 | ||||
446 | Fix RemoveAll; | |||
447 | RemoveAll.Message = "remove all unused includes"; | |||
448 | for (const auto &Diag : UnusedIncludes) { | |||
449 | assert(Diag.Fixes.size() == 1 && "Expected exactly one fix.")(static_cast <bool> (Diag.Fixes.size() == 1 && "Expected exactly one fix." ) ? void (0) : __assert_fail ("Diag.Fixes.size() == 1 && \"Expected exactly one fix.\"" , "clang-tools-extra/clangd/IncludeCleaner.cpp", 449, __extension__ __PRETTY_FUNCTION__)); | |||
450 | RemoveAll.Edits.insert(RemoveAll.Edits.end(), | |||
451 | Diag.Fixes.front().Edits.begin(), | |||
452 | Diag.Fixes.front().Edits.end()); | |||
453 | } | |||
454 | ||||
455 | // TODO(hokein): emit a suitable text for the label. | |||
456 | ChangeAnnotation Annotation = {/*label=*/"", | |||
457 | /*needsConfirmation=*/true, | |||
458 | /*description=*/""}; | |||
459 | static const ChangeAnnotationIdentifier RemoveAllUnusedID = | |||
460 | "RemoveAllUnusedIncludes"; | |||
461 | for (unsigned I = 0; I < RemoveAll.Edits.size(); ++I) { | |||
462 | ChangeAnnotationIdentifier ID = RemoveAllUnusedID + std::to_string(I); | |||
463 | RemoveAll.Edits[I].annotationId = ID; | |||
464 | RemoveAll.Annotations.push_back({ID, Annotation}); | |||
465 | } | |||
466 | return RemoveAll; | |||
467 | } | |||
468 | Fix addAllMissingIncludes(llvm::ArrayRef<Diag> MissingIncludeDiags) { | |||
469 | assert(!MissingIncludeDiags.empty())(static_cast <bool> (!MissingIncludeDiags.empty()) ? void (0) : __assert_fail ("!MissingIncludeDiags.empty()", "clang-tools-extra/clangd/IncludeCleaner.cpp" , 469, __extension__ __PRETTY_FUNCTION__)); | |||
470 | ||||
471 | Fix AddAllMissing; | |||
472 | AddAllMissing.Message = "add all missing includes"; | |||
473 | // A map to deduplicate the edits with the same new text. | |||
474 | // newText (#include "my_missing_header.h") -> TextEdit. | |||
475 | llvm::StringMap<TextEdit> Edits; | |||
476 | for (const auto &Diag : MissingIncludeDiags) { | |||
477 | assert(Diag.Fixes.size() == 1 && "Expected exactly one fix.")(static_cast <bool> (Diag.Fixes.size() == 1 && "Expected exactly one fix." ) ? void (0) : __assert_fail ("Diag.Fixes.size() == 1 && \"Expected exactly one fix.\"" , "clang-tools-extra/clangd/IncludeCleaner.cpp", 477, __extension__ __PRETTY_FUNCTION__)); | |||
478 | for (const auto& Edit : Diag.Fixes.front().Edits) { | |||
479 | Edits.try_emplace(Edit.newText, Edit); | |||
480 | } | |||
481 | } | |||
482 | // FIXME(hokein): emit used symbol reference in the annotation. | |||
483 | ChangeAnnotation Annotation = {/*label=*/"", | |||
484 | /*needsConfirmation=*/true, | |||
485 | /*description=*/""}; | |||
486 | static const ChangeAnnotationIdentifier AddAllMissingID = | |||
487 | "AddAllMissingIncludes"; | |||
488 | unsigned I = 0; | |||
489 | for (auto &It : Edits) { | |||
490 | ChangeAnnotationIdentifier ID = AddAllMissingID + std::to_string(I++); | |||
491 | AddAllMissing.Edits.push_back(std::move(It.getValue())); | |||
492 | AddAllMissing.Edits.back().annotationId = ID; | |||
493 | ||||
494 | AddAllMissing.Annotations.push_back({ID, Annotation}); | |||
495 | } | |||
496 | return AddAllMissing; | |||
497 | } | |||
498 | Fix fixAll(const Fix& RemoveAllUnused, const Fix& AddAllMissing) { | |||
499 | Fix FixAll; | |||
500 | FixAll.Message = "fix all includes"; | |||
501 | ||||
502 | for (const auto &F : RemoveAllUnused.Edits) | |||
503 | FixAll.Edits.push_back(F); | |||
504 | for (const auto &F : AddAllMissing.Edits) | |||
505 | FixAll.Edits.push_back(F); | |||
506 | ||||
507 | for (const auto& A : RemoveAllUnused.Annotations) | |||
508 | FixAll.Annotations.push_back(A); | |||
509 | for (const auto& A : AddAllMissing.Annotations) | |||
510 | FixAll.Annotations.push_back(A); | |||
511 | return FixAll; | |||
512 | } | |||
513 | ||||
514 | std::vector<Diag> generateIncludeCleanerDiagnostic( | |||
515 | ParsedAST &AST, const IncludeCleanerFindings &Findings, | |||
516 | llvm::StringRef Code) { | |||
517 | std::vector<Diag> UnusedIncludes = generateUnusedIncludeDiagnostics( | |||
518 | AST.tuPath(), Findings.UnusedIncludes, Code); | |||
519 | std::optional<Fix> RemoveAllUnused;; | |||
520 | if (UnusedIncludes.size() > 1) | |||
521 | RemoveAllUnused = removeAllUnusedIncludes(UnusedIncludes); | |||
522 | ||||
523 | std::vector<Diag> MissingIncludeDiags = generateMissingIncludeDiagnostics( | |||
524 | AST, Findings.MissingIncludes, Code); | |||
525 | std::optional<Fix> AddAllMissing; | |||
526 | if (MissingIncludeDiags.size() > 1) | |||
527 | AddAllMissing = addAllMissingIncludes(MissingIncludeDiags); | |||
528 | ||||
529 | std::optional<Fix> FixAll; | |||
530 | if (RemoveAllUnused && AddAllMissing) | |||
531 | FixAll = fixAll(*RemoveAllUnused, *AddAllMissing); | |||
532 | ||||
533 | auto AddBatchFix = [](const std::optional<Fix> &F, clang::clangd::Diag *Out) { | |||
534 | if (!F) return; | |||
535 | Out->Fixes.push_back(*F); | |||
536 | }; | |||
537 | for (auto &Diag : MissingIncludeDiags) { | |||
538 | AddBatchFix(AddAllMissing, &Diag); | |||
539 | AddBatchFix(FixAll, &Diag); | |||
540 | } | |||
541 | for (auto &Diag : UnusedIncludes) { | |||
542 | AddBatchFix(RemoveAllUnused, &Diag); | |||
543 | AddBatchFix(FixAll, &Diag); | |||
544 | } | |||
545 | ||||
546 | auto Result = std::move(MissingIncludeDiags); | |||
547 | llvm::move(UnusedIncludes, | |||
548 | std::back_inserter(Result)); | |||
549 | return Result; | |||
550 | } | |||
551 | ||||
552 | std::vector<Diag> issueIncludeCleanerDiagnostics(ParsedAST &AST, | |||
553 | llvm::StringRef Code) { | |||
554 | // Interaction is only polished for C/CPP. | |||
555 | if (AST.getLangOpts().ObjC) | |||
| ||||
556 | return {}; | |||
557 | ||||
558 | trace::Span Tracer("IncludeCleaner::issueIncludeCleanerDiagnostics"); | |||
559 | ||||
560 | const Config &Cfg = Config::current(); | |||
561 | IncludeCleanerFindings Findings; | |||
562 | if (Cfg.Diagnostics.MissingIncludes == Config::IncludesPolicy::Strict || | |||
563 | Cfg.Diagnostics.UnusedIncludes == Config::IncludesPolicy::Strict) { | |||
564 | // will need include-cleaner results, call it once | |||
565 | Findings = computeIncludeCleanerFindings(AST); | |||
566 | } | |||
567 | return generateIncludeCleanerDiagnostic(AST, Findings, Code); | |||
568 | } | |||
569 | ||||
570 | std::optional<include_cleaner::Header> | |||
571 | firstMatchedProvider(const include_cleaner::Includes &Includes, | |||
572 | llvm::ArrayRef<include_cleaner::Header> Providers) { | |||
573 | for (const auto &H : Providers) { | |||
574 | if (!Includes.match(H).empty()) | |||
575 | return H; | |||
576 | } | |||
577 | // No match for this provider in the includes list. | |||
578 | return std::nullopt; | |||
579 | } | |||
580 | } // namespace clangd | |||
581 | } // namespace clang |