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 | OS.flush(); |
323 | return capitalize(std::move(Result)); |
324 | } |
325 | |
326 | /// Returns a message sent to LSP for the note of the main diagnostic. |
327 | std::string noteMessage(const Diag &Main, const DiagBase &Note, |
328 | const ClangdDiagnosticOptions &Opts) { |
329 | std::string Result; |
330 | llvm::raw_string_ostream OS(Result); |
331 | OS << Note.Message; |
332 | // If the client doesn't support structured links between the note and the |
333 | // original diagnostic, then emit the main diagnostic to give context. |
334 | if (!Opts.EmitRelatedLocations) { |
335 | OS << "\n\n" ; |
336 | printDiag(OS, D: Main); |
337 | } |
338 | OS.flush(); |
339 | return capitalize(std::move(Result)); |
340 | } |
341 | |
342 | void setTags(clangd::Diag &D) { |
343 | static const auto *DeprecatedDiags = new llvm::DenseSet<unsigned>{ |
344 | diag::warn_access_decl_deprecated, |
345 | diag::warn_atl_uuid_deprecated, |
346 | diag::warn_deprecated, |
347 | diag::warn_deprecated_altivec_src_compat, |
348 | diag::warn_deprecated_comma_subscript, |
349 | diag::warn_deprecated_copy, |
350 | diag::warn_deprecated_copy_with_dtor, |
351 | diag::warn_deprecated_copy_with_user_provided_copy, |
352 | diag::warn_deprecated_copy_with_user_provided_dtor, |
353 | diag::warn_deprecated_def, |
354 | diag::warn_deprecated_increment_decrement_volatile, |
355 | diag::warn_deprecated_message, |
356 | diag::warn_deprecated_redundant_constexpr_static_def, |
357 | diag::warn_deprecated_register, |
358 | diag::warn_deprecated_simple_assign_volatile, |
359 | diag::warn_deprecated_string_literal_conversion, |
360 | diag::warn_deprecated_this_capture, |
361 | diag::warn_deprecated_volatile_param, |
362 | diag::warn_deprecated_volatile_return, |
363 | diag::warn_deprecated_volatile_structured_binding, |
364 | diag::warn_opencl_attr_deprecated_ignored, |
365 | diag::warn_property_method_deprecated, |
366 | diag::warn_vector_mode_deprecated, |
367 | }; |
368 | static const auto *UnusedDiags = new llvm::DenseSet<unsigned>{ |
369 | diag::warn_opencl_attr_deprecated_ignored, |
370 | diag::warn_pragma_attribute_unused, |
371 | diag::warn_unused_but_set_parameter, |
372 | diag::warn_unused_but_set_variable, |
373 | diag::warn_unused_comparison, |
374 | diag::warn_unused_const_variable, |
375 | diag::warn_unused_exception_param, |
376 | diag::warn_unused_function, |
377 | diag::warn_unused_label, |
378 | diag::warn_unused_lambda_capture, |
379 | diag::warn_unused_local_typedef, |
380 | diag::warn_unused_member_function, |
381 | diag::warn_unused_parameter, |
382 | diag::warn_unused_private_field, |
383 | diag::warn_unused_property_backing_ivar, |
384 | diag::warn_unused_template, |
385 | diag::warn_unused_variable, |
386 | }; |
387 | if (DeprecatedDiags->contains(D.ID)) { |
388 | D.Tags.push_back(Elt: DiagnosticTag::Deprecated); |
389 | } else if (UnusedDiags->contains(D.ID)) { |
390 | D.Tags.push_back(Elt: DiagnosticTag::Unnecessary); |
391 | } |
392 | if (D.Source == Diag::ClangTidy) { |
393 | if (llvm::StringRef(D.Name).starts_with(Prefix: "misc-unused-" )) |
394 | D.Tags.push_back(Elt: DiagnosticTag::Unnecessary); |
395 | if (llvm::StringRef(D.Name).starts_with(Prefix: "modernize-" )) |
396 | D.Tags.push_back(Elt: DiagnosticTag::Deprecated); |
397 | } |
398 | } |
399 | } // namespace |
400 | |
401 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const DiagBase &D) { |
402 | OS << "[" ; |
403 | if (!D.InsideMainFile) |
404 | OS << D.File << ":" ; |
405 | OS << D.Range.start << "-" << D.Range.end << "] " ; |
406 | |
407 | return OS << D.Message; |
408 | } |
409 | |
410 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Fix &F) { |
411 | OS << F.Message << " {" ; |
412 | const char *Sep = "" ; |
413 | for (const auto &Edit : F.Edits) { |
414 | OS << Sep << Edit; |
415 | Sep = ", " ; |
416 | } |
417 | return OS << "}" ; |
418 | } |
419 | |
420 | llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const Diag &D) { |
421 | OS << static_cast<const DiagBase &>(D); |
422 | if (!D.Notes.empty()) { |
423 | OS << ", notes: {" ; |
424 | const char *Sep = "" ; |
425 | for (auto &Note : D.Notes) { |
426 | OS << Sep << Note; |
427 | Sep = ", " ; |
428 | } |
429 | OS << "}" ; |
430 | } |
431 | if (!D.Fixes.empty()) { |
432 | OS << ", fixes: {" ; |
433 | const char *Sep = "" ; |
434 | for (auto &Fix : D.Fixes) { |
435 | OS << Sep << Fix; |
436 | Sep = ", " ; |
437 | } |
438 | OS << "}" ; |
439 | } |
440 | return OS; |
441 | } |
442 | |
443 | Diag toDiag(const llvm::SMDiagnostic &D, Diag::DiagSource Source) { |
444 | Diag Result; |
445 | Result.Message = D.getMessage().str(); |
446 | switch (D.getKind()) { |
447 | case llvm::SourceMgr::DK_Error: |
448 | Result.Severity = DiagnosticsEngine::Error; |
449 | break; |
450 | case llvm::SourceMgr::DK_Warning: |
451 | Result.Severity = DiagnosticsEngine::Warning; |
452 | break; |
453 | default: |
454 | break; |
455 | } |
456 | Result.Source = Source; |
457 | Result.AbsFile = D.getFilename().str(); |
458 | Result.InsideMainFile = D.getSourceMgr()->FindBufferContainingLoc( |
459 | Loc: D.getLoc()) == D.getSourceMgr()->getMainFileID(); |
460 | if (D.getRanges().empty()) |
461 | Result.Range = {.start: {.line: D.getLineNo() - 1, .character: D.getColumnNo()}, |
462 | .end: {.line: D.getLineNo() - 1, .character: D.getColumnNo()}}; |
463 | else |
464 | Result.Range = {.start: {.line: D.getLineNo() - 1, .character: (int)D.getRanges().front().first}, |
465 | .end: {.line: D.getLineNo() - 1, .character: (int)D.getRanges().front().second}}; |
466 | return Result; |
467 | } |
468 | |
469 | void toLSPDiags( |
470 | const Diag &D, const URIForFile &File, const ClangdDiagnosticOptions &Opts, |
471 | llvm::function_ref<void(clangd::Diagnostic, llvm::ArrayRef<Fix>)> OutFn) { |
472 | clangd::Diagnostic Main; |
473 | Main.severity = getSeverity(L: D.Severity); |
474 | // We downgrade severity for certain noisy warnings, like deprecated |
475 | // declartions. These already have visible decorations inside the editor and |
476 | // most users find the extra clutter in the UI (gutter, minimap, diagnostics |
477 | // views) overwhelming. |
478 | if (D.Severity == DiagnosticsEngine::Warning) { |
479 | if (llvm::is_contained(D.Tags, DiagnosticTag::Deprecated)) |
480 | Main.severity = getSeverity(L: DiagnosticsEngine::Remark); |
481 | } |
482 | |
483 | // Main diagnostic should always refer to a range inside main file. If a |
484 | // diagnostic made it so for, it means either itself or one of its notes is |
485 | // inside main file. It's also possible that there's a fix in the main file, |
486 | // but we preserve fixes iff primary diagnostic is in the main file. |
487 | if (D.InsideMainFile) { |
488 | Main.range = D.Range; |
489 | } else { |
490 | auto It = |
491 | llvm::find_if(D.Notes, [](const Note &N) { return N.InsideMainFile; }); |
492 | assert(It != D.Notes.end() && |
493 | "neither the main diagnostic nor notes are inside main file" ); |
494 | Main.range = It->Range; |
495 | } |
496 | |
497 | Main.code = D.Name; |
498 | if (auto URI = getDiagnosticDocURI(D.Source, ID: D.ID, Name: D.Name)) { |
499 | Main.codeDescription.emplace(); |
500 | Main.codeDescription->href = std::move(*URI); |
501 | } |
502 | switch (D.Source) { |
503 | case Diag::Clang: |
504 | Main.source = "clang" ; |
505 | break; |
506 | case Diag::ClangTidy: |
507 | Main.source = "clang-tidy" ; |
508 | break; |
509 | case Diag::Clangd: |
510 | Main.source = "clangd" ; |
511 | break; |
512 | case Diag::ClangdConfig: |
513 | Main.source = "clangd-config" ; |
514 | break; |
515 | case Diag::Unknown: |
516 | break; |
517 | } |
518 | if (Opts.SendDiagnosticCategory && !D.Category.empty()) |
519 | Main.category = D.Category; |
520 | |
521 | Main.message = mainMessage(D, Opts); |
522 | if (Opts.EmitRelatedLocations) { |
523 | Main.relatedInformation.emplace(); |
524 | for (auto &Note : D.Notes) { |
525 | if (!Note.AbsFile) { |
526 | vlog("Dropping note from unknown file: {0}" , Note); |
527 | continue; |
528 | } |
529 | DiagnosticRelatedInformation RelInfo; |
530 | RelInfo.location.range = Note.Range; |
531 | RelInfo.location.uri = |
532 | URIForFile::canonicalize(*Note.AbsFile, File.file()); |
533 | RelInfo.message = noteMessage(D, Note, Opts); |
534 | Main.relatedInformation->push_back(std::move(RelInfo)); |
535 | } |
536 | } |
537 | Main.tags = D.Tags; |
538 | // FIXME: Get rid of the copies here by taking in a mutable clangd::Diag. |
539 | for (auto &Entry : D.OpaqueData) |
540 | Main.data.insert({Entry.first, Entry.second}); |
541 | OutFn(std::move(Main), D.Fixes); |
542 | |
543 | // If we didn't emit the notes as relatedLocations, emit separate diagnostics |
544 | // so the user can find the locations easily. |
545 | if (!Opts.EmitRelatedLocations) |
546 | for (auto &Note : D.Notes) { |
547 | if (!Note.InsideMainFile) |
548 | continue; |
549 | clangd::Diagnostic Res; |
550 | Res.severity = getSeverity(Note.Severity); |
551 | Res.range = Note.Range; |
552 | Res.message = noteMessage(D, Note, Opts); |
553 | OutFn(std::move(Res), llvm::ArrayRef<Fix>()); |
554 | } |
555 | } |
556 | |
557 | int getSeverity(DiagnosticsEngine::Level L) { |
558 | switch (L) { |
559 | case DiagnosticsEngine::Remark: |
560 | return 4; |
561 | case DiagnosticsEngine::Note: |
562 | return 3; |
563 | case DiagnosticsEngine::Warning: |
564 | return 2; |
565 | case DiagnosticsEngine::Fatal: |
566 | case DiagnosticsEngine::Error: |
567 | return 1; |
568 | case DiagnosticsEngine::Ignored: |
569 | return 0; |
570 | } |
571 | llvm_unreachable("Unknown diagnostic level!" ); |
572 | } |
573 | |
574 | std::vector<Diag> StoreDiags::take(const clang::tidy::ClangTidyContext *Tidy) { |
575 | // Do not forget to emit a pending diagnostic if there is one. |
576 | flushLastDiag(); |
577 | |
578 | // Fill in name/source now that we have all the context needed to map them. |
579 | for (auto &Diag : Output) { |
580 | if (const char *ClangDiag = getDiagnosticCode(Diag.ID)) { |
581 | // Warnings controlled by -Wfoo are better recognized by that name. |
582 | StringRef Warning = DiagnosticIDs::getWarningOptionForDiag(Diag.ID); |
583 | if (!Warning.empty()) { |
584 | Diag.Name = ("-W" + Warning).str(); |
585 | } else { |
586 | StringRef Name(ClangDiag); |
587 | // Almost always an error, with a name like err_enum_class_reference. |
588 | // Drop the err_ prefix for brevity. |
589 | Name.consume_front("err_" ); |
590 | Diag.Name = std::string(Name); |
591 | } |
592 | Diag.Source = Diag::Clang; |
593 | } else if (Tidy != nullptr) { |
594 | std::string TidyDiag = Tidy->getCheckName(Diag.ID); |
595 | if (!TidyDiag.empty()) { |
596 | Diag.Name = std::move(TidyDiag); |
597 | Diag.Source = Diag::ClangTidy; |
598 | // clang-tidy bakes the name into diagnostic messages. Strip it out. |
599 | // It would be much nicer to make clang-tidy not do this. |
600 | auto CleanMessage = [&](std::string &Msg) { |
601 | StringRef Rest(Msg); |
602 | if (Rest.consume_back("]" ) && Rest.consume_back(Diag.Name) && |
603 | Rest.consume_back(" [" )) |
604 | Msg.resize(Rest.size()); |
605 | }; |
606 | CleanMessage(Diag.Message); |
607 | for (auto &Note : Diag.Notes) |
608 | CleanMessage(Note.Message); |
609 | for (auto &Fix : Diag.Fixes) |
610 | CleanMessage(Fix.Message); |
611 | } |
612 | } |
613 | setTags(Diag); |
614 | } |
615 | // Deduplicate clang-tidy diagnostics -- some clang-tidy checks may emit |
616 | // duplicated messages due to various reasons (e.g. the check doesn't handle |
617 | // template instantiations well; clang-tidy alias checks). |
618 | std::set<std::pair<Range, std::string>> SeenDiags; |
619 | llvm::erase_if(Output, [&](const Diag &D) { |
620 | return !SeenDiags.emplace(D.Range, D.Message).second; |
621 | }); |
622 | return std::move(Output); |
623 | } |
624 | |
625 | void StoreDiags::BeginSourceFile(const LangOptions &Opts, |
626 | const Preprocessor *PP) { |
627 | LangOpts = Opts; |
628 | if (PP) { |
629 | OrigSrcMgr = &PP->getSourceManager(); |
630 | } |
631 | } |
632 | |
633 | void StoreDiags::EndSourceFile() { |
634 | flushLastDiag(); |
635 | LangOpts = std::nullopt; |
636 | OrigSrcMgr = nullptr; |
637 | } |
638 | |
639 | /// Sanitizes a piece for presenting it in a synthesized fix message. Ensures |
640 | /// the result is not too large and does not contain newlines. |
641 | static void writeCodeToFixMessage(llvm::raw_ostream &OS, llvm::StringRef Code) { |
642 | constexpr unsigned MaxLen = 50; |
643 | if (Code == "\n" ) { |
644 | OS << "\\n" ; |
645 | return; |
646 | } |
647 | // Only show the first line if there are many. |
648 | llvm::StringRef R = Code.split('\n').first; |
649 | // Shorten the message if it's too long. |
650 | R = R.take_front(N: MaxLen); |
651 | |
652 | OS << R; |
653 | if (R.size() != Code.size()) |
654 | OS << "…" ; |
655 | } |
656 | |
657 | /// Fills \p D with all information, except the location-related bits. |
658 | /// Also note that ID and Name are not part of clangd::DiagBase and should be |
659 | /// set elsewhere. |
660 | static void fillNonLocationData(DiagnosticsEngine::Level DiagLevel, |
661 | const clang::Diagnostic &Info, |
662 | clangd::DiagBase &D) { |
663 | llvm::SmallString<64> Message; |
664 | Info.FormatDiagnostic(Message); |
665 | |
666 | D.Message = std::string(Message); |
667 | D.Severity = DiagLevel; |
668 | D.Category = DiagnosticIDs::getCategoryNameFromID( |
669 | CategoryID: DiagnosticIDs::getCategoryNumberForDiag(DiagID: Info.getID())) |
670 | .str(); |
671 | } |
672 | |
673 | void StoreDiags::HandleDiagnostic(DiagnosticsEngine::Level DiagLevel, |
674 | const clang::Diagnostic &Info) { |
675 | // If the diagnostic was generated for a different SourceManager, skip it. |
676 | // This happens when a module is imported and needs to be implicitly built. |
677 | // The compilation of that module will use the same StoreDiags, but different |
678 | // SourceManager. |
679 | if (OrigSrcMgr && Info.hasSourceManager() && |
680 | OrigSrcMgr != &Info.getSourceManager()) { |
681 | IgnoreDiagnostics::log(DiagLevel, Info); |
682 | return; |
683 | } |
684 | |
685 | DiagnosticConsumer::HandleDiagnostic(DiagLevel, Info); |
686 | bool OriginallyError = |
687 | Info.getDiags()->getDiagnosticIDs()->isDefaultMappingAsError( |
688 | DiagID: Info.getID()); |
689 | |
690 | if (Info.getLocation().isInvalid()) { |
691 | // Handle diagnostics coming from command-line arguments. The source manager |
692 | // is *not* available at this point, so we cannot use it. |
693 | if (!OriginallyError) { |
694 | IgnoreDiagnostics::log(DiagLevel, Info); |
695 | return; // non-errors add too much noise, do not show them. |
696 | } |
697 | |
698 | flushLastDiag(); |
699 | |
700 | LastDiag = Diag(); |
701 | LastDiagLoc.reset(); |
702 | LastDiagOriginallyError = OriginallyError; |
703 | LastDiag->ID = Info.getID(); |
704 | fillNonLocationData(DiagLevel, Info, D&: *LastDiag); |
705 | LastDiag->InsideMainFile = true; |
706 | // Put it at the start of the main file, for a lack of a better place. |
707 | LastDiag->Range.start = Position{.line: 0, .character: 0}; |
708 | LastDiag->Range.end = Position{.line: 0, .character: 0}; |
709 | return; |
710 | } |
711 | |
712 | if (!LangOpts || !Info.hasSourceManager()) { |
713 | IgnoreDiagnostics::log(DiagLevel, Info); |
714 | return; |
715 | } |
716 | |
717 | SourceManager &SM = Info.getSourceManager(); |
718 | |
719 | auto FillDiagBase = [&](DiagBase &D) { |
720 | fillNonLocationData(DiagLevel, Info, D); |
721 | |
722 | SourceLocation PatchLoc = |
723 | translatePreamblePatchLocation(Loc: Info.getLocation(), SM); |
724 | D.InsideMainFile = isInsideMainFile(Loc: PatchLoc, SM); |
725 | if (auto DRange = diagnosticRange(D: Info, L: *LangOpts)) |
726 | D.Range = *DRange; |
727 | else |
728 | D.Severity = DiagnosticsEngine::Ignored; |
729 | auto FID = SM.getFileID(SpellingLoc: Info.getLocation()); |
730 | if (const auto FE = SM.getFileEntryRefForID(FID)) { |
731 | D.File = FE->getName().str(); |
732 | D.AbsFile = getCanonicalPath(F: *FE, FileMgr&: SM.getFileManager()); |
733 | } |
734 | D.ID = Info.getID(); |
735 | return D; |
736 | }; |
737 | |
738 | auto AddFix = [&](bool SyntheticMessage) -> bool { |
739 | assert(!Info.getFixItHints().empty() && |
740 | "diagnostic does not have attached fix-its" ); |
741 | // No point in generating fixes, if the diagnostic is for a different file. |
742 | if (!LastDiag->InsideMainFile) |
743 | return false; |
744 | // Copy as we may modify the ranges. |
745 | auto FixIts = Info.getFixItHints().vec(); |
746 | llvm::SmallVector<TextEdit, 1> Edits; |
747 | for (auto &FixIt : FixIts) { |
748 | // Allow fixits within a single macro-arg expansion to be applied. |
749 | // This can be incorrect if the argument is expanded multiple times in |
750 | // different contexts. Hopefully this is rare! |
751 | if (FixIt.RemoveRange.getBegin().isMacroID() && |
752 | FixIt.RemoveRange.getEnd().isMacroID() && |
753 | SM.getFileID(FixIt.RemoveRange.getBegin()) == |
754 | SM.getFileID(FixIt.RemoveRange.getEnd())) { |
755 | FixIt.RemoveRange = CharSourceRange( |
756 | {SM.getTopMacroCallerLoc(FixIt.RemoveRange.getBegin()), |
757 | SM.getTopMacroCallerLoc(FixIt.RemoveRange.getEnd())}, |
758 | FixIt.RemoveRange.isTokenRange()); |
759 | } |
760 | // Otherwise, follow clang's behavior: no fixits in macros. |
761 | if (FixIt.RemoveRange.getBegin().isMacroID() || |
762 | FixIt.RemoveRange.getEnd().isMacroID()) |
763 | return false; |
764 | if (!isInsideMainFile(FixIt.RemoveRange.getBegin(), SM)) |
765 | return false; |
766 | Edits.push_back(toTextEdit(FixIt, SM, *LangOpts)); |
767 | } |
768 | |
769 | llvm::SmallString<64> Message; |
770 | // If requested and possible, create a message like "change 'foo' to 'bar'". |
771 | if (SyntheticMessage && FixIts.size() == 1) { |
772 | const auto &FixIt = FixIts.front(); |
773 | bool Invalid = false; |
774 | llvm::StringRef Remove = |
775 | Lexer::getSourceText(Range: FixIt.RemoveRange, SM, LangOpts: *LangOpts, Invalid: &Invalid); |
776 | llvm::StringRef Insert = FixIt.CodeToInsert; |
777 | if (!Invalid) { |
778 | llvm::raw_svector_ostream M(Message); |
779 | if (!Remove.empty() && !Insert.empty()) { |
780 | M << "change '" ; |
781 | writeCodeToFixMessage(OS&: M, Code: Remove); |
782 | M << "' to '" ; |
783 | writeCodeToFixMessage(OS&: M, Code: Insert); |
784 | M << "'" ; |
785 | } else if (!Remove.empty()) { |
786 | M << "remove '" ; |
787 | writeCodeToFixMessage(OS&: M, Code: Remove); |
788 | M << "'" ; |
789 | } else if (!Insert.empty()) { |
790 | M << "insert '" ; |
791 | writeCodeToFixMessage(OS&: M, Code: Insert); |
792 | M << "'" ; |
793 | } |
794 | // Don't allow source code to inject newlines into diagnostics. |
795 | std::replace(Message.begin(), Message.end(), '\n', ' '); |
796 | } |
797 | } |
798 | if (Message.empty()) // either !SyntheticMessage, or we failed to make one. |
799 | Info.FormatDiagnostic(Message); |
800 | LastDiag->Fixes.push_back( |
801 | x: Fix{std::string(Message), std::move(Edits), {}}); |
802 | return true; |
803 | }; |
804 | |
805 | if (!isNote(L: DiagLevel)) { |
806 | // Handle the new main diagnostic. |
807 | flushLastDiag(); |
808 | |
809 | LastDiag = Diag(); |
810 | // FIXME: Merge with feature modules. |
811 | if (Adjuster) |
812 | DiagLevel = Adjuster(DiagLevel, Info); |
813 | |
814 | FillDiagBase(*LastDiag); |
815 | if (isExcluded(DiagID: LastDiag->ID)) |
816 | LastDiag->Severity = DiagnosticsEngine::Ignored; |
817 | if (DiagCB) |
818 | DiagCB(Info, *LastDiag); |
819 | // Don't bother filling in the rest if diag is going to be dropped. |
820 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
821 | return; |
822 | |
823 | LastDiagLoc.emplace(Info.getLocation(), Info.getSourceManager()); |
824 | LastDiagOriginallyError = OriginallyError; |
825 | if (!Info.getFixItHints().empty()) |
826 | AddFix(true /* try to invent a message instead of repeating the diag */); |
827 | if (Fixer) { |
828 | auto = Fixer(LastDiag->Severity, Info); |
829 | LastDiag->Fixes.insert(LastDiag->Fixes.end(), ExtraFixes.begin(), |
830 | ExtraFixes.end()); |
831 | } |
832 | } else { |
833 | // Handle a note to an existing diagnostic. |
834 | if (!LastDiag) { |
835 | assert(false && "Adding a note without main diagnostic" ); |
836 | IgnoreDiagnostics::log(DiagLevel, Info); |
837 | return; |
838 | } |
839 | |
840 | // If a diagnostic was suppressed due to the suppression filter, |
841 | // also suppress notes associated with it. |
842 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
843 | return; |
844 | |
845 | // Give include-fixer a chance to replace a note with a fix. |
846 | if (Fixer) { |
847 | auto ReplacementFixes = Fixer(LastDiag->Severity, Info); |
848 | if (!ReplacementFixes.empty()) { |
849 | assert(Info.getNumFixItHints() == 0 && |
850 | "Include-fixer replaced a note with clang fix-its attached!" ); |
851 | LastDiag->Fixes.insert(LastDiag->Fixes.end(), ReplacementFixes.begin(), |
852 | ReplacementFixes.end()); |
853 | return; |
854 | } |
855 | } |
856 | |
857 | if (!Info.getFixItHints().empty()) { |
858 | // A clang note with fix-it is not a separate diagnostic in clangd. We |
859 | // attach it as a Fix to the main diagnostic instead. |
860 | if (!AddFix(false /* use the note as the message */)) |
861 | IgnoreDiagnostics::log(DiagLevel, Info); |
862 | } else { |
863 | // A clang note without fix-its corresponds to clangd::Note. |
864 | Note N; |
865 | FillDiagBase(N); |
866 | |
867 | LastDiag->Notes.push_back(std::move(N)); |
868 | } |
869 | } |
870 | } |
871 | |
872 | void StoreDiags::flushLastDiag() { |
873 | if (!LastDiag) |
874 | return; |
875 | auto Finish = llvm::make_scope_exit([&, NDiags(Output.size())] { |
876 | if (Output.size() == NDiags) // No new diag emitted. |
877 | vlog("Dropped diagnostic: {0}: {1}" , LastDiag->File, LastDiag->Message); |
878 | LastDiag.reset(); |
879 | }); |
880 | |
881 | if (LastDiag->Severity == DiagnosticsEngine::Ignored) |
882 | return; |
883 | // Move errors that occur from headers into main file. |
884 | if (!LastDiag->InsideMainFile && LastDiagLoc && LastDiagOriginallyError) { |
885 | if (tryMoveToMainFile(D&: *LastDiag, DiagLoc: *LastDiagLoc)) { |
886 | // Suppress multiple errors from the same inclusion. |
887 | if (!IncludedErrorLocations |
888 | .insert({LastDiag->Range.start.line, |
889 | LastDiag->Range.start.character}) |
890 | .second) |
891 | return; |
892 | } |
893 | } |
894 | if (!mentionsMainFile(D: *LastDiag)) |
895 | return; |
896 | Output.push_back(std::move(*LastDiag)); |
897 | } |
898 | |
899 | bool isBuiltinDiagnosticSuppressed(unsigned ID, |
900 | const llvm::StringSet<> &Suppress, |
901 | const LangOptions &LangOpts) { |
902 | // Don't complain about header-only stuff in mainfiles if it's a header. |
903 | // FIXME: would be cleaner to suppress in clang, once we decide whether the |
904 | // behavior should be to silently-ignore or respect the pragma. |
905 | if (ID == diag::pp_pragma_sysheader_in_main_file && LangOpts.IsHeaderFile) |
906 | return true; |
907 | |
908 | if (const char *CodePtr = getDiagnosticCode(ID)) { |
909 | if (Suppress.contains(normalizeSuppressedCode(CodePtr))) |
910 | return true; |
911 | } |
912 | StringRef Warning = DiagnosticIDs::getWarningOptionForDiag(DiagID: ID); |
913 | if (!Warning.empty() && Suppress.contains(Warning)) |
914 | return true; |
915 | return false; |
916 | } |
917 | |
918 | llvm::StringRef normalizeSuppressedCode(llvm::StringRef Code) { |
919 | Code.consume_front(Prefix: "err_" ); |
920 | Code.consume_front(Prefix: "-W" ); |
921 | return Code; |
922 | } |
923 | |
924 | std::optional<std::string> getDiagnosticDocURI(Diag::DiagSource Source, |
925 | unsigned ID, |
926 | llvm::StringRef Name) { |
927 | switch (Source) { |
928 | case Diag::Unknown: |
929 | break; |
930 | case Diag::Clang: |
931 | // There is a page listing many warning flags, but it provides too little |
932 | // information to be worth linking. |
933 | // https://clang.llvm.org/docs/DiagnosticsReference.html |
934 | break; |
935 | case Diag::ClangTidy: { |
936 | StringRef Module, Check; |
937 | // This won't correctly get the module for clang-analyzer checks, but as we |
938 | // don't link in the analyzer that shouldn't be an issue. |
939 | // This would also need updating if anyone decides to create a module with a |
940 | // '-' in the name. |
941 | std::tie(Module, Check) = Name.split('-'); |
942 | if (Module.empty() || Check.empty()) |
943 | return std::nullopt; |
944 | return ("https://clang.llvm.org/extra/clang-tidy/checks/" + Module + "/" + |
945 | Check + ".html" ) |
946 | .str(); |
947 | } |
948 | case Diag::Clangd: |
949 | if (Name == "unused-includes" || Name == "missing-includes" ) |
950 | return {"https://clangd.llvm.org/guides/include-cleaner" }; |
951 | break; |
952 | case Diag::ClangdConfig: |
953 | // FIXME: we should link to https://clangd.llvm.org/config |
954 | // However we have no diagnostic codes, which the link should describe! |
955 | break; |
956 | } |
957 | return std::nullopt; |
958 | } |
959 | |
960 | } // namespace clangd |
961 | } // namespace clang |
962 | |