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