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 ExtraFixes = 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 |
Definitions
- getDiagnosticCode
- mentionsMainFile
- isExcluded
- locationInRange
- diagnosticRange
- getMainFileRange
- tryMoveToMainFile
- isNote
- diagLeveltoString
- printDiag
- capitalize
- mainMessage
- noteMessage
- setTags
- operator<<
- operator<<
- operator<<
- toDiag
- toLSPDiags
- getSeverity
- take
- BeginSourceFile
- EndSourceFile
- writeCodeToFixMessage
- fillNonLocationData
- HandleDiagnostic
- flushLastDiag
- isDiagnosticSuppressed
- normalizeSuppressedCode
Learn to use CMake with our Intro Training
Find out more