Skip to main content

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}