ruff_db/diagnostic/mod.rs
1use std::fmt::{Display, Formatter};
2use std::{borrow::Cow, path::Path, sync::Arc};
3
4use ruff_diagnostics::{Applicability, Fix};
5use ruff_source_file::{LineColumn, SourceCode, SourceFile};
6
7use annotate_snippets::Level as AnnotateLevel;
8use ruff_text_size::{Ranged, TextRange, TextSize};
9#[cfg(feature = "serde")]
10use serde::Serialize;
11
12pub use self::render::{
13 DisplayDiagnostic, DisplayDiagnostics, DummyFileResolver, FileResolver, Input,
14};
15pub use self::stylesheet::{DiagnosticStylesheet, fmt_with_hyperlink};
16use crate::cancellation::CancellationToken;
17use crate::{Db, files::File};
18
19mod render;
20mod stylesheet;
21
22/// A collection of information that can be rendered into a diagnostic.
23///
24/// A diagnostic is a collection of information gathered by a tool intended
25/// for presentation to an end user, and which describes a group of related
26/// characteristics in the inputs given to the tool. Typically, but not always,
27/// a characteristic is a deficiency. An example of a characteristic that is
28/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
29#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
30pub struct Diagnostic {
31 /// The actual diagnostic.
32 ///
33 /// We box the diagnostic since it is somewhat big.
34 inner: Arc<DiagnosticInner>,
35}
36
37impl Diagnostic {
38 /// Create a new diagnostic with the given identifier, severity and
39 /// message.
40 ///
41 /// The identifier should be something that uniquely identifies the _type_
42 /// of diagnostic being reported. It should be usable as a reference point
43 /// for humans communicating about diagnostic categories. It will also
44 /// appear in the output when this diagnostic is rendered.
45 ///
46 /// The severity should describe the assumed level of importance to an end
47 /// user.
48 ///
49 /// The message is meant to be read by end users. The primary message
50 /// is meant to be a single terse description (usually a short phrase)
51 /// describing the group of related characteristics that the diagnostic
52 /// describes. Stated differently, if only one thing from a diagnostic can
53 /// be shown to an end user in a particular context, it is the primary
54 /// message.
55 ///
56 /// # Types implementing `IntoDiagnosticMessage`
57 ///
58 /// Callers can pass anything that implements `std::fmt::Display`
59 /// directly. If callers want or need to avoid cloning the diagnostic
60 /// message, then they can also pass a `DiagnosticMessage` directly.
61 pub fn new<'a>(
62 id: DiagnosticId,
63 severity: Severity,
64 message: impl IntoDiagnosticMessage + 'a,
65 ) -> Diagnostic {
66 let inner = Arc::new(DiagnosticInner {
67 id,
68 severity,
69 message: message.into_diagnostic_message(),
70 custom_concise_message: None,
71 documentation_url: None,
72 annotations: vec![],
73 subs: vec![],
74 fix: None,
75 parent: None,
76 noqa_offset: None,
77 secondary_code: None,
78 header_offset: 0,
79 });
80 Diagnostic { inner }
81 }
82
83 /// Creates a `Diagnostic` for a syntax error.
84 ///
85 /// Unlike the more general [`Diagnostic::new`], this requires a [`Span`] and a [`TextRange`]
86 /// attached to it.
87 ///
88 /// This should _probably_ be a method on the syntax errors, but
89 /// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of
90 /// the other way around. And since we want to do this conversion in a couple
91 /// places, it makes sense to centralize it _somewhere_. So it's here for now.
92 pub fn invalid_syntax(
93 span: impl Into<Span>,
94 message: impl IntoDiagnosticMessage,
95 range: impl Ranged,
96 ) -> Diagnostic {
97 let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
98 let span = span.into().with_range(range.range());
99 diag.annotate(Annotation::primary(span));
100 diag
101 }
102
103 /// Adds sub diagnostics that tell the user that this is a bug in ty
104 /// and asks them to open an issue on GitHub.
105 pub fn add_bug_sub_diagnostics(&mut self, url_encoded_title: &str) {
106 self.sub(SubDiagnostic::new(
107 SubDiagnosticSeverity::Info,
108 "This indicates a bug in ty.",
109 ));
110
111 self.sub(SubDiagnostic::new(
112 SubDiagnosticSeverity::Info,
113 format_args!(
114 "If you could open an issue at https://github.com/astral-sh/ty/issues/new?title={url_encoded_title}, we'd be very appreciative!"
115 ),
116 ));
117 self.sub(SubDiagnostic::new(
118 SubDiagnosticSeverity::Info,
119 format!(
120 "Platform: {os} {arch}",
121 os = std::env::consts::OS,
122 arch = std::env::consts::ARCH
123 ),
124 ));
125 if let Some(version) = crate::program_version() {
126 self.sub(SubDiagnostic::new(
127 SubDiagnosticSeverity::Info,
128 format!("Version: {version}"),
129 ));
130 }
131
132 self.sub(SubDiagnostic::new(
133 SubDiagnosticSeverity::Info,
134 format!(
135 "Args: {args:?}",
136 args = std::env::args().collect::<Vec<_>>()
137 ),
138 ));
139 }
140
141 /// Add an annotation to this diagnostic.
142 ///
143 /// Annotations for a diagnostic are optional, but if any are added,
144 /// callers should strive to make at least one of them primary. That is, it
145 /// should be constructed via [`Annotation::primary`]. A diagnostic with no
146 /// primary annotations is allowed, but its rendering may be sub-optimal.
147 pub fn annotate(&mut self, ann: Annotation) {
148 Arc::make_mut(&mut self.inner).annotations.push(ann);
149 }
150
151 /// Adds an "info" sub-diagnostic with the given message.
152 ///
153 /// If callers want to add an "info" sub-diagnostic with annotations, then
154 /// create a [`SubDiagnostic`] manually and use [`Diagnostic::sub`] to
155 /// attach it to a parent diagnostic.
156 ///
157 /// An "info" diagnostic is useful when contextualizing or otherwise
158 /// helpful information can be added to help end users understand the
159 /// headline message better. For example, if the headline message is about
160 /// a function call being invalid, a useful "info"
161 /// sub-diagnostic could show the function definition (or only the relevant
162 /// parts of it).
163 ///
164 /// # Types implementing `IntoDiagnosticMessage`
165 ///
166 /// Callers can pass anything that implements `std::fmt::Display`
167 /// directly. If callers want or need to avoid cloning the diagnostic
168 /// message, then they can also pass a `DiagnosticMessage` directly.
169 pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
170 self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
171 }
172
173 /// Adds an "info" sub-diagnostic before any existing sub-diagnostics.
174 pub fn prepend_info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
175 Arc::make_mut(&mut self.inner)
176 .subs
177 .insert(0, SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
178 }
179
180 /// Adds a "help" sub-diagnostic with the given message.
181 ///
182 /// See the closely related [`Diagnostic::info`] method for more details.
183 pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
184 self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
185 }
186
187 /// Adds a "sub" diagnostic to this diagnostic.
188 ///
189 /// This is useful when a sub diagnostic has its own annotations attached
190 /// to it. For the simpler case of a sub-diagnostic with only a message,
191 /// using a method like [`Diagnostic::info`] may be more convenient.
192 pub fn sub(&mut self, sub: SubDiagnostic) {
193 Arc::make_mut(&mut self.inner).subs.push(sub);
194 }
195
196 /// Return a `std::fmt::Display` implementation that renders this
197 /// diagnostic into a human readable format.
198 ///
199 /// Note that this `Display` impl includes a trailing line terminator, so
200 /// callers should prefer using this with `write!` instead of `writeln!`.
201 pub fn display<'a>(
202 &'a self,
203 resolver: &'a dyn FileResolver,
204 config: &'a DisplayDiagnosticConfig,
205 ) -> DisplayDiagnostic<'a> {
206 DisplayDiagnostic::new(resolver, config, self)
207 }
208
209 /// Returns the identifier for this diagnostic.
210 pub fn id(&self) -> DiagnosticId {
211 self.inner.id
212 }
213
214 /// Returns the headline message for this diagnostic.
215 ///
216 /// A diagnostic always has a message, but it may be empty.
217 pub fn headline_message(&self) -> &str {
218 self.inner.message.as_str()
219 }
220
221 /// Sets the headline message for this diagnostic.
222 pub fn set_headline_message(&mut self, message: impl IntoDiagnosticMessage) {
223 Arc::make_mut(&mut self.inner).message = message.into_diagnostic_message();
224 }
225
226 /// Introspects this diagnostic and returns its message for concise formatting.
227 ///
228 /// When we concisely format diagnostics, we likely want to not only
229 /// include the headline message but also the message attached
230 /// to the primary annotation. In particular, the primary annotation often
231 /// contains *essential* information or context for understanding the
232 /// diagnostic.
233 ///
234 /// The type returned implements the `std::fmt::Display` trait. In most
235 /// cases, just converting it to a string (or printing it) will do what
236 /// you want.
237 pub fn concise_message(&self) -> ConciseMessage<'_> {
238 if let Some(custom_message) = &self.inner.custom_concise_message {
239 return ConciseMessage::Custom(custom_message.as_str());
240 }
241
242 let main = self.inner.message.as_str();
243 let annotation = self
244 .primary_annotation()
245 .and_then(|ann| ann.get_message())
246 .unwrap_or_default();
247 if annotation.is_empty() {
248 ConciseMessage::MainDiagnostic(main)
249 } else {
250 ConciseMessage::Both { main, annotation }
251 }
252 }
253
254 /// Set a custom message for the concise formatting of this diagnostic.
255 ///
256 /// This overrides the default behavior of generating a concise message
257 /// from the headline message and the primary annotation.
258 pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
259 Arc::make_mut(&mut self.inner).custom_concise_message =
260 Some(message.into_diagnostic_message());
261 }
262
263 /// Remove the custom concise message, restoring the default behavior of generating a concise
264 /// message from the headline message and the primary annotation.
265 pub fn clear_concise_message(&mut self) {
266 Arc::make_mut(&mut self.inner).custom_concise_message = None;
267 }
268
269 /// Returns the severity of this diagnostic.
270 ///
271 /// Note that this may be different than the severity of sub-diagnostics.
272 pub fn severity(&self) -> Severity {
273 self.inner.severity
274 }
275
276 /// Returns a shared borrow of the "primary" annotation of this diagnostic
277 /// if one exists.
278 ///
279 /// When there are multiple primary annotations, then the first one that
280 /// was added to this diagnostic is returned.
281 pub fn primary_annotation(&self) -> Option<&Annotation> {
282 self.inner.annotations.iter().find(|ann| ann.is_primary)
283 }
284
285 /// Returns a mutable borrow of the "primary" annotation of this diagnostic
286 /// if one exists.
287 ///
288 /// When there are multiple primary annotations, then the first one that
289 /// was added to this diagnostic is returned.
290 pub fn primary_annotation_mut(&mut self) -> Option<&mut Annotation> {
291 Arc::make_mut(&mut self.inner)
292 .annotations
293 .iter_mut()
294 .find(|ann| ann.is_primary)
295 }
296
297 /// Returns all annotations in the order in which they were added.
298 pub fn annotations(&self) -> &[Annotation] {
299 &self.inner.annotations
300 }
301
302 /// Returns a mutable borrow of all annotations of this diagnostic.
303 pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
304 Arc::make_mut(&mut self.inner).annotations.iter_mut()
305 }
306
307 /// Returns the "primary" span of this diagnostic if one exists.
308 ///
309 /// When there are multiple primary spans, then the first one that was
310 /// added to this diagnostic is returned.
311 pub fn primary_span(&self) -> Option<Span> {
312 self.primary_annotation().map(|ann| ann.span.clone())
313 }
314
315 /// Returns a reference to the primary span of this diagnostic.
316 fn primary_span_ref(&self) -> Option<&Span> {
317 self.primary_annotation().map(|ann| &ann.span)
318 }
319
320 /// Returns the tags from the primary annotation of this diagnostic if it exists.
321 pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> {
322 self.primary_annotation().map(|ann| ann.tags.as_slice())
323 }
324
325 /// Returns the "primary" span of this diagnostic, panicking if it does not exist.
326 ///
327 /// This should typically only be used when working with diagnostics in Ruff, where diagnostics
328 /// are currently required to have a primary span.
329 ///
330 /// See [`Diagnostic::primary_span`] for more details.
331 pub fn expect_primary_span(&self) -> Span {
332 self.primary_span().expect("Expected a primary span")
333 }
334
335 /// Returns a key that can be used to sort two diagnostics into the canonical order
336 /// in which they should appear when rendered.
337 pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a {
338 RenderingSortKey {
339 db,
340 diagnostic: self,
341 }
342 }
343
344 /// Returns all annotations, skipping the first primary annotation.
345 pub fn secondary_annotations(&self) -> impl Iterator<Item = &Annotation> {
346 secondary_annotations(self.inner.annotations.iter())
347 }
348
349 pub fn sub_diagnostics(&self) -> &[SubDiagnostic] {
350 &self.inner.subs
351 }
352
353 /// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
354 pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
355 Arc::make_mut(&mut self.inner).subs.iter_mut()
356 }
357
358 /// Returns the fix for this diagnostic if it exists.
359 pub fn fix(&self) -> Option<&Fix> {
360 self.inner.fix.as_ref()
361 }
362
363 #[cfg(test)]
364 fn fix_mut(&mut self) -> Option<&mut Fix> {
365 Arc::make_mut(&mut self.inner).fix.as_mut()
366 }
367
368 /// Set the fix for this diagnostic.
369 pub fn set_fix(&mut self, fix: Fix) {
370 debug_assert!(
371 self.primary_span().is_some(),
372 "Expected a source file for a diagnostic with a fix"
373 );
374 Arc::make_mut(&mut self.inner).fix = Some(fix);
375 }
376
377 /// If `fix` is `Some`, set the fix for this diagnostic.
378 pub fn set_optional_fix(&mut self, fix: Option<Fix>) {
379 if let Some(fix) = fix {
380 self.set_fix(fix);
381 }
382 }
383
384 /// Remove the fix for this diagnostic.
385 pub fn remove_fix(&mut self) {
386 Arc::make_mut(&mut self.inner).fix = None;
387 }
388
389 /// Returns `true` if the diagnostic has a fix that applies at the configured applicability
390 /// level.
391 pub fn has_applicable_fix(&self, fix_applicability: Applicability) -> bool {
392 self.fix().is_some_and(|fix| fix.applies(fix_applicability))
393 }
394
395 pub fn documentation_url(&self) -> Option<&str> {
396 self.inner.documentation_url.as_deref()
397 }
398
399 pub fn set_documentation_url(&mut self, url: Option<String>) {
400 Arc::make_mut(&mut self.inner).documentation_url = url;
401 }
402
403 /// Returns the offset of the parent statement for this diagnostic if it exists.
404 ///
405 /// This is primarily used for checking noqa/secondary code suppressions.
406 pub fn parent(&self) -> Option<TextSize> {
407 self.inner.parent
408 }
409
410 /// Set the offset of the diagnostic's parent statement.
411 pub fn set_parent(&mut self, parent: TextSize) {
412 Arc::make_mut(&mut self.inner).parent = Some(parent);
413 }
414
415 /// Returns the remapped offset for a suppression comment if it exists.
416 ///
417 /// Like [`Diagnostic::parent`], this is used for noqa code suppression comments in Ruff.
418 #[cfg(feature = "serde")]
419 fn noqa_offset(&self) -> Option<TextSize> {
420 self.inner.noqa_offset
421 }
422
423 /// Set the remapped offset for a suppression comment.
424 pub fn set_noqa_offset(&mut self, noqa_offset: TextSize) {
425 Arc::make_mut(&mut self.inner).noqa_offset = Some(noqa_offset);
426 }
427
428 /// Returns the secondary code for the diagnostic if it exists.
429 ///
430 /// The "primary" code for the diagnostic is its lint name. Diagnostics in ty don't have
431 /// secondary codes (yet), but in Ruff the noqa code is used.
432 pub fn secondary_code(&self) -> Option<&SecondaryCode> {
433 self.inner.secondary_code.as_ref()
434 }
435
436 /// Returns the secondary code for the diagnostic if it exists, or the lint name otherwise.
437 ///
438 /// This is a common pattern for Ruff diagnostics, which want to use the noqa code in general,
439 /// but fall back on the `invalid-syntax` identifier for syntax errors, which don't have
440 /// secondary codes.
441 pub fn secondary_code_or_id(&self) -> &str {
442 self.secondary_code()
443 .map_or_else(|| self.inner.id.as_str(), SecondaryCode::as_str)
444 }
445
446 /// Set the secondary code for this diagnostic.
447 pub fn set_secondary_code(&mut self, code: SecondaryCode) {
448 Arc::make_mut(&mut self.inner).secondary_code = Some(code);
449 }
450
451 /// Returns the name used to represent the diagnostic.
452 pub fn name(&self) -> &'static str {
453 self.id().as_str()
454 }
455
456 /// Returns `true` if `self` is a syntax error message.
457 pub fn is_invalid_syntax(&self) -> bool {
458 self.id().is_invalid_syntax()
459 }
460
461 /// Returns the message of the first sub-diagnostic with a `Help` severity.
462 ///
463 /// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
464 /// general this is not the guaranteed meaning of such a message.
465 pub fn first_help_text(&self) -> Option<&str> {
466 self.sub_diagnostics()
467 .iter()
468 .find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
469 .map(|sub| sub.inner.message.as_str())
470 }
471
472 /// Returns the filename for the message.
473 ///
474 /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
475 pub fn expect_ruff_filename(&self) -> String {
476 self.expect_primary_span()
477 .expect_ruff_file()
478 .name()
479 .to_string()
480 }
481
482 /// Computes the start source location for the message.
483 ///
484 /// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
485 /// or if the span has no range.
486 pub fn ruff_start_location(&self) -> Option<LineColumn> {
487 Some(
488 self.ruff_source_file()?
489 .to_source_code()
490 .line_column(self.range()?.start()),
491 )
492 }
493
494 /// Computes the end source location for the message.
495 ///
496 /// Returns None if the diagnostic has no primary span, if its file is not a `SourceFile`,
497 /// or if the span has no range.
498 pub fn ruff_end_location(&self) -> Option<LineColumn> {
499 Some(
500 self.ruff_source_file()?
501 .to_source_code()
502 .line_column(self.range()?.end()),
503 )
504 }
505
506 /// Returns the [`SourceFile`] which the message belongs to.
507 pub fn ruff_source_file(&self) -> Option<&SourceFile> {
508 self.primary_span_ref()?.as_ruff_file()
509 }
510
511 /// Returns the [`SourceFile`] which the message belongs to.
512 ///
513 /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
514 fn expect_ruff_source_file(&self) -> &SourceFile {
515 self.ruff_source_file()
516 .expect("Expected a ruff source file")
517 }
518
519 /// Returns the [`TextRange`] for the diagnostic.
520 pub fn range(&self) -> Option<TextRange> {
521 self.primary_span()?.range()
522 }
523
524 /// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
525 ///
526 /// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
527 pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
528 let a = (
529 self.severity().is_fatal(),
530 self.expect_ruff_source_file(),
531 self.range().map(|r| r.start()),
532 );
533 let b = (
534 other.severity().is_fatal(),
535 other.expect_ruff_source_file(),
536 other.range().map(|r| r.start()),
537 );
538
539 a.cmp(&b)
540 }
541
542 /// Add an offset for aligning the header sigil with the line number separators in a diff.
543 pub fn set_header_offset(&mut self, offset: usize) {
544 Arc::make_mut(&mut self.inner).header_offset = offset;
545 }
546}
547
548#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
549struct DiagnosticInner {
550 id: DiagnosticId,
551 documentation_url: Option<String>,
552 severity: Severity,
553 message: DiagnosticMessage,
554 custom_concise_message: Option<DiagnosticMessage>,
555 annotations: Vec<Annotation>,
556 subs: Vec<SubDiagnostic>,
557 fix: Option<Fix>,
558 parent: Option<TextSize>,
559 noqa_offset: Option<TextSize>,
560 secondary_code: Option<SecondaryCode>,
561 header_offset: usize,
562}
563
564struct RenderingSortKey<'a> {
565 db: &'a dyn Db,
566 diagnostic: &'a Diagnostic,
567}
568
569impl Ord for RenderingSortKey<'_> {
570 // We sort diagnostics in a way that keeps them in source order
571 // and grouped by file. After that, we fall back to severity
572 // (with fatal messages sorting before info messages) and then
573 // finally the diagnostic ID and concise message.
574 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
575 if let (Some(span1), Some(span2)) = (
576 self.diagnostic.primary_span(),
577 other.diagnostic.primary_span(),
578 ) {
579 let file1 = span1.file();
580 let file2 = span2.file();
581 if file1 != file2 {
582 let order = file1.path(&self.db).cmp(file2.path(&self.db));
583 if order.is_ne() {
584 return order;
585 }
586 }
587
588 if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) {
589 let order = range1.start().cmp(&range2.start());
590 if order.is_ne() {
591 return order;
592 }
593 }
594 }
595 // Reverse so that, e.g., Fatal sorts before Info.
596 let order = self
597 .diagnostic
598 .severity()
599 .cmp(&other.diagnostic.severity())
600 .reverse();
601 if order.is_ne() {
602 return order;
603 }
604 let order = self.diagnostic.id().cmp(&other.diagnostic.id());
605 if order.is_ne() {
606 return order;
607 }
608
609 self.diagnostic
610 .concise_message()
611 .to_str()
612 .cmp(&other.diagnostic.concise_message().to_str())
613 }
614}
615
616impl PartialOrd for RenderingSortKey<'_> {
617 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
618 Some(self.cmp(other))
619 }
620}
621
622impl PartialEq for RenderingSortKey<'_> {
623 fn eq(&self, other: &Self) -> bool {
624 self.cmp(other).is_eq()
625 }
626}
627
628impl Eq for RenderingSortKey<'_> {}
629
630/// A collection of information subservient to a diagnostic.
631///
632/// A sub-diagnostic is always rendered after the parent diagnostic it is
633/// attached to. A parent diagnostic may have many sub-diagnostics, and it is
634/// guaranteed that they will not interleave with one another in rendering.
635///
636/// Currently, the order in which sub-diagnostics are rendered relative to one
637/// another (for a single parent diagnostic) is the order in which they were
638/// attached to the diagnostic.
639#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
640pub struct SubDiagnostic {
641 /// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
642 /// pointer-sized.
643 inner: Box<SubDiagnosticInner>,
644}
645
646impl SubDiagnostic {
647 /// Create a new sub-diagnostic with the given severity and message.
648 ///
649 /// The severity should describe the assumed level of importance to an end
650 /// user.
651 ///
652 /// The message is meant to be read by end users. The primary message
653 /// is meant to be a single terse description (usually a short phrase)
654 /// describing the group of related characteristics that the sub-diagnostic
655 /// describes. Stated differently, if only one thing from a diagnostic can
656 /// be shown to an end user in a particular context, it is the primary
657 /// message.
658 ///
659 /// # Types implementing `IntoDiagnosticMessage`
660 ///
661 /// Callers can pass anything that implements `std::fmt::Display`
662 /// directly. If callers want or need to avoid cloning the diagnostic
663 /// message, then they can also pass a `DiagnosticMessage` directly.
664 pub fn new<'a>(
665 severity: SubDiagnosticSeverity,
666 message: impl IntoDiagnosticMessage + 'a,
667 ) -> SubDiagnostic {
668 let inner = Box::new(SubDiagnosticInner {
669 severity,
670 message: message.into_diagnostic_message(),
671 annotations: vec![],
672 });
673 SubDiagnostic { inner }
674 }
675
676 /// Add an annotation to this sub-diagnostic.
677 ///
678 /// Annotations for a sub-diagnostic, like for a diagnostic, are optional.
679 /// If any are added, callers should strive to make at least one of them
680 /// primary. That is, it should be constructed via [`Annotation::primary`].
681 /// A diagnostic with no primary annotations is allowed, but its rendering
682 /// may be sub-optimal.
683 ///
684 /// Note that it is expected to be somewhat more common for sub-diagnostics
685 /// to have no annotations (e.g., a simple note) than for a diagnostic to
686 /// have no annotations.
687 pub fn annotate(&mut self, ann: Annotation) {
688 self.inner.annotations.push(ann);
689 }
690
691 pub fn annotations(&self) -> &[Annotation] {
692 &self.inner.annotations
693 }
694
695 /// Returns all annotations, skipping the first primary annotation.
696 pub fn secondary_annotations(&self) -> impl Iterator<Item = &Annotation> {
697 secondary_annotations(self.inner.annotations.iter())
698 }
699
700 /// Returns a mutable borrow of the annotations of this sub-diagnostic.
701 pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
702 self.inner.annotations.iter_mut()
703 }
704
705 /// Returns a shared borrow of the "primary" annotation of this diagnostic
706 /// if one exists.
707 ///
708 /// When there are multiple primary annotations, then the first one that
709 /// was added to this diagnostic is returned.
710 pub fn primary_annotation(&self) -> Option<&Annotation> {
711 self.inner.annotations.iter().find(|ann| ann.is_primary)
712 }
713
714 /// Returns a reference to the primary span of this sub-diagnostic.
715 pub fn primary_span_ref(&self) -> Option<&Span> {
716 self.primary_annotation().map(Annotation::get_span)
717 }
718
719 /// Returns the headline message for this sub-diagnostic.
720 ///
721 /// A sub-diagnostic always has a message, but it may be empty.
722 pub fn headline_message(&self) -> &str {
723 self.inner.message.as_str()
724 }
725
726 /// Introspects this sub-diagnostic and returns its message for concise formatting.
727 ///
728 /// When we concisely format diagnostics, we likely want to not only
729 /// include the headline message but also the message attached
730 /// to the primary annotation. In particular, the primary annotation often
731 /// contains *essential* information or context for understanding the
732 /// diagnostic.
733 ///
734 /// The type returned implements the `std::fmt::Display` trait. In most
735 /// cases, just converting it to a string (or printing it) will do what
736 /// you want.
737 pub fn concise_message(&self) -> ConciseMessage<'_> {
738 let main = self.headline_message();
739 let annotation = self
740 .primary_annotation()
741 .and_then(|ann| ann.get_message())
742 .unwrap_or_default();
743 if annotation.is_empty() {
744 ConciseMessage::MainDiagnostic(main)
745 } else {
746 ConciseMessage::Both { main, annotation }
747 }
748 }
749
750 pub fn severity(&self) -> SubDiagnosticSeverity {
751 self.inner.severity
752 }
753}
754
755#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
756struct SubDiagnosticInner {
757 severity: SubDiagnosticSeverity,
758 message: DiagnosticMessage,
759 annotations: Vec<Annotation>,
760}
761
762/// Returns all annotations, skipping the first primary annotation.
763fn secondary_annotations<'a>(
764 annotations: impl Iterator<Item = &'a Annotation>,
765) -> impl Iterator<Item = &'a Annotation> {
766 let mut seen_primary = false;
767 annotations.filter(move |ann| {
768 if seen_primary {
769 true
770 } else if ann.is_primary {
771 seen_primary = true;
772 false
773 } else {
774 true
775 }
776 })
777}
778
779/// A pointer to a subsequence in the end user's input.
780///
781/// Also known as an annotation, the pointer can optionally contain a short
782/// message, typically describing in general terms what is being pointed to.
783///
784/// An annotation is either primary or secondary, depending on whether it was
785/// constructed via [`Annotation::primary`] or [`Annotation::secondary`].
786/// Semantically, a primary annotation is meant to point to the "locus" of a
787/// diagnostic. Visually, the difference between a primary and a secondary
788/// annotation is usually just a different form of highlighting on the
789/// corresponding span.
790///
791/// # Advice
792///
793/// The span on an annotation should be as _specific_ as possible. For example,
794/// if there is a problem with a function call because one of its arguments has
795/// an invalid type, then the span should point to the specific argument and
796/// not to the entire function call.
797///
798/// Messages attached to annotations should also be as brief and specific as
799/// possible. Long messages could negative impact the quality of rendering.
800#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
801pub struct Annotation {
802 /// The span of this annotation, corresponding to some subsequence of the
803 /// user's input that we want to highlight.
804 span: Span,
805 /// An optional message associated with this annotation's span.
806 ///
807 /// When present, rendering will include this message in the output and
808 /// draw a line between the highlighted span and the message.
809 message: Option<DiagnosticMessage>,
810 /// Whether this annotation is "primary" or not. When it isn't primary, an
811 /// annotation is said to be "secondary."
812 is_primary: bool,
813 /// The diagnostic tags associated with this annotation.
814 tags: Vec<DiagnosticTag>,
815 /// Whether the snippet for this annotation should be hidden.
816 ///
817 /// When set, rendering will only include the file's name and (optional) range. Everything else
818 /// is omitted, including any file snippet or message.
819 hide_snippet: bool,
820}
821
822impl Annotation {
823 /// Create a "primary" annotation.
824 ///
825 /// A primary annotation is meant to highlight the "locus" of a diagnostic.
826 /// That is, it should point to something in the end user's input that is
827 /// the subject or "point" of a diagnostic.
828 ///
829 /// A diagnostic may have many primary annotations. A diagnostic may not
830 /// have any annotations, but if it does, at least one _ought_ to be
831 /// primary.
832 pub fn primary(span: Span) -> Annotation {
833 Annotation {
834 span,
835 message: None,
836 is_primary: true,
837 tags: Vec::new(),
838 hide_snippet: false,
839 }
840 }
841
842 /// Create a "secondary" annotation.
843 ///
844 /// A secondary annotation is meant to highlight relevant context for a
845 /// diagnostic, but not to point to the "locus" of the diagnostic.
846 ///
847 /// A diagnostic with only secondary annotations is usually not sensible,
848 /// but it is allowed and will produce a reasonable rendering.
849 pub fn secondary(span: Span) -> Annotation {
850 Annotation {
851 span,
852 message: None,
853 is_primary: false,
854 tags: Vec::new(),
855 hide_snippet: false,
856 }
857 }
858
859 /// Attach a message to this annotation.
860 ///
861 /// An annotation without a message will still have a presence in
862 /// rendering. In particular, it will highlight the span association with
863 /// this annotation in some way.
864 ///
865 /// When a message is attached to an annotation, then it will be associated
866 /// with the highlighted span in some way during rendering.
867 ///
868 /// # Types implementing `IntoDiagnosticMessage`
869 ///
870 /// Callers can pass anything that implements `std::fmt::Display`
871 /// directly. If callers want or need to avoid cloning the diagnostic
872 /// message, then they can also pass a `DiagnosticMessage` directly.
873 pub fn message<'a>(self, message: impl IntoDiagnosticMessage + 'a) -> Annotation {
874 let message = Some(message.into_diagnostic_message());
875 Annotation { message, ..self }
876 }
877
878 /// Sets the message on this annotation.
879 ///
880 /// If one was already set, then this overwrites it.
881 ///
882 /// This is useful if one needs to set the message on an annotation,
883 /// and all one has is a `&mut Annotation`. For example, via
884 /// `Diagnostic::primary_annotation_mut`.
885 pub fn set_message<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
886 self.message = Some(message.into_diagnostic_message());
887 }
888
889 /// Returns the message attached to this annotation, if one exists.
890 pub fn get_message(&self) -> Option<&str> {
891 self.message.as_ref().map(|m| m.as_str())
892 }
893
894 /// Returns the `Span` associated with this annotation.
895 pub fn get_span(&self) -> &Span {
896 &self.span
897 }
898
899 /// Sets the span on this annotation.
900 pub fn set_span(&mut self, span: Span) {
901 self.span = span;
902 }
903
904 /// Attaches an additional tag to this annotation.
905 pub fn push_tag(&mut self, tag: DiagnosticTag) {
906 self.tags.push(tag);
907 }
908
909 /// Set whether or not the snippet on this annotation should be suppressed when rendering.
910 ///
911 /// Such annotations are only rendered with their file name and range, if available. This is
912 /// intended for backwards compatibility with Ruff diagnostics, which historically used
913 /// `TextRange::default` to indicate a file-level diagnostic. In the new diagnostic model, a
914 /// [`Span`] with a range of `None` should be used instead, as mentioned in the `Span`
915 /// documentation.
916 ///
917 /// TODO(brent) update this usage in Ruff and remove `is_file_level` entirely. See
918 /// <https://github.com/astral-sh/ruff/issues/19688>, especially my first comment, for more
919 /// details. As of 2025-09-26 we also use this to suppress snippet rendering for formatter
920 /// diagnostics, which also need to have a range, so we probably can't eliminate this entirely.
921 pub fn hide_snippet(&mut self, yes: bool) {
922 self.hide_snippet = yes;
923 }
924
925 pub fn is_primary(&self) -> bool {
926 self.is_primary
927 }
928}
929
930/// Tags that can be associated with an annotation.
931///
932/// These tags are used to provide additional information about the annotation.
933/// and are passed through to the language server protocol.
934#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
935pub enum DiagnosticTag {
936 /// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
937 Unnecessary,
938 /// Deprecated or obsolete code.
939 Deprecated,
940}
941
942/// A string identifier for a lint rule.
943///
944/// This string is used in command line and configuration interfaces. The name should always
945/// be in kebab case, e.g. `no-foo` (all lower case).
946///
947/// Rules use kebab case, e.g. `no-foo`.
948#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
949pub struct LintName(&'static str);
950
951impl LintName {
952 pub const fn of(name: &'static str) -> Self {
953 Self(name)
954 }
955
956 pub const fn as_str(&self) -> &'static str {
957 self.0
958 }
959}
960
961impl std::ops::Deref for LintName {
962 type Target = str;
963
964 fn deref(&self) -> &Self::Target {
965 self.0
966 }
967}
968
969impl std::fmt::Display for LintName {
970 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
971 f.write_str(self.0)
972 }
973}
974
975impl PartialEq<str> for LintName {
976 fn eq(&self, other: &str) -> bool {
977 self.0 == other
978 }
979}
980
981impl PartialEq<&str> for LintName {
982 fn eq(&self, other: &&str) -> bool {
983 self.0 == *other
984 }
985}
986
987/// Uniquely identifies the kind of a diagnostic.
988#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
989pub enum DiagnosticId {
990 Panic,
991
992 /// Some I/O operation failed
993 Io,
994
995 /// Some code contains a syntax error
996 InvalidSyntax,
997
998 /// A lint violation.
999 ///
1000 /// Lints can be suppressed and some lints can be enabled or disabled in the configuration.
1001 Lint(LintName),
1002
1003 /// A revealed type: Created by `reveal_type(expression)`.
1004 RevealedType,
1005
1006 /// No rule with the given name exists.
1007 UnknownRule,
1008
1009 /// A glob pattern doesn't follow the expected syntax.
1010 InvalidGlob,
1011
1012 /// A PEP 723 script contains invalid metadata or configuration.
1013 InvalidScriptMetadata,
1014
1015 /// An `include` glob without any patterns.
1016 ///
1017 /// ## Why is this bad?
1018 /// An `include` glob without any patterns won't match any files. This is probably a mistake and
1019 /// either the `include` should be removed or a pattern should be added.
1020 ///
1021 /// ## Example
1022 /// ```toml
1023 /// [src]
1024 /// include = []
1025 /// ```
1026 ///
1027 /// Use instead:
1028 ///
1029 /// ```toml
1030 /// [src]
1031 /// include = ["src"]
1032 /// ```
1033 ///
1034 /// or remove the `include` option.
1035 EmptyInclude,
1036
1037 /// An override configuration is unnecessary because it applies to all files.
1038 ///
1039 /// ## Why is this bad?
1040 /// An overrides section that applies to all files is probably a mistake and can be rolled-up into the root configuration.
1041 ///
1042 /// ## Example
1043 /// ```toml
1044 /// [[overrides]]
1045 /// [overrides.rules]
1046 /// unused-reference = "ignore"
1047 /// ```
1048 ///
1049 /// Use instead:
1050 ///
1051 /// ```toml
1052 /// [rules]
1053 /// unused-reference = "ignore"
1054 /// ```
1055 ///
1056 /// or
1057 ///
1058 /// ```toml
1059 /// [[overrides]]
1060 /// include = ["test"]
1061 ///
1062 /// [overrides.rules]
1063 /// unused-reference = "ignore"
1064 /// ```
1065 UnnecessaryOverridesSection,
1066
1067 /// An `overrides` section in the configuration that doesn't contain any overrides.
1068 ///
1069 /// ## Why is this bad?
1070 /// An `overrides` section without any configuration overrides is probably a mistake.
1071 /// It is either a leftover after removing overrides, or a user forgot to add any overrides,
1072 /// or used an incorrect syntax to do so (e.g. used `rules` instead of `overrides.rules`).
1073 ///
1074 /// ## Example
1075 /// ```toml
1076 /// [[overrides]]
1077 /// include = ["test"]
1078 /// # no `[overrides.rules]`
1079 /// ```
1080 UselessOverridesSection,
1081
1082 /// Use of a deprecated setting.
1083 DeprecatedSetting,
1084
1085 /// Use of a Python version that ty doesn't support.
1086 UnsupportedPythonVersion,
1087
1088 /// The code needs to be formatted.
1089 Unformatted,
1090
1091 /// Use of an invalid command-line option.
1092 InvalidCliOption,
1093
1094 /// Experimental feature requires preview mode.
1095 PreviewFeature,
1096
1097 /// An internal assumption was violated.
1098 ///
1099 /// This indicates a bug in the program rather than a user error.
1100 InternalError,
1101}
1102
1103impl DiagnosticId {
1104 /// Creates a new `DiagnosticId` for a lint with the given name.
1105 pub const fn lint(name: &'static str) -> Self {
1106 Self::Lint(LintName::of(name))
1107 }
1108
1109 /// Returns `true` if this `DiagnosticId` represents a lint.
1110 pub fn is_lint(&self) -> bool {
1111 matches!(self, DiagnosticId::Lint(_))
1112 }
1113
1114 pub const fn as_lint(&self) -> Option<LintName> {
1115 match self {
1116 DiagnosticId::Lint(name) => Some(*name),
1117 _ => None,
1118 }
1119 }
1120
1121 /// Returns `true` if this `DiagnosticId` represents a lint with the given name.
1122 pub fn is_lint_named(&self, name: &str) -> bool {
1123 matches!(self, DiagnosticId::Lint(self_name) if self_name == name)
1124 }
1125
1126 pub fn strip_category(code: &str) -> Option<&str> {
1127 code.split_once(':').map(|(_, rest)| rest)
1128 }
1129
1130 /// Returns a concise description of this diagnostic ID.
1131 ///
1132 /// Note that this doesn't include the lint's category. It
1133 /// only includes the lint's name.
1134 pub fn as_str(&self) -> &'static str {
1135 match self {
1136 DiagnosticId::Panic => "panic",
1137 DiagnosticId::Io => "io",
1138 DiagnosticId::InvalidSyntax => "invalid-syntax",
1139 DiagnosticId::Lint(name) => name.as_str(),
1140 DiagnosticId::RevealedType => "revealed-type",
1141 DiagnosticId::UnknownRule => "unknown-rule",
1142 DiagnosticId::InvalidGlob => "invalid-glob",
1143 DiagnosticId::InvalidScriptMetadata => "invalid-script-metadata",
1144 DiagnosticId::EmptyInclude => "empty-include",
1145 DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section",
1146 DiagnosticId::UselessOverridesSection => "useless-overrides-section",
1147 DiagnosticId::DeprecatedSetting => "deprecated-setting",
1148 DiagnosticId::UnsupportedPythonVersion => "unsupported-python-version",
1149 DiagnosticId::Unformatted => "unformatted",
1150 DiagnosticId::InvalidCliOption => "invalid-cli-option",
1151 DiagnosticId::PreviewFeature => "preview-feature",
1152 DiagnosticId::InternalError => "internal-error",
1153 }
1154 }
1155
1156 fn is_invalid_syntax(&self) -> bool {
1157 matches!(self, Self::InvalidSyntax)
1158 }
1159}
1160
1161impl std::fmt::Display for DiagnosticId {
1162 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1163 write!(f, "{}", self.as_str())
1164 }
1165}
1166
1167/// A unified file representation for both ruff and ty.
1168///
1169/// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain
1170/// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff
1171/// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight
1172/// [`SourceFile`], respectively.
1173///
1174/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
1175/// emitting diagnostics from both ty and ruff.
1176#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
1177pub enum UnifiedFile {
1178 Ty(File),
1179 Ruff(SourceFile),
1180}
1181
1182impl UnifiedFile {
1183 fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str {
1184 match self {
1185 UnifiedFile::Ty(file) => resolver.path(*file),
1186 UnifiedFile::Ruff(file) => file.name(),
1187 }
1188 }
1189
1190 /// Return the file's path relative to the current working directory.
1191 fn relative_path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a Path {
1192 let cwd = resolver.current_directory();
1193 let path = Path::new(self.path(resolver));
1194
1195 if let Ok(path) = path.strip_prefix(cwd) {
1196 return path;
1197 }
1198
1199 path
1200 }
1201
1202 fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource {
1203 match self {
1204 UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)),
1205 UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()),
1206 }
1207 }
1208}
1209
1210/// A unified wrapper for types that can be converted to a [`SourceCode`].
1211///
1212/// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code.
1213/// [`DiagnosticSource`] wraps both of these and provides the single
1214/// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate
1215/// lifetimes.
1216///
1217/// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file
1218/// and [`FileResolver`].
1219#[derive(Clone, Debug)]
1220enum DiagnosticSource {
1221 Ty(Input),
1222 Ruff(SourceFile),
1223}
1224
1225impl DiagnosticSource {
1226 /// Returns this input as a `SourceCode` for convenient querying.
1227 fn as_source_code(&self) -> SourceCode<'_, '_> {
1228 match self {
1229 DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index),
1230 DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()),
1231 }
1232 }
1233}
1234
1235/// A span represents the source of a diagnostic.
1236///
1237/// It consists of a `File` and an optional range into that file. When the
1238/// range isn't present, it semantically implies that the diagnostic refers to
1239/// the entire file. For example, when the file should be executable but isn't.
1240#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
1241pub struct Span {
1242 file: UnifiedFile,
1243 range: Option<TextRange>,
1244}
1245
1246impl Span {
1247 /// Returns the `UnifiedFile` attached to this `Span`.
1248 pub fn file(&self) -> &UnifiedFile {
1249 &self.file
1250 }
1251
1252 /// Returns the range, if available, attached to this `Span`.
1253 ///
1254 /// When there is no range, it is convention to assume that this `Span`
1255 /// refers to the corresponding `File` as a whole. In some cases, consumers
1256 /// of this API may use the range `0..0` to represent this case.
1257 pub fn range(&self) -> Option<TextRange> {
1258 self.range
1259 }
1260
1261 /// Returns a new `Span` with the given `range` attached to it.
1262 pub fn with_range(self, range: TextRange) -> Span {
1263 self.with_optional_range(Some(range))
1264 }
1265
1266 /// Returns a new `Span` with the given optional `range` attached to it.
1267 pub fn with_optional_range(self, range: Option<TextRange>) -> Span {
1268 Span { range, ..self }
1269 }
1270
1271 /// Returns the [`File`] attached to this [`Span`].
1272 ///
1273 /// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`].
1274 pub fn expect_ty_file(&self) -> File {
1275 match self.file {
1276 UnifiedFile::Ty(file) => file,
1277 UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"),
1278 }
1279 }
1280
1281 /// Returns the [`SourceFile`] attached to this [`Span`].
1282 ///
1283 /// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`].
1284 fn expect_ruff_file(&self) -> &SourceFile {
1285 self.as_ruff_file()
1286 .expect("Expected a ruff `SourceFile`, found a ty `File`")
1287 }
1288
1289 /// Returns the [`SourceFile`] attached to this [`Span`].
1290 pub fn as_ruff_file(&self) -> Option<&SourceFile> {
1291 match &self.file {
1292 UnifiedFile::Ty(_) => None,
1293 UnifiedFile::Ruff(file) => Some(file),
1294 }
1295 }
1296}
1297
1298impl From<File> for Span {
1299 fn from(file: File) -> Span {
1300 let file = UnifiedFile::Ty(file);
1301 Span { file, range: None }
1302 }
1303}
1304
1305impl From<SourceFile> for Span {
1306 fn from(file: SourceFile) -> Self {
1307 let file = UnifiedFile::Ruff(file);
1308 Span { file, range: None }
1309 }
1310}
1311
1312impl From<crate::files::FileRange> for Span {
1313 fn from(file_range: crate::files::FileRange) -> Span {
1314 Span::from(file_range.file()).with_range(file_range.range())
1315 }
1316}
1317
1318#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
1319#[cfg_attr(feature = "serde", derive(Serialize))]
1320#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
1321pub enum Severity {
1322 Info,
1323 Warning,
1324 Error,
1325 Fatal,
1326}
1327
1328impl Severity {
1329 fn to_annotate(self) -> AnnotateLevel<'static> {
1330 match self {
1331 Severity::Info => AnnotateLevel::INFO,
1332 Severity::Warning => AnnotateLevel::WARNING,
1333 Severity::Error => AnnotateLevel::ERROR,
1334 // NOTE: Should we really collapse this to "error"?
1335 //
1336 // After collapsing this, the snapshot tests seem to reveal that we
1337 // don't currently have any *tests* with a `fatal` severity level.
1338 // And maybe *rendering* this as just an `error` is fine. If we
1339 // really do need different rendering, then I think we can add a
1340 // `Level::Fatal`. ---AG
1341 Severity::Fatal => AnnotateLevel::ERROR,
1342 }
1343 }
1344
1345 pub const fn is_fatal(self) -> bool {
1346 matches!(self, Severity::Fatal)
1347 }
1348}
1349
1350/// Like [`Severity`] but exclusively for sub-diagnostics.
1351///
1352/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
1353/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
1354/// deleted and the two combined again.
1355#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
1356pub enum SubDiagnosticSeverity {
1357 Help,
1358 Info,
1359 Warning,
1360 Error,
1361 Fatal,
1362}
1363
1364impl SubDiagnosticSeverity {
1365 fn to_annotate(self) -> AnnotateLevel<'static> {
1366 match self {
1367 SubDiagnosticSeverity::Help => AnnotateLevel::HELP,
1368 SubDiagnosticSeverity::Info => AnnotateLevel::INFO,
1369 SubDiagnosticSeverity::Warning => AnnotateLevel::WARNING,
1370 SubDiagnosticSeverity::Error => AnnotateLevel::ERROR,
1371 SubDiagnosticSeverity::Fatal => AnnotateLevel::ERROR,
1372 }
1373 }
1374}
1375
1376impl Display for SubDiagnosticSeverity {
1377 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1378 let s = match self {
1379 SubDiagnosticSeverity::Help => "help",
1380 SubDiagnosticSeverity::Info => "info",
1381 SubDiagnosticSeverity::Warning => "warning",
1382 SubDiagnosticSeverity::Error => "error",
1383 SubDiagnosticSeverity::Fatal => "fatal",
1384 };
1385 f.write_str(s)
1386 }
1387}
1388
1389/// Controls whether colored diagnostic output includes hyperlinks.
1390#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
1391pub enum HyperlinkMode {
1392 /// Detect hyperlink support from the environment.
1393 #[default]
1394 Auto,
1395 /// Always emit hyperlinks.
1396 Always,
1397 /// Never emit hyperlinks.
1398 Never,
1399}
1400
1401/// Configuration for rendering diagnostics.
1402#[derive(Clone, Debug)]
1403pub struct DisplayDiagnosticConfig {
1404 /// The program name used in structured output formats (e.g., JUnit, GitHub).
1405 program: &'static str,
1406 /// The format to use for diagnostic rendering.
1407 ///
1408 /// This uses the "full" format by default.
1409 format: DiagnosticFormat,
1410 /// Whether to enable colors or not.
1411 ///
1412 /// Disabled by default.
1413 color: bool,
1414 /// Whether to emit hyperlinks in colored diagnostic output.
1415 ///
1416 /// By default, hyperlink support is detected from the environment.
1417 hyperlinks: HyperlinkMode,
1418 /// Whether to anonymize line numbers in full diagnostic output.
1419 ///
1420 /// Disabled by default.
1421 anonymized_line_numbers: bool,
1422 /// The number of non-empty lines to show around each snippet.
1423 ///
1424 /// NOTE: It seems like making this a property of rendering *could*
1425 /// be wrong. In particular, I have a suspicion that we may want
1426 /// more granular control over this, perhaps based on the kind of
1427 /// diagnostic or even the snippet itself. But I chose to put this
1428 /// here for now as the most "sensible" place for it to live until
1429 /// we had more concrete use cases. ---AG
1430 context: usize,
1431 /// The "merge window" for annotations and fix diff hunks.
1432 ///
1433 /// Nearby annotations or fix edits are rendered in a single source frame even when their
1434 /// configured context windows would not otherwise overlap.
1435 merge_window: usize,
1436 /// Whether to use preview formatting for Ruff diagnostics.
1437 preview: bool,
1438 /// Whether to prefer rule codes over human-readable rule names in Ruff diagnostic output.
1439 prefer_rule_codes: bool,
1440 /// Whether to hide the real `Severity` of diagnostics.
1441 ///
1442 /// This is intended for temporary use by Ruff, which only has a single `error` severity at the
1443 /// moment. We should be able to remove this option when Ruff gets more severities.
1444 hide_severity: bool,
1445 /// Whether to show the availability of a fix in a diagnostic.
1446 show_fix_status: bool,
1447 /// The lowest applicability that should be shown when reporting diagnostics.
1448 fix_applicability: Applicability,
1449
1450 cancellation_token: Option<CancellationToken>,
1451}
1452
1453impl DisplayDiagnosticConfig {
1454 pub fn new(program: &'static str) -> DisplayDiagnosticConfig {
1455 DisplayDiagnosticConfig {
1456 program,
1457 format: DiagnosticFormat::default(),
1458 color: false,
1459 hyperlinks: HyperlinkMode::Auto,
1460 anonymized_line_numbers: false,
1461 context: 2,
1462 merge_window: 2,
1463 preview: false,
1464 prefer_rule_codes: false,
1465 hide_severity: false,
1466 show_fix_status: false,
1467 fix_applicability: Applicability::Safe,
1468 cancellation_token: None,
1469 }
1470 }
1471
1472 /// Whether to enable concise diagnostic output or not.
1473 pub fn format(self, format: DiagnosticFormat) -> DisplayDiagnosticConfig {
1474 DisplayDiagnosticConfig { format, ..self }
1475 }
1476
1477 /// Whether to enable colors or not.
1478 pub fn color(self, yes: bool) -> DisplayDiagnosticConfig {
1479 DisplayDiagnosticConfig { color: yes, ..self }
1480 }
1481
1482 /// Configures hyperlink rendering for colored diagnostic output.
1483 pub fn hyperlinks(self, mode: HyperlinkMode) -> DisplayDiagnosticConfig {
1484 DisplayDiagnosticConfig {
1485 hyperlinks: mode,
1486 ..self
1487 }
1488 }
1489
1490 /// Whether to anonymize line numbers in full diagnostic output.
1491 pub fn anonymized_line_numbers(self, yes: bool) -> DisplayDiagnosticConfig {
1492 DisplayDiagnosticConfig {
1493 anonymized_line_numbers: yes,
1494 ..self
1495 }
1496 }
1497
1498 /// Set the number of contextual lines to show around each snippet.
1499 pub fn context(self, lines: usize) -> DisplayDiagnosticConfig {
1500 DisplayDiagnosticConfig {
1501 context: lines,
1502 ..self
1503 }
1504 }
1505
1506 /// Set the "merge window" for annotations and fix diff hunks.
1507 ///
1508 /// Nearby annotations or fix edits are rendered in a single source frame even when their
1509 /// configured context windows would not otherwise overlap.
1510 #[cfg(test)]
1511 fn merge_window(self, lines: usize) -> DisplayDiagnosticConfig {
1512 DisplayDiagnosticConfig {
1513 merge_window: lines,
1514 ..self
1515 }
1516 }
1517
1518 /// Whether to enable preview behavior or not.
1519 pub fn preview(self, yes: bool) -> DisplayDiagnosticConfig {
1520 DisplayDiagnosticConfig {
1521 preview: yes,
1522 ..self
1523 }
1524 }
1525
1526 pub fn preview_enabled(&self) -> bool {
1527 self.preview
1528 }
1529
1530 /// Whether to prefer rule codes over human-readable rule names, even in preview mode.
1531 pub fn prefer_rule_codes(self, yes: bool) -> DisplayDiagnosticConfig {
1532 DisplayDiagnosticConfig {
1533 prefer_rule_codes: yes,
1534 ..self
1535 }
1536 }
1537
1538 /// Whether rule codes are explicitly preferred over human-readable rule names.
1539 pub fn is_prefer_rule_codes_enabled(&self) -> bool {
1540 self.prefer_rule_codes
1541 }
1542
1543 /// Whether to hide a diagnostic's severity or not.
1544 pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig {
1545 DisplayDiagnosticConfig {
1546 hide_severity: yes,
1547 ..self
1548 }
1549 }
1550
1551 /// Whether to show a fix's availability or not.
1552 pub fn with_show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
1553 DisplayDiagnosticConfig {
1554 show_fix_status: yes,
1555 ..self
1556 }
1557 }
1558
1559 /// Set the lowest fix applicability that should be shown.
1560 ///
1561 /// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
1562 /// availability for unsafe or display-only fixes.
1563 ///
1564 /// Note that this option is currently ignored when `hide_severity` is false.
1565 pub fn with_fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
1566 DisplayDiagnosticConfig {
1567 fix_applicability: applicability,
1568 ..self
1569 }
1570 }
1571
1572 pub fn show_fix_status(&self) -> bool {
1573 self.show_fix_status
1574 }
1575
1576 pub fn fix_applicability(&self) -> Applicability {
1577 self.fix_applicability
1578 }
1579
1580 pub fn with_cancellation_token(
1581 mut self,
1582 token: Option<CancellationToken>,
1583 ) -> DisplayDiagnosticConfig {
1584 self.cancellation_token = token;
1585 self
1586 }
1587
1588 fn is_canceled(&self) -> bool {
1589 self.cancellation_token
1590 .as_ref()
1591 .is_some_and(|token| token.is_cancelled())
1592 }
1593}
1594
1595/// The diagnostic output format.
1596#[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
1597#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
1598#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
1599#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1600pub enum DiagnosticFormat {
1601 /// The default full mode will print "pretty" diagnostics.
1602 ///
1603 /// That is, color will be used when printing to a `tty`.
1604 /// Moreover, diagnostic messages may include additional
1605 /// context and annotations on the input to help understand
1606 /// the message.
1607 #[default]
1608 Full,
1609 /// Print diagnostics in a concise mode.
1610 ///
1611 /// This will guarantee that each diagnostic is printed on
1612 /// a single line. Only the most important or primary aspects
1613 /// of the diagnostic are included. Contextual information is
1614 /// dropped.
1615 ///
1616 /// This may use color when printing to a `tty`.
1617 Concise,
1618 /// Print diagnostics in the [Azure Pipelines] format.
1619 ///
1620 /// [Azure Pipelines]: https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#logissue-log-an-error-or-warning
1621 Azure,
1622 /// Print diagnostics in JSON format.
1623 ///
1624 /// Unlike `json-lines`, this prints all of the diagnostics as a JSON array.
1625 #[cfg(feature = "serde")]
1626 Json,
1627 /// Print diagnostics in JSON format, one per line.
1628 ///
1629 /// This will print each diagnostic as a separate JSON object on its own line. See the `json`
1630 /// format for an array of all diagnostics. See <https://jsonlines.org/> for more details.
1631 #[cfg(feature = "serde")]
1632 JsonLines,
1633 /// Print diagnostics in the JSON format expected by [reviewdog].
1634 ///
1635 /// [reviewdog]: https://github.com/reviewdog/reviewdog
1636 #[cfg(feature = "serde")]
1637 Rdjson,
1638 /// Print diagnostics in the format emitted by Pylint.
1639 Pylint,
1640 /// Print diagnostics in the format expected by JUnit.
1641 #[cfg(feature = "junit")]
1642 Junit,
1643 /// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
1644 ///
1645 /// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
1646 #[cfg(feature = "serde")]
1647 Gitlab,
1648
1649 /// Print diagnostics in the format used by [GitHub Actions] workflow error annotations.
1650 ///
1651 /// [GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message
1652 Github,
1653}
1654
1655/// A representation of the kinds of messages inside a diagnostic.
1656pub enum ConciseMessage<'a> {
1657 /// A diagnostic contains a non-empty headline message and an empty
1658 /// primary annotation message.
1659 MainDiagnostic(&'a str),
1660 /// A diagnostic contains a non-empty headline message and a non-empty
1661 /// primary annotation message.
1662 Both { main: &'a str, annotation: &'a str },
1663 /// A custom concise message has been provided.
1664 Custom(&'a str),
1665}
1666
1667impl<'a> ConciseMessage<'a> {
1668 pub fn to_str(&self) -> Cow<'a, str> {
1669 match self {
1670 ConciseMessage::MainDiagnostic(s) | ConciseMessage::Custom(s) => Cow::Borrowed(s),
1671 ConciseMessage::Both { .. } => Cow::Owned(self.to_string()),
1672 }
1673 }
1674}
1675
1676impl std::fmt::Display for ConciseMessage<'_> {
1677 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
1678 match *self {
1679 ConciseMessage::MainDiagnostic(main) => {
1680 write!(f, "{main}")
1681 }
1682 ConciseMessage::Both { main, annotation } => {
1683 write!(f, "{main}: {annotation}")
1684 }
1685 ConciseMessage::Custom(message) => {
1686 write!(f, "{message}")
1687 }
1688 }
1689 }
1690}
1691
1692#[cfg(feature = "serde")]
1693impl serde::Serialize for ConciseMessage<'_> {
1694 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1695 where
1696 S: serde::Serializer,
1697 {
1698 serializer.collect_str(self)
1699 }
1700}
1701
1702/// A diagnostic message string.
1703///
1704/// This is, for all intents and purposes, equivalent to a `Box<str>`.
1705/// But it does not implement `std::fmt::Display`. Indeed, that it its
1706/// entire reason for existence. It provides a way to pass a string
1707/// directly into diagnostic methods that accept messages without copying
1708/// that string. This works via the `IntoDiagnosticMessage` trait.
1709///
1710/// In most cases, callers shouldn't need to use this. Instead, there is
1711/// a blanket trait implementation for `IntoDiagnosticMessage` for
1712/// anything that implements `std::fmt::Display`.
1713#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
1714pub struct DiagnosticMessage(Box<str>);
1715
1716impl DiagnosticMessage {
1717 /// Returns this message as a borrowed string.
1718 pub fn as_str(&self) -> &str {
1719 &self.0
1720 }
1721}
1722
1723impl From<&str> for DiagnosticMessage {
1724 fn from(s: &str) -> DiagnosticMessage {
1725 DiagnosticMessage(s.into())
1726 }
1727}
1728
1729impl From<String> for DiagnosticMessage {
1730 fn from(s: String) -> DiagnosticMessage {
1731 DiagnosticMessage(s.into())
1732 }
1733}
1734
1735impl From<Box<str>> for DiagnosticMessage {
1736 fn from(s: Box<str>) -> DiagnosticMessage {
1737 DiagnosticMessage(s)
1738 }
1739}
1740
1741impl IntoDiagnosticMessage for DiagnosticMessage {
1742 fn into_diagnostic_message(self) -> DiagnosticMessage {
1743 self
1744 }
1745}
1746
1747/// A trait for values that can be converted into a diagnostic message.
1748///
1749/// Users of the diagnostic API can largely think of this trait as effectively
1750/// equivalent to `std::fmt::Display`. Indeed, everything that implements
1751/// `Display` also implements this trait. That means wherever this trait is
1752/// accepted, you can use things like `format_args!`.
1753///
1754/// The purpose of this trait is to provide a means to give arguments _other_
1755/// than `std::fmt::Display` trait implementations. Or rather, to permit
1756/// the diagnostic API to treat them differently. For example, this lets
1757/// callers wrap a string in a `DiagnosticMessage` and provide it directly
1758/// to any of the diagnostic APIs that accept a message. This will move the
1759/// string and avoid any unnecessary copies. (If we instead required only
1760/// `std::fmt::Display`, then this would potentially result in a copy via the
1761/// `ToString` trait implementation.)
1762pub trait IntoDiagnosticMessage {
1763 fn into_diagnostic_message(self) -> DiagnosticMessage;
1764}
1765
1766/// Every `IntoDiagnosticMessage` is accepted, so to is `std::fmt::Display`.
1767impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
1768 fn into_diagnostic_message(self) -> DiagnosticMessage {
1769 DiagnosticMessage::from(self.to_string())
1770 }
1771}
1772
1773/// A secondary identifier for a lint diagnostic.
1774///
1775/// For Ruff rules this means the noqa code.
1776#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
1777#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
1778pub struct SecondaryCode(String);
1779
1780impl SecondaryCode {
1781 pub fn new(code: String) -> Self {
1782 Self(code)
1783 }
1784
1785 pub fn as_str(&self) -> &str {
1786 &self.0
1787 }
1788}
1789
1790impl std::fmt::Display for SecondaryCode {
1791 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1792 f.write_str(&self.0)
1793 }
1794}
1795
1796impl std::ops::Deref for SecondaryCode {
1797 type Target = str;
1798
1799 fn deref(&self) -> &Self::Target {
1800 &self.0
1801 }
1802}
1803
1804impl PartialEq<&str> for SecondaryCode {
1805 fn eq(&self, other: &&str) -> bool {
1806 self.0 == *other
1807 }
1808}
1809
1810impl PartialEq<SecondaryCode> for &str {
1811 fn eq(&self, other: &SecondaryCode) -> bool {
1812 other.eq(self)
1813 }
1814}
1815
1816// for `hashbrown::EntryRef`
1817impl From<&SecondaryCode> for SecondaryCode {
1818 fn from(value: &SecondaryCode) -> Self {
1819 value.clone()
1820 }
1821}