| 1 | //===--- Diagnostics.cpp -----------------------------------------*- 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 "Diagnostics.h" |
| 10 | #include "../clang-tidy/ClangTidyDiagnosticConsumer.h" |
| 11 | #include "Compiler.h" |
| 12 | #include "Protocol.h" |
| 13 | #include "SourceCode.h" |
| 14 | #include "support/Logger.h" |
| 15 | #include "clang/Basic/AllDiagnostics.h" // IWYU pragma: keep |
| 16 | #include "clang/Basic/Diagnostic.h" |
| 17 | #include "clang/Basic/DiagnosticIDs.h" |
| 18 | #include "clang/Basic/LLVM.h" |
| 19 | #include "clang/Basic/SourceLocation.h" |
| 20 | #include "clang/Basic/SourceManager.h" |
| 21 | #include "clang/Basic/TokenKinds.h" |
| 22 | #include "clang/Lex/Lexer.h" |
| 23 | #include "clang/Lex/Token.h" |
| 24 | #include "llvm/ADT/ArrayRef.h" |
| 25 | #include "llvm/ADT/DenseSet.h" |
| 26 | #include "llvm/ADT/STLExtras.h" |
| 27 | #include "llvm/ADT/STLFunctionalExtras.h" |
| 28 | #include "llvm/ADT/ScopeExit.h" |
| 29 | #include "llvm/ADT/SmallString.h" |
| 30 | #include "llvm/ADT/SmallVector.h" |
| 31 | #include "llvm/ADT/StringExtras.h" |
| 32 | #include "llvm/ADT/StringRef.h" |
| 33 | #include "llvm/ADT/StringSet.h" |
| 34 | #include "llvm/ADT/Twine.h" |
| 35 | #include "llvm/Support/ErrorHandling.h" |
| 36 | #include "llvm/Support/FormatVariadic.h" |
| 37 | #include "llvm/Support/Path.h" |
| 38 | #include "llvm/Support/SourceMgr.h" |
| 39 | #include "llvm/Support/raw_ostream.h" |
| 40 | #include <algorithm> |
| 41 | #include <cassert> |
| 42 | #include <optional> |
| 43 | #include <set> |
| 44 | #include <string> |
| 45 | #include <tuple> |
| 46 | #include <utility> |
| 47 | #include <vector> |
| 48 | |
| 49 | namespace clang { |
| 50 | namespace clangd { |
| 51 | namespace { |
| 52 | |
| 53 | const char *getDiagnosticCode(unsigned ID) { |
| 54 | switch (ID) { |
| 55 | #define DIAG(ENUM, CLASS, DEFAULT_MAPPING, DESC, GROPU, SFINAE, NOWERROR, \ |
| 56 | SHOWINSYSHEADER, SHOWINSYSMACRO, DEFERRABLE, CATEGORY) \ |
| 57 | case clang::diag::ENUM: \ |
| 58 | return #ENUM; |
| 59 | #include "clang/Basic/DiagnosticASTKinds.inc" |
| 60 | #include "clang/Basic/DiagnosticAnalysisKinds.inc" |
| 61 | #include "clang/Basic/DiagnosticCommentKinds.inc" |
| 62 | #include "clang/Basic/DiagnosticCommonKinds.inc" |
| 63 | #include "clang/Basic/DiagnosticDriverKinds.inc" |
| 64 | #include "clang/Basic/DiagnosticFrontendKinds.inc" |
| 65 | #include "clang/Basic/DiagnosticLexKinds.inc" |
| 66 | #include "clang/Basic/DiagnosticParseKinds.inc" |
| 67 | #include "clang/Basic/DiagnosticRefactoringKinds.inc" |
| 68 | #include "clang/Basic/DiagnosticSemaKinds.inc" |
| 69 | #include "clang/Basic/DiagnosticSerializationKinds.inc" |
| 70 | #undef DIAG |
| 71 | default: |
| 72 | return nullptr; |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | bool mentionsMainFile(const Diag &D) { |
| 77 | if (D.InsideMainFile) |
| 78 | return true; |
| 79 | // Fixes are always in the main file. |
| 80 | if (!D.Fixes.empty()) |
| 81 | return true; |
| 82 | for (auto &N : D.Notes) { |
| 83 | if (N.InsideMainFile) |
| 84 | return true; |
| 85 | } |
| 86 | return false; |
| 87 | } |
| 88 | |
| 89 | bool isExcluded(unsigned DiagID) { |
| 90 | // clang will always fail parsing MS ASM, we don't link in desc + asm parser. |
| 91 | if (DiagID == clang::diag::err_msasm_unable_to_create_target || |
| 92 | DiagID == clang::diag::err_msasm_unsupported_arch) |
| 93 | return true; |
| 94 | return false; |
| 95 | } |
| 96 | |
| 97 | // Checks whether a location is within a half-open range. |
| 98 | // Note that clang also uses closed source ranges, which this can't handle! |
| 99 | bool locationInRange(SourceLocation L, CharSourceRange R, |
| 100 | const SourceManager &M) { |
| 101 | assert(R.isCharRange()); |
| 102 | if (!R.isValid() || M.getFileID(SpellingLoc: R.getBegin()) != M.getFileID(SpellingLoc: R.getEnd()) || |
| 103 | M.getFileID(SpellingLoc: R.getBegin()) != M.getFileID(SpellingLoc: L)) |
| 104 | return false; |
| 105 | return L != R.getEnd() && M.isPointWithin(Location: L, Start: R.getBegin(), End: R.getEnd()); |
| 106 | } |
| 107 | |
| 108 | // Clang diags have a location (shown as ^) and 0 or more ranges (~~~~). |
| 109 | // LSP needs a single range. |
| 110 | std::optional<Range> diagnosticRange(const clang::Diagnostic &D, |
| 111 | const LangOptions &L) { |
| 112 | auto &M = D.getSourceManager(); |
| 113 | auto PatchedRange = [&M](CharSourceRange &R) { |
| 114 | R.setBegin(translatePreamblePatchLocation(Loc: R.getBegin(), SM: M)); |
| 115 | R.setEnd(translatePreamblePatchLocation(Loc: R.getEnd(), SM: M)); |
| 116 | return R; |
| 117 | }; |
| 118 | auto Loc = M.getFileLoc(Loc: D.getLocation()); |
| 119 | for (const auto &CR : D.getRanges()) { |
| 120 | auto R = Lexer::makeFileCharRange(CR, M, L); |
| 121 | if (locationInRange(Loc, R, M)) |
| 122 | return halfOpenToRange(M, PatchedRange(R)); |
| 123 | } |
| 124 | // The range may be given as a fixit hint instead. |
| 125 | for (const auto &F : D.getFixItHints()) { |
| 126 | auto R = Lexer::makeFileCharRange(Range: F.RemoveRange, SM: M, LangOpts: L); |
| 127 | if (locationInRange(L: Loc, R, M)) |
| 128 | return halfOpenToRange(SM: M, R: PatchedRange(R)); |
| 129 | } |
| 130 | // Source locations from stale preambles might become OOB. |
| 131 | // FIXME: These diagnostics might point to wrong locations even when they're |
| 132 | // not OOB. |
| 133 | auto [FID, Offset] = M.getDecomposedLoc(Loc); |
| 134 | if (Offset > M.getBufferData(FID).size()) |
| 135 | return std::nullopt; |
| 136 | // If the token at the location is not a comment, we use the token. |
| 137 | // If we can't get the token at the location, fall back to using the location |
| 138 | auto R = CharSourceRange::getCharRange(R: Loc); |
| 139 | Token Tok; |
| 140 | if (!Lexer::getRawToken(Loc, Result&: Tok, SM: M, LangOpts: L, IgnoreWhiteSpace: true) && Tok.isNot(K: tok::comment)) |
| 141 | R = CharSourceRange::getTokenRange(B: Tok.getLocation(), E: Tok.getEndLoc()); |
| 142 | return halfOpenToRange(SM: M, R: PatchedRange(R)); |
| 143 | } |
| 144 | |
| 145 | // Try to find a location in the main-file to report the diagnostic D. |
| 146 | // Returns a description like "in included file", or nullptr on failure. |
| 147 | const char *getMainFileRange(const Diag &D, const SourceManager &SM, |
| 148 | SourceLocation DiagLoc, Range &R) { |
| 149 | // Look for a note in the main file indicating template instantiation. |
| 150 | for (const auto &N : D.Notes) { |
| 151 | if (N.InsideMainFile) { |
| 152 | switch (N.ID) { |
| 153 | case diag::note_template_class_instantiation_was_here: |
| 154 | case diag::note_template_class_explicit_specialization_was_here: |
| 155 | case diag::note_template_class_instantiation_here: |
| 156 | case diag::note_template_member_class_here: |
| 157 | case diag::note_template_member_function_here: |
| 158 | case diag::note_function_template_spec_here: |
| 159 | case diag::note_template_static_data_member_def_here: |
| 160 | case diag::note_template_variable_def_here: |
| 161 | case diag::note_template_enum_def_here: |
| 162 | case diag::note_template_nsdmi_here: |
| 163 | case diag::note_template_type_alias_instantiation_here: |
| 164 | case diag::note_template_exception_spec_instantiation_here: |
| 165 | case diag::note_template_requirement_instantiation_here: |
| 166 | case diag::note_evaluating_exception_spec_here: |
| 167 | case diag::note_default_arg_instantiation_here: |
| 168 | case diag::note_default_function_arg_instantiation_here: |
| 169 | case diag::note_explicit_template_arg_substitution_here: |
| 170 | case diag::note_function_template_deduction_instantiation_here: |
| 171 | case diag::note_deduced_template_arg_substitution_here: |
| 172 | case diag::note_prior_template_arg_substitution: |
| 173 | case diag::note_template_default_arg_checking: |
| 174 | case diag::note_concept_specialization_here: |
| 175 | case diag::note_nested_requirement_here: |
| 176 | case diag::note_checking_constraints_for_template_id_here: |
| 177 | case diag::note_checking_constraints_for_var_spec_id_here: |
| 178 | case diag::note_checking_constraints_for_class_spec_id_here: |
| 179 | case diag::note_checking_constraints_for_function_here: |
| 180 | case diag::note_constraint_substitution_here: |
| 181 | case diag::note_constraint_normalization_here: |
| 182 | case diag::note_parameter_mapping_substitution_here: |
| 183 | R = N.Range; |
| 184 | return "in template" ; |
| 185 | default: |
| 186 | break; |
| 187 | } |
| 188 | } |
| 189 | } |
| 190 | // Look for where the file with the error was #included. |
| 191 | auto GetIncludeLoc = [&SM](SourceLocation SLoc) { |
| 192 | return SM.getIncludeLoc(FID: SM.getFileID(SpellingLoc: SLoc)); |
| 193 | }; |
| 194 | for (auto IncludeLocation = GetIncludeLoc(SM.getExpansionLoc(Loc: DiagLoc)); |
| 195 | IncludeLocation.isValid(); |
| 196 | IncludeLocation = GetIncludeLoc(IncludeLocation)) { |
| 197 | if (clangd::isInsideMainFile(Loc: IncludeLocation, SM)) { |
| 198 | R.start = sourceLocToPosition(SM, Loc: IncludeLocation); |
| 199 | R.end = sourceLocToPosition( |
| 200 | SM, |
| 201 | Loc: Lexer::getLocForEndOfToken(Loc: IncludeLocation, Offset: 0, SM, LangOpts: LangOptions())); |
| 202 | return "in included file" ; |
| 203 | } |
| 204 | } |
| 205 | return nullptr; |
| 206 | } |
| 207 | |
| 208 | // Place the diagnostic the main file, rather than the header, if possible: |
| 209 | // - for errors in included files, use the #include location |
| 210 | // - for errors in template instantiation, use the instantiation location |
| 211 | // In both cases, add the original header location as a note. |
| 212 | bool tryMoveToMainFile(Diag &D, FullSourceLoc DiagLoc) { |
| 213 | const SourceManager &SM = DiagLoc.getManager(); |
| 214 | DiagLoc = DiagLoc.getExpansionLoc(); |
| 215 | Range R; |
| 216 | const char *Prefix = getMainFileRange(D, SM, DiagLoc, R); |
| 217 | if (!Prefix) |
| 218 | return false; |
| 219 | |
| 220 | // Add a note that will point to real diagnostic. |
| 221 | auto FE = *SM.getFileEntryRefForID(FID: SM.getFileID(SpellingLoc: DiagLoc)); |
| 222 | D.Notes.emplace(D.Notes.begin()); |
| 223 | Note &N = D.Notes.front(); |
| 224 | N.AbsFile = std::string(FE.getFileEntry().tryGetRealPathName()); |
| 225 | N.File = std::string(FE.getName()); |
| 226 | N.Message = "error occurred here" ; |
| 227 | N.Range = D.Range; |
| 228 | |
| 229 | // Update diag to point at include inside main file. |
| 230 | D.File = SM.getFileEntryRefForID(FID: SM.getMainFileID())->getName().str(); |
| 231 | D.Range = std::move(R); |
| 232 | D.InsideMainFile = true; |
| 233 | // Update message to mention original file. |
| 234 | D.Message = llvm::formatv("{0}: {1}" , Prefix, D.Message); |
| 235 | return true; |
| 236 | } |
| 237 | |
| 238 | bool isNote(DiagnosticsEngine::Level L) { |
| 239 | return L == DiagnosticsEngine::Note || L == DiagnosticsEngine::Remark; |
| 240 | } |
| 241 | |
| 242 | llvm::StringRef diagLeveltoString(DiagnosticsEngine::Level Lvl) { |
| 243 | switch (Lvl) { |
| 244 | case DiagnosticsEngine::Ignored: |
| 245 | return "ignored" ; |
| 246 | case DiagnosticsEngine::Note: |
| 247 | return "note" ; |
| 248 | case DiagnosticsEngine::Remark: |
| 249 | return "remark" ; |
| 250 | case DiagnosticsEngine::Warning: |
| 251 | return "warning" ; |
| 252 | case DiagnosticsEngine::Error: |
| 253 | return "error" ; |
| 254 | case DiagnosticsEngine::Fatal: |
| 255 | return "fatal error" ; |
| 256 | } |
| 257 | llvm_unreachable("unhandled DiagnosticsEngine::Level" ); |
| 258 | } |
| 259 | |
| 260 | /// Prints a single diagnostic in a clang-like manner, the output includes |
| 261 | /// location, severity and error message. An example of the output message is: |
| 262 | /// |
| 263 | /// main.cpp:12:23: error: undeclared identifier |
| 264 | /// |
| 265 | /// For main file we only print the basename and for all other files we print |
| 266 | /// the filename on a separate line to provide a slightly more readable output |
| 267 | /// in the editors: |
| 268 | /// |
| 269 | /// dir1/dir2/dir3/../../dir4/header.h:12:23 |
| 270 | /// error: undeclared identifier |
| 271 | void printDiag(llvm::raw_string_ostream &OS, const DiagBase &D) { |
| 272 | if (D.InsideMainFile) { |
| 273 | // Paths to main files are often taken from compile_command.json, where they |
| 274 | // are typically absolute. To reduce noise we print only basename for them, |
| 275 | // it should not be confusing and saves space. |
| 276 | OS << llvm::sys::path::filename(path: D.File) << ":" ; |
| 277 | } else { |
| 278 | OS << D.File << ":" ; |
| 279 | } |
| 280 | // Note +1 to line and character. clangd::Range is zero-based, but when |
| 281 | // printing for users we want one-based indexes. |
| 282 | auto Pos = D.Range.start; |
| 283 | OS << (Pos.line + 1) << ":" << (Pos.character + 1) << ":" ; |
| 284 | // The non-main-file paths are often too long, putting them on a separate |
| 285 | // line improves readability. |
| 286 | if (D.InsideMainFile) |
| 287 | OS << " " ; |
| 288 | else |
| 289 | OS << "\n" ; |
| 290 | OS << diagLeveltoString(Lvl: D.Severity) << ": " << D.Message; |
| 291 | } |
| 292 | |
| 293 | /// Capitalizes the first word in the diagnostic's message. |
| 294 | std::string capitalize(std::string Message) { |
| 295 | if (!Message.empty()) |
| 296 | Message[0] = llvm::toUpper(x: Message[0]); |
| 297 | return Message; |
| 298 | } |
| 299 | |
| 300 | /// Returns a message sent to LSP for the main diagnostic in \p D. |
| 301 | /// This message may include notes, if they're not emitted in some other way. |
| 302 | /// Example output: |
| 303 | /// |
| 304 | /// no matching function for call to 'foo' |
| 305 | /// |
| 306 | /// main.cpp:3:5: note: candidate function not viable: requires 2 arguments |
| 307 | /// |
| 308 | /// dir1/dir2/dir3/../../dir4/header.h:12:23 |
| 309 | /// note: candidate function not viable: requires 3 arguments |
| 310 | std::string mainMessage(const Diag &D, const ClangdDiagnosticOptions &Opts) { |
| 311 | std::string Result; |
| 312 | llvm::raw_string_ostream OS(Result); |
| 313 | OS << D.Message; |
| 314 | if (Opts.DisplayFixesCount && !D.Fixes.empty()) |
| 315 | OS << " (" << (D.Fixes.size() > 1 ? "fixes" : "fix" ) << " available)" ; |
| 316 | // If notes aren't emitted as structured info, add them to the message. |
| 317 | if (!Opts.EmitRelatedLocations) |
| 318 | for (auto &Note : D.Notes) { |
| 319 | OS << "\n\n" ; |
| 320 | printDiag(OS, Note); |
| 321 | } |
| 322 | return capitalize(std::move(Result)); |
| 323 | } |
| 324 | |
| 325 | /// Returns a message sent to LSP for the note of the main diagnostic. |
| 326 | std::string noteMessage(const Diag &Main, const DiagBase &Note, |
| 327 | const ClangdDiagnosticOptions &Opts) { |
| 328 | std::string Result; |
| 329 | llvm::raw_string_ostream OS(Result); |
| 330 | OS << Note.Message; |
| 331 | // If the client doesn't support structured links between the note and the |
| 332 | // original diagnostic, then emit the main diagnostic to give context. |
| 333 | if (!Opts.EmitRelatedLocations) { |
| 334 | OS << "\n\n" ; |
| 335 | printDiag(OS, D: Main); |
| 336 | } |
| 337 | return capitalize(std::move(Result)); |
| 338 | } |
| 339 | |
| 340 | void setTags(clangd::Diag &D) { |
| 341 | static const auto *DeprecatedDiags = new llvm::DenseSet<unsigned>{ |
| 342 | diag::warn_access_decl_deprecated, |
| 343 | diag::warn_atl_uuid_deprecated, |
| 344 | diag::warn_deprecated, |
| 345 | diag::warn_deprecated_altivec_src_compat, |
| 346 | diag::warn_deprecated_comma_subscript, |
| 347 | diag::warn_deprecated_copy, |
| 348 | diag::warn_deprecated_copy_with_dtor, |
| 349 | diag::warn_deprecated_copy_with_user_provided_copy, |
| 350 | diag::warn_deprecated_copy_with_user_provided_dtor, |
| 351 | diag::warn_deprecated_def, |
| 352 | diag::warn_deprecated_increment_decrement_volatile, |
| 353 | diag::warn_deprecated_message, |
| 354 | diag::warn_deprecated_redundant_constexpr_static_def, |
| 355 | diag::warn_deprecated_register, |
| 356 | diag::warn_deprecated_simple_assign_volatile, |
| 357 | diag::warn_deprecated_string_literal_conversion, |
| 358 | diag::warn_deprecated_this_capture, |
| 359 | diag::warn_deprecated_volatile_param, |
| 360 | diag::warn_deprecated_volatile_return, |
| 361 | diag::warn_deprecated_volatile_structured_binding, |
| 362 | diag::warn_opencl_attr_deprecated_ignored, |
| 363 | diag::warn_property_method_deprecated, |
| 364 | diag::warn_vector_mode_deprecated, |
| 365 | }; |
| 366 | static const auto *UnusedDiags = new llvm::DenseSet<unsigned>{ |
| 367 | diag::warn_opencl_attr_deprecated_ignored, |
| 368 | diag::warn_pragma_attribute_unused, |
| 369 | diag::warn_unused_but_set_parameter, |
| 370 | diag::warn_unused_but_set_variable, |
| 371 | diag::warn_unused_comparison, |
| 372 | diag::warn_unused_const_variable, |
| 373 | diag::warn_unused_exception_param, |
| 374 | diag::warn_unused_function, |
| 375 | diag::warn_unused_label, |
| 376 | diag::warn_unused_lambda_capture, |
| 377 | diag::warn_unused_local_typedef, |
| 378 | diag::warn_unused_member_function, |
| 379 | diag::warn_unused_parameter, |
| 380 | diag::warn_unused_private_field, |
| 381 | diag::warn_unused_property_backing_ivar, |
| 382 | diag::warn_unused_template, |
| 383 | diag::warn_unused_variable, |
| 384 | }; |
| 385 | if (DeprecatedDiags->contains(D.ID)) { |
| 386 | D.Tags.push_back(Elt: DiagnosticTag::Deprecated); |
| 387 | } else if (UnusedDiags->contains(D.ID)) { |
| 388 | D.Tags.push_back(Elt: DiagnosticTag::Unnecessary); |
| 389 | } |
| 390 | if (D.Source == Diag::ClangTidy) { |
| 391 | if (llvm::StringRef(D.Name).starts_with(Prefix: "misc-unused-" )) |
| 392 | D.Tags.push_back(Elt: DiagnosticTag::Unnecessary); |
| 393 | if (llvm::StringRef(D.Name).starts_with(Prefix: "modernize-" )) |
| 394 | D.Tags.push_back(Elt: DiagnosticTag::Deprecated); |
| 395 | } |
| 396 | } |
| 397 | } // namespace |
| 398 | |
| 399 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const DiagBase &D) { |
| 400 | OS << "[" ; |
| 401 | if (!D.InsideMainFile) |
| 402 | OS << D.File << ":" ; |
| 403 | OS << D.Range.start << "-" << D.Range.end << "] " ; |
| 404 | |
| 405 | return OS << D.Message; |
| 406 | } |
| 407 | |
| 408 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Fix &F) { |
| 409 | OS << F.Message << " {" ; |
| 410 | const char *Sep = "" ; |
| 411 | for (const auto &Edit : F.Edits) { |
| 412 | OS << Sep << Edit; |
| 413 | Sep = ", " ; |
| 414 | } |
| 415 | return OS << "}" ; |
| 416 | } |
| 417 | |
| 418 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Diag &D) { |
| 419 | OS << static_cast<const DiagBase &>(D); |
| 420 | if (!D.Notes.empty()) { |
| 421 | OS << ", notes: {" ; |
| 422 | const char *Sep = "" ; |
| 423 | for (auto &Note : D.Notes) { |
| 424 | OS << Sep << Note; |
| 425 | Sep = ", " ; |
| 426 | } |
| 427 | OS << "}" ; |
| 428 | } |
| 429 | if (!D.Fixes.empty()) { |
| 430 | OS << ", fixes: {" ; |
| 431 | const char *Sep = "" ; |
| 432 | for (auto &Fix : D.Fixes) { |
| 433 | OS << Sep << Fix; |
| 434 | Sep = ", " ; |
| 435 | } |
| 436 | OS << "}" ; |
| 437 | } |
| 438 | return OS; |
| 439 | } |
| 440 | |
| 441 | Diag toDiag(const llvm::SMDiagnostic &D, Diag::DiagSource Source) { |
| 442 | Diag Result; |
| 443 | Result.Message = D.getMessage().str(); |
| 444 | switch (D.getKind()) { |
| 445 | case llvm::SourceMgr::DK_Error: |
| 446 | Result.Severity = DiagnosticsEngine::Error; |
| 447 | break; |
| 448 | case llvm::SourceMgr::DK_Warning: |
| 449 | Result.Severity = DiagnosticsEngine::Warning; |
| 450 | break; |
| 451 | default: |
| 452 | break; |
| 453 | } |
| 454 | Result.Source = Source; |
| 455 | Result.AbsFile = D.getFilename().str(); |
| 456 | Result.InsideMainFile = D.getSourceMgr()->FindBufferContainingLoc( |
| 457 | Loc: D.getLoc()) == D.getSourceMgr()->getMainFileID(); |
| 458 | if (D.getRanges().empty()) |
| 459 | Result.Range = {.start: {.line: D.getLineNo() - 1, .character: D.getColumnNo()}, |
| 460 | .end: {.line: D.getLineNo() - 1, .character: D.getColumnNo()}}; |
| 461 | else |
| 462 | Result.Range = {.start: {.line: D.getLineNo() - 1, .character: (int)D.getRanges().front().first}, |
| 463 | .end: {.line: D.getLineNo() - 1, .character: (int)D.getRanges().front().second}}; |
| 464 | return Result; |
| 465 | } |
| 466 | |
| 467 | void toLSPDiags( |
| 468 | const Diag &D, const URIForFile &File, const ClangdDiagnosticOptions &Opts, |
| 469 | llvm::function_ref<void(clangd::Diagnostic, llvm::ArrayRef<Fix>)> OutFn) { |
| 470 | clangd::Diagnostic Main; |
| 471 | Main.severity = getSeverity(L: D.Severity); |
| 472 | // We downgrade severity for certain noisy warnings, like deprecated |
| 473 | // declartions. These already have visible decorations inside the editor and |
| 474 | // most users find the extra clutter in the UI (gutter, minimap, diagnostics |
| 475 | // views) overwhelming. |
| 476 | if (D.Severity == DiagnosticsEngine::Warning) { |
| 477 | if (llvm::is_contained(D.Tags, DiagnosticTag::Deprecated)) |
| 478 | Main.severity = getSeverity(L: DiagnosticsEngine::Remark); |
| 479 | } |
| 480 | |
| 481 | // Main diagnostic should always refer to a range inside main file. If a |
| 482 | // diagnostic made it so for, it means either itself or one of its notes is |
| 483 | // inside main file. It's also possible that there's a fix in the main file, |
| 484 | // but we preserve fixes iff primary diagnostic is in the main file. |
| 485 | if (D.InsideMainFile) { |
| 486 | Main.range = D.Range; |
| 487 | } else { |
| 488 | auto It = |
| 489 | llvm::find_if(D.Notes, [](const Note &N) { return N.InsideMainFile; }); |
| 490 | assert(It != D.Notes.end() && |
| 491 | "neither the main diagnostic nor notes are inside main file" ); |
| 492 | Main.range = It->Range; |
| 493 | } |
| 494 | |
| 495 | Main.code = D.Name; |
| 496 | if (auto URI = getDiagnosticDocURI(D.Source, ID: D.ID, Name: D.Name)) { |
| 497 | Main.codeDescription.emplace(); |
| 498 | Main.codeDescription->href = std::move(*URI); |
| 499 | } |
| 500 | switch (D.Source) { |
| 501 | case Diag::Clang: |
| 502 | Main.source = "clang" ; |
| 503 | break; |
| 504 | case Diag::ClangTidy: |
| 505 | Main.source = "clang-tidy" ; |
| 506 | break; |
| 507 | case Diag::Clangd: |
| 508 | Main.source = "clangd" ; |
| 509 | break; |
| 510 | case Diag::ClangdConfig: |
| 511 | Main.source = "clangd-config" ; |
| 512 | break; |
| 513 | case Diag::Unknown: |
| 514 | break; |
| 515 | } |
| 516 | if (Opts.SendDiagnosticCategory && !D.Category.empty()) |
| 517 | Main.category = D.Category; |
| 518 | |
| 519 | Main.message = mainMessage(D, Opts); |
| 520 | if (Opts.EmitRelatedLocations) { |
| 521 | Main.relatedInformation.emplace(); |
| 522 | for (auto &Note : D.Notes) { |
| 523 | if (!Note.AbsFile) { |
| 524 | vlog("Dropping note from unknown file: {0}" , Note); |
| 525 | continue; |
| 526 | } |
| 527 | DiagnosticRelatedInformation RelInfo; |
| 528 | RelInfo.location.range = Note.Range; |
| 529 | RelInfo.location.uri = |
| 530 | URIForFile::canonicalize(*Note.AbsFile, File.file()); |
| 531 | RelInfo.message = noteMessage(D, Note, Opts); |
| 532 | Main.relatedInformation->push_back(std::move(RelInfo)); |
| 533 | } |
| 534 | } |
| 535 | Main.tags = D.Tags; |
| 536 | // FIXME: Get rid of the copies here by taking in a mutable clangd::Diag. |
| 537 | for (auto &Entry : D.OpaqueData) |
| 538 | Main.data.insert({Entry.first, Entry.second}); |
| 539 | OutFn(std::move(Main), D.Fixes); |
| 540 | |
| 541 | // If we didn't emit the notes as relatedLocations, emit separate diagnostics |
| 542 | // so the user can find the locations easily. |
| 543 | if (!Opts.EmitRelatedLocations) |
| 544 | for (auto &Note : D.Notes) { |
| 545 | if (!Note.InsideMainFile) |
| 546 | continue; |
| 547 | clangd::Diagnostic Res; |
| 548 | Res.severity = getSeverity(Note.Severity); |
| 549 | Res.range = Note.Range; |
| 550 | Res.message = noteMessage(D, Note, Opts); |
| 551 | OutFn(std::move(Res), llvm::ArrayRef<Fix>()); |
| 552 | } |
| 553 | } |
| 554 | |
| 555 | int getSeverity(DiagnosticsEngine::Level L) { |
| 556 | switch (L) { |
| 557 | case DiagnosticsEngine::Remark: |
| 558 | return 4; |
| 559 | case DiagnosticsEngine::Note: |
| 560 | return 3; |
| 561 | case DiagnosticsEngine::Warning: |
| 562 | return 2; |
| 563 | case DiagnosticsEngine::Fatal: |
| 564 | case DiagnosticsEngine::Error: |
| 565 | return 1; |
| 566 | case DiagnosticsEngine::Ignored: |
| 567 | return 0; |
| 568 | } |
| 569 | llvm_unreachable("Unknown diagnostic level!" ); |
| 570 | } |
| 571 | |
| 572 | std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) { |
| 573 | // Do not forget to emit a pending diagnostic if there is one. |
| 574 | flushLastDiag(); |
| 575 | |
| 576 | // Fill in name/source now that we have all the context needed to map them. |
| 577 | for (auto &Diag : Output) { |
| 578 | if (const char *ClangDiag = getDiagnosticCode(Diag.ID)) { |
| 579 | // Warnings controlled by -Wfoo are better recognized by that name. |
| 580 | StringRef Warning = [&] { |
| 581 | if (OrigSrcMgr) { |
| 582 | return OrigSrcMgr->getDiagnostics() |
| 583 | .getDiagnosticIDs() |
| 584 | ->getWarningOptionForDiag(Diag.ID); |
| 585 | } |
| 586 | if (!DiagnosticIDs::IsCustomDiag(Diag.ID)) |
| 587 | return DiagnosticIDs{}.getWarningOptionForDiag(Diag.ID); |
| 588 | return StringRef{}; |
| 589 | }(); |
| 590 | |
| 591 | if (!Warning.empty()) { |
| 592 | Diag.Name = ("-W" + Warning).str(); |
| 593 | } else { |
| 594 | StringRef Name(ClangDiag); |
| 595 | // Almost always an error, with a name like err_enum_class_reference. |
| 596 | // Drop the err_ prefix for brevity. |
| 597 | Name.consume_front("err_" ); |
| 598 | Diag.Name = std::string(Name); |
| 599 | } |
| 600 | Diag.Source = Diag::Clang; |
| 601 | } else if (Tidy != nullptr) { |
| 602 | std::string TidyDiag = Tidy->getCheckName(Diag.ID); |
| 603 | if (!TidyDiag.empty()) { |
| 604 | Diag.Name = std::move(TidyDiag); |
| 605 | Diag.Source = Diag::ClangTidy; |
| 606 | // clang-tidy bakes the name into diagnostic messages. Strip it out. |
| 607 | // It would be much nicer to make clang-tidy not do this. |
| 608 | auto CleanMessage = [&](std::string &Msg) { |
| 609 | StringRef Rest(Msg); |
| 610 | if (Rest.consume_back("]" ) && Rest.consume_back(Diag.Name) && |
| 611 | Rest.consume_back(" [" )) |
| 612 | Msg.resize(Rest.size()); |
| 613 | }; |
| 614 | CleanMessage(Diag.Message); |
| 615 | for (auto &Note : Diag.Notes) |
| 616 | CleanMessage(Note.Message); |
| 617 | for (auto &Fix : Diag.Fixes) |
| 618 | CleanMessage(Fix.Message); |
| 619 | } |
| 620 | } |
| 621 | setTags(Diag); |
| 622 | } |
| 623 | // Deduplicate clang-tidy diagnostics -- some clang-tidy checks may emit |
| 624 | // duplicated messages due to various reasons (e.g. the check doesn't handle |
| 625 | // template instantiations well; clang-tidy alias checks). |
| 626 | std::set<std::pair<Range, std::string>> SeenDiags; |
| 627 | llvm::erase_if(Output, [&](const Diag &D) { |
| 628 | return !SeenDiags.emplace(D.Range, D.Message).second; |
| 629 | }); |
| 630 | return std::move(Output); |
| 631 | } |
| 632 | |
| 633 | void StoreDiags::BeginSourceFile(const LangOptions &Opts, |
| 634 | const Preprocessor *PP) { |
| 635 | LangOpts = Opts; |
| 636 | if (PP) { |
| 637 | OrigSrcMgr = &PP->getSourceManager(); |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | void StoreDiags::EndSourceFile() { |
| 642 | flushLastDiag(); |
| 643 | LangOpts = std::nullopt; |
| 644 | OrigSrcMgr = nullptr; |
| 645 | } |
| 646 | |
| 647 | /// Sanitizes a piece for presenting it in a synthesized fix message. Ensures |
| 648 | /// the result is not too large and does not contain newlines. |
| 649 | static void writeCodeToFixMessage(llvm::raw_ostream &OS, llvm::StringRef Code) { |
| 650 | constexpr unsigned MaxLen = 50; |
| 651 | if (Code == "\n" ) { |
| 652 | OS << "\\n" ; |
| 653 | return; |
| 654 | } |
| 655 | // Only show the first line if there are many. |
| 656 | llvm::StringRef R = Code.split('\n').first; |
| 657 | // Shorten the message if it's too long. |
| 658 | R = R.take_front(N: MaxLen); |
| 659 | |
| 660 | OS << R; |
| 661 | if (R.size() != Code.size()) |
| 662 | OS << "…" ; |
| 663 | } |
| 664 | |
| 665 | /// Fills \p D with all information, except the location-related bits. |
| 666 | /// Also note that ID and Name are not part of clangd::DiagBase and should be |
| 667 | /// set elsewhere. |
| 668 | static void fillNonLocationData(DiagnosticsEngine::Level DiagLevel, |
| 669 | const clang::Diagnostic &Info, |
| 670 | clangd::DiagBase &D) { |
| 671 | llvm::SmallString<64> Message; |
| 672 | Info.FormatDiagnostic(Message); |
| 673 | |
| 674 | D.Message = std::string(Message); |
| 675 | D.Severity = DiagLevel; |
| 676 | D.Category = DiagnosticIDs::getCategoryNameFromID( |
| 677 | CategoryID: DiagnosticIDs::getCategoryNumberForDiag(DiagID: Info.getID())) |
| 678 | .str(); |
| 679 | } |
| 680 | |
| 681 | void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, |
| 682 | const clang::Diagnostic &Info) { |
| 683 | // If the diagnostic was generated for a different SourceManager, skip it. |
| 684 | // This happens when a module is imported and needs to be implicitly built. |
| 685 | // The compilation of that module will use the same StoreDiags, but different |
| 686 | // SourceManager. |
| 687 | if (OrigSrcMgr && Info.hasSourceManager() && |
| 688 | OrigSrcMgr != &Info.getSourceManager()) { |
| 689 | IgnoreDiagnostics::log(DiagLevel, Info); |
| 690 | return; |
| 691 | } |
| 692 | |
| 693 | DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info); |
| 694 | bool OriginallyError = |
| 695 | Info.getDiags()->getDiagnosticIDs()->isDefaultMappingAsError( |
| 696 | DiagID: Info.getID()); |
| 697 | |
| 698 | if (Info.getLocation().isInvalid()) { |
| 699 | // Handle diagnostics coming from command-line arguments. The source manager |
| 700 | // is *not* available at this point, so we cannot use it. |
| 701 | if (!OriginallyError) { |
| 702 | IgnoreDiagnostics::log(DiagLevel, Info); |
| 703 | return; // non-errors add too much noise, do not show them. |
| 704 | } |
| 705 | |
| 706 | flushLastDiag(); |
| 707 | |
| 708 | LastDiag = Diag(); |
| 709 | LastDiagLoc.reset(); |
| 710 | LastDiagOriginallyError = OriginallyError; |
| 711 | LastDiag->ID = Info.getID(); |
| 712 | fillNonLocationData(DiagLevel, Info, D&: *LastDiag); |
| 713 | LastDiag->InsideMainFile = true; |
| 714 | // Put it at the start of the main file, for a lack of a better place. |
| 715 | LastDiag->Range.start = Position{.line: 0, .character: 0}; |
| 716 | LastDiag->Range.end = Position{.line: 0, .character: 0}; |
| 717 | return; |
| 718 | } |
| 719 | |
| 720 | if (!LangOpts || !Info.hasSourceManager()) { |
| 721 | IgnoreDiagnostics::log(DiagLevel, Info); |
| 722 | return; |
| 723 | } |
| 724 | |
| 725 | SourceManager &SM = Info.getSourceManager(); |
| 726 | |
| 727 | auto FillDiagBase = [&](DiagBase &D) { |
| 728 | fillNonLocationData(DiagLevel, Info, D); |
| 729 | |
| 730 | SourceLocation PatchLoc = |
| 731 | translatePreamblePatchLocation(Loc: Info.getLocation(), SM); |
| 732 | D.InsideMainFile = isInsideMainFile(Loc: PatchLoc, SM); |
| 733 | if (auto DRange = diagnosticRange(D: Info, L: *LangOpts)) |
| 734 | D.Range = *DRange; |
| 735 | else |
| 736 | D.Severity = DiagnosticsEngine::Ignored; |
| 737 | auto FID = SM.getFileID(SpellingLoc: Info.getLocation()); |
| 738 | if (const auto FE = SM.getFileEntryRefForID(FID)) { |
| 739 | D.File = FE->getName().str(); |
| 740 | D.AbsFile = getCanonicalPath(F: *FE, FileMgr&: SM.getFileManager()); |
| 741 | } |
| 742 | D.ID = Info.getID(); |
| 743 | return D; |
| 744 | }; |
| 745 | |
| 746 | auto AddFix = [&](bool SyntheticMessage) -> bool { |
| 747 | assert(!Info.getFixItHints().empty() && |
| 748 | "diagnostic does not have attached fix-its" ); |
| 749 | // No point in generating fixes, if the diagnostic is for a different file. |
| 750 | if (!LastDiag->InsideMainFile) |
| 751 | return false; |
| 752 | // Copy as we may modify the ranges. |
| 753 | auto FixIts = Info.getFixItHints().vec(); |
| 754 | llvm::SmallVector<TextEdit, 1> Edits; |
| 755 | for (auto &FixIt : FixIts) { |
| 756 | // Allow fixits within a single macro-arg expansion to be applied. |
| 757 | // This can be incorrect if the argument is expanded multiple times in |
| 758 | // different contexts. Hopefully this is rare! |
| 759 | if (FixIt.RemoveRange.getBegin().isMacroID() && |
| 760 | FixIt.RemoveRange.getEnd().isMacroID() && |
| 761 | SM.getFileID(FixIt.RemoveRange.getBegin()) == |
| 762 | SM.getFileID(FixIt.RemoveRange.getEnd())) { |
| 763 | FixIt.RemoveRange = CharSourceRange( |
| 764 | {SM.getTopMacroCallerLoc(FixIt.RemoveRange.getBegin()), |
| 765 | SM.getTopMacroCallerLoc(FixIt.RemoveRange.getEnd())}, |
| 766 | FixIt.RemoveRange.isTokenRange()); |
| 767 | } |
| 768 | // Otherwise, follow clang's behavior: no fixits in macros. |
| 769 | if (FixIt.RemoveRange.getBegin().isMacroID() || |
| 770 | FixIt.RemoveRange.getEnd().isMacroID()) |
| 771 | return false; |
| 772 | if (!isInsideMainFile(FixIt.RemoveRange.getBegin(), SM)) |
| 773 | return false; |
| 774 | Edits.push_back(toTextEdit(FixIt, SM, *LangOpts)); |
| 775 | } |
| 776 | |
| 777 | llvm::SmallString<64> Message; |
| 778 | // If requested and possible, create a message like "change 'foo' to 'bar'". |
| 779 | if (SyntheticMessage && FixIts.size() == 1) { |
| 780 | const auto &FixIt = FixIts.front(); |
| 781 | bool Invalid = false; |
| 782 | llvm::StringRef Remove = |
| 783 | Lexer::getSourceText(Range: FixIt.RemoveRange, SM, LangOpts: *LangOpts, Invalid: &Invalid); |
| 784 | llvm::StringRef Insert = FixIt.CodeToInsert; |
| 785 | if (!Invalid) { |
| 786 | llvm::raw_svector_ostream M(Message); |
| 787 | if (!Remove.empty() && !Insert.empty()) { |
| 788 | M << "change '" ; |
| 789 | writeCodeToFixMessage(OS&: M, Code: Remove); |
| 790 | M << "' to '" ; |
| 791 | writeCodeToFixMessage(OS&: M, Code: Insert); |
| 792 | M << "'" ; |
| 793 | } else if (!Remove.empty()) { |
| 794 | M << "remove '" ; |
| 795 | writeCodeToFixMessage(OS&: M, Code: Remove); |
| 796 | M << "'" ; |
| 797 | } else if (!Insert.empty()) { |
| 798 | M << "insert '" ; |
| 799 | writeCodeToFixMessage(OS&: M, Code: Insert); |
| 800 | M << "'" ; |
| 801 | } |
| 802 | // Don't allow source code to inject newlines into diagnostics. |
| 803 | llvm::replace(Message, '\n', ' '); |
| 804 | } |
| 805 | } |
| 806 | if (Message.empty()) // either !SyntheticMessage, or we failed to make one. |
| 807 | Info.FormatDiagnostic(Message); |
| 808 | LastDiag->Fixes.push_back( |
| 809 | x: Fix{std::string(Message), std::move(Edits), {}}); |
| 810 | return true; |
| 811 | }; |
| 812 | |
| 813 | if (!isNote(L: DiagLevel)) { |
| 814 | // Handle the new main diagnostic. |
| 815 | flushLastDiag(); |
| 816 | |
| 817 | LastDiag = Diag(); |
| 818 | // FIXME: Merge with feature modules. |
| 819 | if (Adjuster) |
| 820 | DiagLevel = Adjuster(DiagLevel, Info); |
| 821 | |
| 822 | FillDiagBase(*LastDiag); |
| 823 | if (isExcluded(DiagID: LastDiag->ID)) |
| 824 | LastDiag->Severity = DiagnosticsEngine::Ignored; |
| 825 | if (DiagCB) |
| 826 | DiagCB(Info, *LastDiag); |
| 827 | // Don't bother filling in the rest if diag is going to be dropped. |
| 828 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
| 829 | return; |
| 830 | |
| 831 | LastDiagLoc.emplace(Info.getLocation(), Info.getSourceManager()); |
| 832 | LastDiagOriginallyError = OriginallyError; |
| 833 | if (!Info.getFixItHints().empty()) |
| 834 | AddFix(true /* try to invent a message instead of repeating the diag */); |
| 835 | if (Fixer) { |
| 836 | auto = Fixer(LastDiag->Severity, Info); |
| 837 | LastDiag->Fixes.insert(LastDiag->Fixes.end(), ExtraFixes.begin(), |
| 838 | ExtraFixes.end()); |
| 839 | } |
| 840 | } else { |
| 841 | // Handle a note to an existing diagnostic. |
| 842 | if (!LastDiag) { |
| 843 | assert(false && "Adding a note without main diagnostic" ); |
| 844 | IgnoreDiagnostics::log(DiagLevel, Info); |
| 845 | return; |
| 846 | } |
| 847 | |
| 848 | // If a diagnostic was suppressed due to the suppression filter, |
| 849 | // also suppress notes associated with it. |
| 850 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
| 851 | return; |
| 852 | |
| 853 | // Give include-fixer a chance to replace a note with a fix. |
| 854 | if (Fixer) { |
| 855 | auto ReplacementFixes = Fixer(LastDiag->Severity, Info); |
| 856 | if (!ReplacementFixes.empty()) { |
| 857 | assert(Info.getNumFixItHints() == 0 && |
| 858 | "Include-fixer replaced a note with clang fix-its attached!" ); |
| 859 | LastDiag->Fixes.insert(LastDiag->Fixes.end(), ReplacementFixes.begin(), |
| 860 | ReplacementFixes.end()); |
| 861 | return; |
| 862 | } |
| 863 | } |
| 864 | |
| 865 | if (!Info.getFixItHints().empty()) { |
| 866 | // A clang note with fix-it is not a separate diagnostic in clangd. We |
| 867 | // attach it as a Fix to the main diagnostic instead. |
| 868 | if (!AddFix(false /* use the note as the message */)) |
| 869 | IgnoreDiagnostics::log(DiagLevel, Info); |
| 870 | } else { |
| 871 | // A clang note without fix-its corresponds to clangd::Note. |
| 872 | Note N; |
| 873 | FillDiagBase(N); |
| 874 | |
| 875 | LastDiag->Notes.push_back(std::move(N)); |
| 876 | } |
| 877 | } |
| 878 | } |
| 879 | |
| 880 | void StoreDiags::flushLastDiag() { |
| 881 | if (!LastDiag) |
| 882 | return; |
| 883 | auto Finish = llvm::make_scope_exit([&, NDiags(Output.size())] { |
| 884 | if (Output.size() == NDiags) // No new diag emitted. |
| 885 | vlog("Dropped diagnostic: {0}: {1}" , LastDiag->File, LastDiag->Message); |
| 886 | LastDiag.reset(); |
| 887 | }); |
| 888 | |
| 889 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
| 890 | return; |
| 891 | // Move errors that occur from headers into main file. |
| 892 | if (!LastDiag->InsideMainFile && LastDiagLoc && LastDiagOriginallyError) { |
| 893 | if (tryMoveToMainFile(D&: *LastDiag, DiagLoc: *LastDiagLoc)) { |
| 894 | // Suppress multiple errors from the same inclusion. |
| 895 | if (!IncludedErrorLocations |
| 896 | .insert({LastDiag->Range.start.line, |
| 897 | LastDiag->Range.start.character}) |
| 898 | .second) |
| 899 | return; |
| 900 | } |
| 901 | } |
| 902 | if (!mentionsMainFile(D: *LastDiag)) |
| 903 | return; |
| 904 | Output.push_back(std::move(*LastDiag)); |
| 905 | } |
| 906 | |
| 907 | bool isDiagnosticSuppressed(const clang::Diagnostic &Diag, |
| 908 | const llvm::StringSet<> &Suppress, |
| 909 | const LangOptions &LangOpts) { |
| 910 | // Don't complain about header-only stuff in mainfiles if it's a header. |
| 911 | // FIXME: would be cleaner to suppress in clang, once we decide whether the |
| 912 | // behavior should be to silently-ignore or respect the pragma. |
| 913 | if (Diag.getID() == diag::pp_pragma_sysheader_in_main_file && |
| 914 | LangOpts.IsHeaderFile) |
| 915 | return true; |
| 916 | |
| 917 | if (const char *CodePtr = getDiagnosticCode(ID: Diag.getID())) { |
| 918 | if (Suppress.contains(normalizeSuppressedCode(CodePtr))) |
| 919 | return true; |
| 920 | } |
| 921 | StringRef Warning = |
| 922 | Diag.getDiags()->getDiagnosticIDs()->getWarningOptionForDiag( |
| 923 | DiagID: Diag.getID()); |
| 924 | if (!Warning.empty() && Suppress.contains(Warning)) |
| 925 | return true; |
| 926 | return false; |
| 927 | } |
| 928 | |
| 929 | llvm::StringRef normalizeSuppressedCode(llvm::StringRef Code) { |
| 930 | Code.consume_front(Prefix: "err_" ); |
| 931 | Code.consume_front(Prefix: "-W" ); |
| 932 | return Code; |
| 933 | } |
| 934 | |
| 935 | std::optional<std::string> getDiagnosticDocURI(Diag::DiagSource Source, |
| 936 | unsigned ID, |
| 937 | llvm::StringRef Name) { |
| 938 | switch (Source) { |
| 939 | case Diag::Unknown: |
| 940 | break; |
| 941 | case Diag::Clang: |
| 942 | // There is a page listing many warning flags, but it provides too little |
| 943 | // information to be worth linking. |
| 944 | // https://clang.llvm.org/docs/DiagnosticsReference.html |
| 945 | break; |
| 946 | case Diag::ClangTidy: { |
| 947 | StringRef Module, Check; |
| 948 | // This won't correctly get the module for clang-analyzer checks, but as we |
| 949 | // don't link in the analyzer that shouldn't be an issue. |
| 950 | // This would also need updating if anyone decides to create a module with a |
| 951 | // '-' in the name. |
| 952 | std::tie(Module, Check) = Name.split('-'); |
| 953 | if (Module.empty() || Check.empty()) |
| 954 | return std::nullopt; |
| 955 | return ("https://clang.llvm.org/extra/clang-tidy/checks/" + Module + "/" + |
| 956 | Check + ".html" ) |
| 957 | .str(); |
| 958 | } |
| 959 | case Diag::Clangd: |
| 960 | if (Name == "unused-includes" || Name == "missing-includes" ) |
| 961 | return {"https://clangd.llvm.org/guides/include-cleaner" }; |
| 962 | break; |
| 963 | case Diag::ClangdConfig: |
| 964 | // FIXME: we should link to https://clangd.llvm.org/config |
| 965 | // However we have no diagnostic codes, which the link should describe! |
| 966 | break; |
| 967 | } |
| 968 | return std::nullopt; |
| 969 | } |
| 970 | |
| 971 | } // namespace clangd |
| 972 | } // namespace clang |
| 973 | |