use std::{fmt::Formatter, path::Path, sync::Arc}; use ruff_diagnostics::{Applicability, Fix}; use ruff_source_file::{LineColumn, SourceCode, SourceFile}; use ruff_annotate_snippets::Level as AnnotateLevel; use ruff_text_size::{Ranged, TextRange, TextSize}; pub use self::render::{ DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary, }; use crate::{Db, files::File}; mod render; mod stylesheet; /// A collection of information that can be rendered into a diagnostic. /// /// A diagnostic is a collection of information gathered by a tool intended /// for presentation to an end user, and which describes a group of related /// characteristics in the inputs given to the tool. Typically, but not always, /// a characteristic is a deficiency. An example of a characteristic that is /// _not_ a deficiency is the `reveal_type` diagnostic for our type checker. #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] pub struct Diagnostic { /// The actual diagnostic. /// /// We box the diagnostic since it is somewhat big. inner: Arc, } impl Diagnostic { /// Create a new diagnostic with the given identifier, severity and /// message. /// /// The identifier should be something that uniquely identifies the _type_ /// of diagnostic being reported. It should be usable as a reference point /// for humans communicating about diagnostic categories. It will also /// appear in the output when this diagnostic is rendered. /// /// The severity should describe the assumed level of importance to an end /// user. /// /// The message is meant to be read by end users. The primary message /// is meant to be a single terse description (usually a short phrase) /// describing the group of related characteristics that the diagnostic /// describes. Stated differently, if only one thing from a diagnostic can /// be shown to an end user in a particular context, it is the primary /// message. /// /// # Types implementing `IntoDiagnosticMessage` /// /// Callers can pass anything that implements `std::fmt::Display` /// directly. If callers want or need to avoid cloning the diagnostic /// message, then they can also pass a `DiagnosticMessage` directly. pub fn new<'a>( id: DiagnosticId, severity: Severity, message: impl IntoDiagnosticMessage + 'a, ) -> Diagnostic { let inner = Arc::new(DiagnosticInner { id, severity, message: message.into_diagnostic_message(), annotations: vec![], subs: vec![], fix: None, parent: None, noqa_offset: None, secondary_code: None, }); Diagnostic { inner } } /// Creates a `Diagnostic` for a syntax error. /// /// Unlike the more general [`Diagnostic::new`], this requires a [`Span`] and a [`TextRange`] /// attached to it. /// /// This should _probably_ be a method on the syntax errors, but /// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of /// the other way around. And since we want to do this conversion in a couple /// places, it makes sense to centralize it _somewhere_. So it's here for now. /// /// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic /// message. pub fn invalid_syntax( span: impl Into, message: impl IntoDiagnosticMessage, range: impl Ranged, ) -> Diagnostic { let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, ""); let span = span.into().with_range(range.range()); diag.annotate(Annotation::primary(span).message(message)); diag } /// Add an annotation to this diagnostic. /// /// Annotations for a diagnostic are optional, but if any are added, /// callers should strive to make at least one of them primary. That is, it /// should be constructed via [`Annotation::primary`]. A diagnostic with no /// primary annotations is allowed, but its rendering may be sub-optimal. pub fn annotate(&mut self, ann: Annotation) { Arc::make_mut(&mut self.inner).annotations.push(ann); } /// Adds an "info" sub-diagnostic with the given message. /// /// If callers want to add an "info" sub-diagnostic with annotations, then /// create a [`SubDiagnostic`] manually and use [`Diagnostic::sub`] to /// attach it to a parent diagnostic. /// /// An "info" diagnostic is useful when contextualizing or otherwise /// helpful information can be added to help end users understand the /// main diagnostic message better. For example, if a the main diagnostic /// message is about a function call being invalid, a useful "info" /// sub-diagnostic could show the function definition (or only the relevant /// parts of it). /// /// # Types implementing `IntoDiagnosticMessage` /// /// Callers can pass anything that implements `std::fmt::Display` /// directly. If callers want or need to avoid cloning the diagnostic /// message, then they can also pass a `DiagnosticMessage` directly. pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) { self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message)); } /// Adds a "help" sub-diagnostic with the given message. /// /// See the closely related [`Diagnostic::info`] method for more details. pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) { self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message)); } /// Adds a "sub" diagnostic to this diagnostic. /// /// This is useful when a sub diagnostic has its own annotations attached /// to it. For the simpler case of a sub-diagnostic with only a message, /// using a method like [`Diagnostic::info`] may be more convenient. pub fn sub(&mut self, sub: SubDiagnostic) { Arc::make_mut(&mut self.inner).subs.push(sub); } /// Return a `std::fmt::Display` implementation that renders this /// diagnostic into a human readable format. /// /// Note that this `Display` impl includes a trailing line terminator, so /// callers should prefer using this with `write!` instead of `writeln!`. pub fn display<'a>( &'a self, resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig, ) -> DisplayDiagnostic<'a> { DisplayDiagnostic::new(resolver, config, self) } /// Returns the identifier for this diagnostic. pub fn id(&self) -> DiagnosticId { self.inner.id } /// Returns the primary message for this diagnostic. /// /// A diagnostic always has a message, but it may be empty. /// /// NOTE: At present, this routine will return the first primary /// annotation's message as the primary message when the main diagnostic /// message is empty. This is meant to facilitate an incremental migration /// in ty over to the new diagnostic data model. (The old data model /// didn't distinguish between messages on the entire diagnostic and /// messages attached to a particular span.) pub fn primary_message(&self) -> &str { if !self.inner.message.as_str().is_empty() { return self.inner.message.as_str(); } // FIXME: As a special case, while we're migrating ty // to the new diagnostic data model, we'll look for a primary // message from the primary annotation. This is because most // ty diagnostics are created with an empty diagnostic // message and instead attach the message to the annotation. // Fixing this will require touching basically every diagnostic // in ty, so we do it this way for now to match the old // semantics. ---AG self.primary_annotation() .and_then(|ann| ann.get_message()) .unwrap_or_default() } /// Introspects this diagnostic and returns what kind of "primary" message /// it contains for concise formatting. /// /// When we concisely format diagnostics, we likely want to not only /// include the primary diagnostic message but also the message attached /// to the primary annotation. In particular, the primary annotation often /// contains *essential* information or context for understanding the /// diagnostic. /// /// The reason why we don't just always return both the main diagnostic /// message and the primary annotation message is because this was written /// in the midst of an incremental migration of ty over to the new /// diagnostic data model. At time of writing, diagnostics were still /// constructed in the old model where the main diagnostic message and the /// primary annotation message were not distinguished from each other. So /// for now, we carefully return what kind of messages this diagnostic /// contains. In effect, if this diagnostic has a non-empty main message /// *and* a non-empty primary annotation message, then the diagnostic is /// 100% using the new diagnostic data model and we can format things /// appropriately. /// /// The type returned implements the `std::fmt::Display` trait. In most /// cases, just converting it to a string (or printing it) will do what /// you want. pub fn concise_message(&self) -> ConciseMessage<'_> { let main = self.inner.message.as_str(); let annotation = self .primary_annotation() .and_then(|ann| ann.get_message()) .unwrap_or_default(); match (main.is_empty(), annotation.is_empty()) { (false, true) => ConciseMessage::MainDiagnostic(main), (true, false) => ConciseMessage::PrimaryAnnotation(annotation), (false, false) => ConciseMessage::Both { main, annotation }, (true, true) => ConciseMessage::Empty, } } /// Returns the severity of this diagnostic. /// /// Note that this may be different than the severity of sub-diagnostics. pub fn severity(&self) -> Severity { self.inner.severity } /// Returns a shared borrow of the "primary" annotation of this diagnostic /// if one exists. /// /// When there are multiple primary annotations, then the first one that /// was added to this diagnostic is returned. pub fn primary_annotation(&self) -> Option<&Annotation> { self.inner.annotations.iter().find(|ann| ann.is_primary) } /// Returns a mutable borrow of the "primary" annotation of this diagnostic /// if one exists. /// /// When there are multiple primary annotations, then the first one that /// was added to this diagnostic is returned. pub fn primary_annotation_mut(&mut self) -> Option<&mut Annotation> { Arc::make_mut(&mut self.inner) .annotations .iter_mut() .find(|ann| ann.is_primary) } /// Returns a mutable borrow of all annotations of this diagnostic. pub fn annotations_mut(&mut self) -> impl Iterator { Arc::make_mut(&mut self.inner).annotations.iter_mut() } /// Returns the "primary" span of this diagnostic if one exists. /// /// When there are multiple primary spans, then the first one that was /// added to this diagnostic is returned. pub fn primary_span(&self) -> Option { self.primary_annotation().map(|ann| ann.span.clone()) } /// Returns a reference to the primary span of this diagnostic. pub fn primary_span_ref(&self) -> Option<&Span> { self.primary_annotation().map(|ann| &ann.span) } /// Returns the tags from the primary annotation of this diagnostic if it exists. pub fn primary_tags(&self) -> Option<&[DiagnosticTag]> { self.primary_annotation().map(|ann| ann.tags.as_slice()) } /// Returns the "primary" span of this diagnostic, panicking if it does not exist. /// /// This should typically only be used when working with diagnostics in Ruff, where diagnostics /// are currently required to have a primary span. /// /// See [`Diagnostic::primary_span`] for more details. pub fn expect_primary_span(&self) -> Span { self.primary_span().expect("Expected a primary span") } /// Returns a key that can be used to sort two diagnostics into the canonical order /// in which they should appear when rendered. pub fn rendering_sort_key<'a>(&'a self, db: &'a dyn Db) -> impl Ord + 'a { RenderingSortKey { db, diagnostic: self, } } /// Returns all annotations, skipping the first primary annotation. pub fn secondary_annotations(&self) -> impl Iterator { let mut seen_primary = false; self.inner.annotations.iter().filter(move |ann| { if seen_primary { true } else if ann.is_primary { seen_primary = true; false } else { true } }) } pub fn sub_diagnostics(&self) -> &[SubDiagnostic] { &self.inner.subs } /// Returns a mutable borrow of the sub-diagnostics of this diagnostic. pub fn sub_diagnostics_mut(&mut self) -> impl Iterator { Arc::make_mut(&mut self.inner).subs.iter_mut() } /// Returns the fix for this diagnostic if it exists. pub fn fix(&self) -> Option<&Fix> { self.inner.fix.as_ref() } #[cfg(test)] pub(crate) fn fix_mut(&mut self) -> Option<&mut Fix> { Arc::make_mut(&mut self.inner).fix.as_mut() } /// Set the fix for this diagnostic. pub fn set_fix(&mut self, fix: Fix) { debug_assert!( self.primary_span().is_some(), "Expected a source file for a diagnostic with a fix" ); Arc::make_mut(&mut self.inner).fix = Some(fix); } /// Remove the fix for this diagnostic. pub fn remove_fix(&mut self) { Arc::make_mut(&mut self.inner).fix = None; } /// Returns `true` if the diagnostic contains a [`Fix`]. pub fn fixable(&self) -> bool { self.fix().is_some() } /// Returns `true` if the diagnostic is [`fixable`](Diagnostic::fixable) and applies at the /// configured applicability level. pub fn has_applicable_fix(&self, config: &DisplayDiagnosticConfig) -> bool { self.fix() .is_some_and(|fix| fix.applies(config.fix_applicability)) } /// Returns the offset of the parent statement for this diagnostic if it exists. /// /// This is primarily used for checking noqa/secondary code suppressions. pub fn parent(&self) -> Option { self.inner.parent } /// Set the offset of the diagnostic's parent statement. pub fn set_parent(&mut self, parent: TextSize) { Arc::make_mut(&mut self.inner).parent = Some(parent); } /// Returns the remapped offset for a suppression comment if it exists. /// /// Like [`Diagnostic::parent`], this is used for noqa code suppression comments in Ruff. pub fn noqa_offset(&self) -> Option { self.inner.noqa_offset } /// Set the remapped offset for a suppression comment. pub fn set_noqa_offset(&mut self, noqa_offset: TextSize) { Arc::make_mut(&mut self.inner).noqa_offset = Some(noqa_offset); } /// Returns the secondary code for the diagnostic if it exists. /// /// The "primary" code for the diagnostic is its lint name. Diagnostics in ty don't have /// secondary codes (yet), but in Ruff the noqa code is used. pub fn secondary_code(&self) -> Option<&SecondaryCode> { self.inner.secondary_code.as_ref() } /// Returns the secondary code for the diagnostic if it exists, or the lint name otherwise. /// /// This is a common pattern for Ruff diagnostics, which want to use the noqa code in general, /// but fall back on the `invalid-syntax` identifier for syntax errors, which don't have /// secondary codes. pub fn secondary_code_or_id(&self) -> &str { self.secondary_code() .map_or_else(|| self.inner.id.as_str(), SecondaryCode::as_str) } /// Set the secondary code for this diagnostic. pub fn set_secondary_code(&mut self, code: SecondaryCode) { Arc::make_mut(&mut self.inner).secondary_code = Some(code); } /// Returns the name used to represent the diagnostic. pub fn name(&self) -> &'static str { self.id().as_str() } /// Returns `true` if `self` is a syntax error message. pub fn is_invalid_syntax(&self) -> bool { self.id().is_invalid_syntax() } /// Returns the message body to display to the user. pub fn body(&self) -> &str { self.primary_message() } /// Returns the message of the first sub-diagnostic with a `Help` severity. /// /// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in /// general this is not the guaranteed meaning of such a message. pub fn first_help_text(&self) -> Option<&str> { self.sub_diagnostics() .iter() .find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help)) .map(|sub| sub.inner.message.as_str()) } /// Returns the URL for the rule documentation, if it exists. pub fn to_ruff_url(&self) -> Option { if self.is_invalid_syntax() { None } else { Some(format!( "{}/rules/{}", env!("CARGO_PKG_HOMEPAGE"), self.name() )) } } /// Returns the filename for the message. /// /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. pub fn expect_ruff_filename(&self) -> String { self.expect_primary_span() .expect_ruff_file() .name() .to_string() } /// Computes the start source location for the message. /// /// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the /// span has no range. pub fn expect_ruff_start_location(&self) -> LineColumn { self.expect_primary_span() .expect_ruff_file() .to_source_code() .line_column(self.expect_range().start()) } /// Computes the end source location for the message. /// /// Panics if the diagnostic has no primary span, if its file is not a `SourceFile`, or if the /// span has no range. pub fn expect_ruff_end_location(&self) -> LineColumn { self.expect_primary_span() .expect_ruff_file() .to_source_code() .line_column(self.expect_range().end()) } /// Returns the [`SourceFile`] which the message belongs to. pub fn ruff_source_file(&self) -> Option<&SourceFile> { self.primary_span_ref()?.as_ruff_file() } /// Returns the [`SourceFile`] which the message belongs to. /// /// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`. pub fn expect_ruff_source_file(&self) -> &SourceFile { self.ruff_source_file() .expect("Expected a ruff source file") } /// Returns the [`TextRange`] for the diagnostic. pub fn range(&self) -> Option { self.primary_span()?.range() } /// Returns the [`TextRange`] for the diagnostic. /// /// Panics if the diagnostic has no primary span or if the span has no range. pub fn expect_range(&self) -> TextRange { self.range().expect("Expected a range for the primary span") } /// Returns the ordering of diagnostics based on the start of their ranges, if they have any. /// /// Panics if either diagnostic has no primary span, if the span has no range, or if its file is /// not a `SourceFile`. pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering { (self.expect_ruff_source_file(), self.expect_range().start()).cmp(&( other.expect_ruff_source_file(), other.expect_range().start(), )) } } #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] struct DiagnosticInner { id: DiagnosticId, severity: Severity, message: DiagnosticMessage, annotations: Vec, subs: Vec, fix: Option, parent: Option, noqa_offset: Option, secondary_code: Option, } struct RenderingSortKey<'a> { db: &'a dyn Db, diagnostic: &'a Diagnostic, } impl Ord for RenderingSortKey<'_> { // We sort diagnostics in a way that keeps them in source order // and grouped by file. After that, we fall back to severity // (with fatal messages sorting before info messages) and then // finally the diagnostic ID. fn cmp(&self, other: &Self) -> std::cmp::Ordering { if let (Some(span1), Some(span2)) = ( self.diagnostic.primary_span(), other.diagnostic.primary_span(), ) { let order = span1.file().path(&self.db).cmp(span2.file().path(&self.db)); if order.is_ne() { return order; } if let (Some(range1), Some(range2)) = (span1.range(), span2.range()) { let order = range1.start().cmp(&range2.start()); if order.is_ne() { return order; } } } // Reverse so that, e.g., Fatal sorts before Info. let order = self .diagnostic .severity() .cmp(&other.diagnostic.severity()) .reverse(); if order.is_ne() { return order; } self.diagnostic.id().cmp(&other.diagnostic.id()) } } impl PartialOrd for RenderingSortKey<'_> { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for RenderingSortKey<'_> { fn eq(&self, other: &Self) -> bool { self.cmp(other).is_eq() } } impl Eq for RenderingSortKey<'_> {} /// A collection of information subservient to a diagnostic. /// /// A sub-diagnostic is always rendered after the parent diagnostic it is /// attached to. A parent diagnostic may have many sub-diagnostics, and it is /// guaranteed that they will not interleave with one another in rendering. /// /// Currently, the order in which sub-diagnostics are rendered relative to one /// another (for a single parent diagnostic) is the order in which they were /// attached to the diagnostic. #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] pub struct SubDiagnostic { /// Like with `Diagnostic`, we box the `SubDiagnostic` to make it /// pointer-sized. inner: Box, } impl SubDiagnostic { /// Create a new sub-diagnostic with the given severity and message. /// /// The severity should describe the assumed level of importance to an end /// user. /// /// The message is meant to be read by end users. The primary message /// is meant to be a single terse description (usually a short phrase) /// describing the group of related characteristics that the sub-diagnostic /// describes. Stated differently, if only one thing from a diagnostic can /// be shown to an end user in a particular context, it is the primary /// message. /// /// # Types implementing `IntoDiagnosticMessage` /// /// Callers can pass anything that implements `std::fmt::Display` /// directly. If callers want or need to avoid cloning the diagnostic /// message, then they can also pass a `DiagnosticMessage` directly. pub fn new<'a>( severity: SubDiagnosticSeverity, message: impl IntoDiagnosticMessage + 'a, ) -> SubDiagnostic { let inner = Box::new(SubDiagnosticInner { severity, message: message.into_diagnostic_message(), annotations: vec![], }); SubDiagnostic { inner } } /// Add an annotation to this sub-diagnostic. /// /// Annotations for a sub-diagnostic, like for a diagnostic, are optional. /// If any are added, callers should strive to make at least one of them /// primary. That is, it should be constructed via [`Annotation::primary`]. /// A diagnostic with no primary annotations is allowed, but its rendering /// may be sub-optimal. /// /// Note that it is expected to be somewhat more common for sub-diagnostics /// to have no annotations (e.g., a simple note) than for a diagnostic to /// have no annotations. pub fn annotate(&mut self, ann: Annotation) { self.inner.annotations.push(ann); } pub fn annotations(&self) -> &[Annotation] { &self.inner.annotations } /// Returns a mutable borrow of the annotations of this sub-diagnostic. pub fn annotations_mut(&mut self) -> impl Iterator { self.inner.annotations.iter_mut() } /// Returns a shared borrow of the "primary" annotation of this diagnostic /// if one exists. /// /// When there are multiple primary annotations, then the first one that /// was added to this diagnostic is returned. pub fn primary_annotation(&self) -> Option<&Annotation> { self.inner.annotations.iter().find(|ann| ann.is_primary) } /// Introspects this diagnostic and returns what kind of "primary" message /// it contains for concise formatting. /// /// When we concisely format diagnostics, we likely want to not only /// include the primary diagnostic message but also the message attached /// to the primary annotation. In particular, the primary annotation often /// contains *essential* information or context for understanding the /// diagnostic. /// /// The reason why we don't just always return both the main diagnostic /// message and the primary annotation message is because this was written /// in the midst of an incremental migration of ty over to the new /// diagnostic data model. At time of writing, diagnostics were still /// constructed in the old model where the main diagnostic message and the /// primary annotation message were not distinguished from each other. So /// for now, we carefully return what kind of messages this diagnostic /// contains. In effect, if this diagnostic has a non-empty main message /// *and* a non-empty primary annotation message, then the diagnostic is /// 100% using the new diagnostic data model and we can format things /// appropriately. /// /// The type returned implements the `std::fmt::Display` trait. In most /// cases, just converting it to a string (or printing it) will do what /// you want. pub fn concise_message(&self) -> ConciseMessage<'_> { let main = self.inner.message.as_str(); let annotation = self .primary_annotation() .and_then(|ann| ann.get_message()) .unwrap_or_default(); match (main.is_empty(), annotation.is_empty()) { (false, true) => ConciseMessage::MainDiagnostic(main), (true, false) => ConciseMessage::PrimaryAnnotation(annotation), (false, false) => ConciseMessage::Both { main, annotation }, (true, true) => ConciseMessage::Empty, } } } #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] struct SubDiagnosticInner { severity: SubDiagnosticSeverity, message: DiagnosticMessage, annotations: Vec, } /// A pointer to a subsequence in the end user's input. /// /// Also known as an annotation, the pointer can optionally contain a short /// message, typically describing in general terms what is being pointed to. /// /// An annotation is either primary or secondary, depending on whether it was /// constructed via [`Annotation::primary`] or [`Annotation::secondary`]. /// Semantically, a primary annotation is meant to point to the "locus" of a /// diagnostic. Visually, the difference between a primary and a secondary /// annotation is usually just a different form of highlighting on the /// corresponding span. /// /// # Advice /// /// The span on an annotation should be as _specific_ as possible. For example, /// if there is a problem with a function call because one of its arguments has /// an invalid type, then the span should point to the specific argument and /// not to the entire function call. /// /// Messages attached to annotations should also be as brief and specific as /// possible. Long messages could negative impact the quality of rendering. #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] pub struct Annotation { /// The span of this annotation, corresponding to some subsequence of the /// user's input that we want to highlight. span: Span, /// An optional message associated with this annotation's span. /// /// When present, rendering will include this message in the output and /// draw a line between the highlighted span and the message. message: Option, /// Whether this annotation is "primary" or not. When it isn't primary, an /// annotation is said to be "secondary." is_primary: bool, /// The diagnostic tags associated with this annotation. tags: Vec, /// Whether this annotation is a file-level or full-file annotation. /// /// When set, rendering will only include the file's name and (optional) range. Everything else /// is omitted, including any file snippet or message. is_file_level: bool, } impl Annotation { /// Create a "primary" annotation. /// /// A primary annotation is meant to highlight the "locus" of a diagnostic. /// That is, it should point to something in the end user's input that is /// the subject or "point" of a diagnostic. /// /// A diagnostic may have many primary annotations. A diagnostic may not /// have any annotations, but if it does, at least one _ought_ to be /// primary. pub fn primary(span: Span) -> Annotation { Annotation { span, message: None, is_primary: true, tags: Vec::new(), is_file_level: false, } } /// Create a "secondary" annotation. /// /// A secondary annotation is meant to highlight relevant context for a /// diagnostic, but not to point to the "locus" of the diagnostic. /// /// A diagnostic with only secondary annotations is usually not sensible, /// but it is allowed and will produce a reasonable rendering. pub fn secondary(span: Span) -> Annotation { Annotation { span, message: None, is_primary: false, tags: Vec::new(), is_file_level: false, } } /// Attach a message to this annotation. /// /// An annotation without a message will still have a presence in /// rendering. In particular, it will highlight the span association with /// this annotation in some way. /// /// When a message is attached to an annotation, then it will be associated /// with the highlighted span in some way during rendering. /// /// # Types implementing `IntoDiagnosticMessage` /// /// Callers can pass anything that implements `std::fmt::Display` /// directly. If callers want or need to avoid cloning the diagnostic /// message, then they can also pass a `DiagnosticMessage` directly. pub fn message<'a>(self, message: impl IntoDiagnosticMessage + 'a) -> Annotation { let message = Some(message.into_diagnostic_message()); Annotation { message, ..self } } /// Sets the message on this annotation. /// /// If one was already set, then this overwrites it. /// /// This is useful if one needs to set the message on an annotation, /// and all one has is a `&mut Annotation`. For example, via /// `Diagnostic::primary_annotation_mut`. pub fn set_message<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) { self.message = Some(message.into_diagnostic_message()); } /// Returns the message attached to this annotation, if one exists. pub fn get_message(&self) -> Option<&str> { self.message.as_ref().map(|m| m.as_str()) } /// Returns the `Span` associated with this annotation. pub fn get_span(&self) -> &Span { &self.span } /// Sets the span on this annotation. pub fn set_span(&mut self, span: Span) { self.span = span; } /// Returns the tags associated with this annotation. pub fn get_tags(&self) -> &[DiagnosticTag] { &self.tags } /// Attaches this tag to this annotation. /// /// It will not replace any existing tags. pub fn tag(mut self, tag: DiagnosticTag) -> Annotation { self.tags.push(tag); self } /// Attaches an additional tag to this annotation. pub fn push_tag(&mut self, tag: DiagnosticTag) { self.tags.push(tag); } /// Set whether or not this annotation is file-level. /// /// File-level annotations are only rendered with their file name and range, if available. This /// is intended for backwards compatibility with Ruff diagnostics, which historically used /// `TextRange::default` to indicate a file-level diagnostic. In the new diagnostic model, a /// [`Span`] with a range of `None` should be used instead, as mentioned in the `Span` /// documentation. /// /// TODO(brent) update this usage in Ruff and remove `is_file_level` entirely. See /// , especially my first comment, for more /// details. pub fn set_file_level(&mut self, yes: bool) { self.is_file_level = yes; } } /// Tags that can be associated with an annotation. /// /// These tags are used to provide additional information about the annotation. /// and are passed through to the language server protocol. #[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)] pub enum DiagnosticTag { /// Unused or unnecessary code. Used for unused parameters, unreachable code, etc. Unnecessary, /// Deprecated or obsolete code. Deprecated, } /// A string identifier for a lint rule. /// /// This string is used in command line and configuration interfaces. The name should always /// be in kebab case, e.g. `no-foo` (all lower case). /// /// Rules use kebab case, e.g. `no-foo`. #[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct LintName(&'static str); impl LintName { pub const fn of(name: &'static str) -> Self { Self(name) } pub const fn as_str(&self) -> &'static str { self.0 } } impl std::ops::Deref for LintName { type Target = str; fn deref(&self) -> &Self::Target { self.0 } } impl std::fmt::Display for LintName { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.0) } } impl PartialEq for LintName { fn eq(&self, other: &str) -> bool { self.0 == other } } impl PartialEq<&str> for LintName { fn eq(&self, other: &&str) -> bool { self.0 == *other } } /// Uniquely identifies the kind of a diagnostic. #[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)] pub enum DiagnosticId { Panic, /// Some I/O operation failed Io, /// Some code contains a syntax error InvalidSyntax, /// A lint violation. /// /// Lints can be suppressed and some lints can be enabled or disabled in the configuration. Lint(LintName), /// A revealed type: Created by `reveal_type(expression)`. RevealedType, /// No rule with the given name exists. UnknownRule, /// A glob pattern doesn't follow the expected syntax. InvalidGlob, /// An `include` glob without any patterns. /// /// ## Why is this bad? /// An `include` glob without any patterns won't match any files. This is probably a mistake and /// either the `include` should be removed or a pattern should be added. /// /// ## Example /// ```toml /// [src] /// include = [] /// ``` /// /// Use instead: /// /// ```toml /// [src] /// include = ["src"] /// ``` /// /// or remove the `include` option. EmptyInclude, /// An override configuration is unnecessary because it applies to all files. /// /// ## Why is this bad? /// An overrides section that applies to all files is probably a mistake and can be rolled-up into the root configuration. /// /// ## Example /// ```toml /// [[overrides]] /// [overrides.rules] /// unused-reference = "ignore" /// ``` /// /// Use instead: /// /// ```toml /// [rules] /// unused-reference = "ignore" /// ``` /// /// or /// /// ```toml /// [[overrides]] /// include = ["test"] /// /// [overrides.rules] /// unused-reference = "ignore" /// ``` UnnecessaryOverridesSection, /// An `overrides` section in the configuration that doesn't contain any overrides. /// /// ## Why is this bad? /// An `overrides` section without any configuration overrides is probably a mistake. /// It is either a leftover after removing overrides, or a user forgot to add any overrides, /// or used an incorrect syntax to do so (e.g. used `rules` instead of `overrides.rules`). /// /// ## Example /// ```toml /// [[overrides]] /// include = ["test"] /// # no `[overrides.rules]` /// ``` UselessOverridesSection, /// Use of a deprecated setting. DeprecatedSetting, } impl DiagnosticId { /// Creates a new `DiagnosticId` for a lint with the given name. pub const fn lint(name: &'static str) -> Self { Self::Lint(LintName::of(name)) } /// Returns `true` if this `DiagnosticId` represents a lint. pub fn is_lint(&self) -> bool { matches!(self, DiagnosticId::Lint(_)) } /// Returns `true` if this `DiagnosticId` represents a lint with the given name. pub fn is_lint_named(&self, name: &str) -> bool { matches!(self, DiagnosticId::Lint(self_name) if self_name == name) } pub fn strip_category(code: &str) -> Option<&str> { code.split_once(':').map(|(_, rest)| rest) } /// Returns a concise description of this diagnostic ID. /// /// Note that this doesn't include the lint's category. It /// only includes the lint's name. pub fn as_str(&self) -> &'static str { match self { DiagnosticId::Panic => "panic", DiagnosticId::Io => "io", DiagnosticId::InvalidSyntax => "invalid-syntax", DiagnosticId::Lint(name) => name.as_str(), DiagnosticId::RevealedType => "revealed-type", DiagnosticId::UnknownRule => "unknown-rule", DiagnosticId::InvalidGlob => "invalid-glob", DiagnosticId::EmptyInclude => "empty-include", DiagnosticId::UnnecessaryOverridesSection => "unnecessary-overrides-section", DiagnosticId::UselessOverridesSection => "useless-overrides-section", DiagnosticId::DeprecatedSetting => "deprecated-setting", } } pub fn is_invalid_syntax(&self) -> bool { matches!(self, Self::InvalidSyntax) } } impl std::fmt::Display for DiagnosticId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } /// A unified file representation for both ruff and ty. /// /// Such a representation is needed for rendering [`Diagnostic`]s that can optionally contain /// [`Annotation`]s with [`Span`]s that need to refer to the text of a file. However, ty and ruff /// use very different file types: a `Copy`-able salsa-interned [`File`], and a heavier-weight /// [`SourceFile`], respectively. /// /// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and /// emitting diagnostics from both ty and ruff. #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub enum UnifiedFile { Ty(File), Ruff(SourceFile), } impl UnifiedFile { pub fn path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a str { match self { UnifiedFile::Ty(file) => resolver.path(*file), UnifiedFile::Ruff(file) => file.name(), } } /// Return the file's path relative to the current working directory. pub fn relative_path<'a>(&'a self, resolver: &'a dyn FileResolver) -> &'a Path { let cwd = resolver.current_directory(); let path = Path::new(self.path(resolver)); if let Ok(path) = path.strip_prefix(cwd) { return path; } path } fn diagnostic_source(&self, resolver: &dyn FileResolver) -> DiagnosticSource { match self { UnifiedFile::Ty(file) => DiagnosticSource::Ty(resolver.input(*file)), UnifiedFile::Ruff(file) => DiagnosticSource::Ruff(file.clone()), } } } /// A unified wrapper for types that can be converted to a [`SourceCode`]. /// /// As with [`UnifiedFile`], ruff and ty use slightly different representations for source code. /// [`DiagnosticSource`] wraps both of these and provides the single /// [`DiagnosticSource::as_source_code`] method to produce a [`SourceCode`] with the appropriate /// lifetimes. /// /// See [`UnifiedFile::diagnostic_source`] for a way to obtain a [`DiagnosticSource`] from a file /// and [`FileResolver`]. #[derive(Clone, Debug)] enum DiagnosticSource { Ty(Input), Ruff(SourceFile), } impl DiagnosticSource { /// Returns this input as a `SourceCode` for convenient querying. fn as_source_code(&self) -> SourceCode<'_, '_> { match self { DiagnosticSource::Ty(input) => SourceCode::new(input.text.as_str(), &input.line_index), DiagnosticSource::Ruff(source) => SourceCode::new(source.source_text(), source.index()), } } } /// A span represents the source of a diagnostic. /// /// It consists of a `File` and an optional range into that file. When the /// range isn't present, it semantically implies that the diagnostic refers to /// the entire file. For example, when the file should be executable but isn't. #[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)] pub struct Span { file: UnifiedFile, range: Option, } impl Span { /// Returns the `UnifiedFile` attached to this `Span`. pub fn file(&self) -> &UnifiedFile { &self.file } /// Returns the range, if available, attached to this `Span`. /// /// When there is no range, it is convention to assume that this `Span` /// refers to the corresponding `File` as a whole. In some cases, consumers /// of this API may use the range `0..0` to represent this case. pub fn range(&self) -> Option { self.range } /// Returns a new `Span` with the given `range` attached to it. pub fn with_range(self, range: TextRange) -> Span { self.with_optional_range(Some(range)) } /// Returns a new `Span` with the given optional `range` attached to it. pub fn with_optional_range(self, range: Option) -> Span { Span { range, ..self } } /// Returns the [`File`] attached to this [`Span`]. /// /// Panics if the file is a [`UnifiedFile::Ruff`] instead of a [`UnifiedFile::Ty`]. pub fn expect_ty_file(&self) -> File { match self.file { UnifiedFile::Ty(file) => file, UnifiedFile::Ruff(_) => panic!("Expected a ty `File`, found a ruff `SourceFile`"), } } /// Returns the [`SourceFile`] attached to this [`Span`]. /// /// Panics if the file is a [`UnifiedFile::Ty`] instead of a [`UnifiedFile::Ruff`]. pub fn expect_ruff_file(&self) -> &SourceFile { self.as_ruff_file() .expect("Expected a ruff `SourceFile`, found a ty `File`") } /// Returns the [`SourceFile`] attached to this [`Span`]. pub fn as_ruff_file(&self) -> Option<&SourceFile> { match &self.file { UnifiedFile::Ty(_) => None, UnifiedFile::Ruff(file) => Some(file), } } } impl From for Span { fn from(file: File) -> Span { let file = UnifiedFile::Ty(file); Span { file, range: None } } } impl From for Span { fn from(file: SourceFile) -> Self { let file = UnifiedFile::Ruff(file); Span { file, range: None } } } impl From for Span { fn from(file_range: crate::files::FileRange) -> Span { Span::from(file_range.file()).with_range(file_range.range()) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)] pub enum Severity { Info, Warning, Error, Fatal, } impl Severity { fn to_annotate(self) -> AnnotateLevel { match self { Severity::Info => AnnotateLevel::Info, Severity::Warning => AnnotateLevel::Warning, Severity::Error => AnnotateLevel::Error, // NOTE: Should we really collapse this to "error"? // // After collapsing this, the snapshot tests seem to reveal that we // don't currently have any *tests* with a `fatal` severity level. // And maybe *rendering* this as just an `error` is fine. If we // really do need different rendering, then I think we can add a // `Level::Fatal`. ---AG Severity::Fatal => AnnotateLevel::Error, } } pub const fn is_fatal(self) -> bool { matches!(self, Severity::Fatal) } } /// Like [`Severity`] but exclusively for sub-diagnostics. /// /// This type only exists to add an additional `Help` severity that isn't present in `Severity` or /// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be /// deleted and the two combined again. #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)] pub enum SubDiagnosticSeverity { Help, Info, Warning, Error, Fatal, } impl SubDiagnosticSeverity { fn to_annotate(self) -> AnnotateLevel { match self { SubDiagnosticSeverity::Help => AnnotateLevel::Help, SubDiagnosticSeverity::Info => AnnotateLevel::Info, SubDiagnosticSeverity::Warning => AnnotateLevel::Warning, SubDiagnosticSeverity::Error => AnnotateLevel::Error, SubDiagnosticSeverity::Fatal => AnnotateLevel::Error, } } } /// Configuration for rendering diagnostics. #[derive(Clone, Debug)] pub struct DisplayDiagnosticConfig { /// The format to use for diagnostic rendering. /// /// This uses the "full" format by default. format: DiagnosticFormat, /// Whether to enable colors or not. /// /// Disabled by default. color: bool, /// The number of non-empty lines to show around each snippet. /// /// NOTE: It seems like making this a property of rendering *could* /// be wrong. In particular, I have a suspicion that we may want /// more granular control over this, perhaps based on the kind of /// diagnostic or even the snippet itself. But I chose to put this /// here for now as the most "sensible" place for it to live until /// we had more concrete use cases. ---AG context: usize, /// Whether to use preview formatting for Ruff diagnostics. #[allow( dead_code, reason = "This is currently only used for JSON but will be needed soon for other formats" )] preview: bool, /// Whether to hide the real `Severity` of diagnostics. /// /// This is intended for temporary use by Ruff, which only has a single `error` severity at the /// moment. We should be able to remove this option when Ruff gets more severities. hide_severity: bool, /// Whether to show the availability of a fix in a diagnostic. show_fix_status: bool, /// Whether to show the diff for an available fix after the main diagnostic. /// /// This currently only applies to `DiagnosticFormat::Full`. show_fix_diff: bool, /// The lowest applicability that should be shown when reporting diagnostics. fix_applicability: Applicability, } impl DisplayDiagnosticConfig { /// Whether to enable concise diagnostic output or not. pub fn format(self, format: DiagnosticFormat) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { format, ..self } } /// Whether to enable colors or not. pub fn color(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { color: yes, ..self } } /// Set the number of contextual lines to show around each snippet. pub fn context(self, lines: usize) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { context: lines, ..self } } /// Whether to enable preview behavior or not. pub fn preview(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { preview: yes, ..self } } /// Whether to hide a diagnostic's severity or not. pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { hide_severity: yes, ..self } } /// Whether to show a fix's availability or not. pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { show_fix_status: yes, ..self } } /// Whether to show a diff for an available fix after the main diagnostic. pub fn show_fix_diff(self, yes: bool) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { show_fix_diff: yes, ..self } } /// Set the lowest fix applicability that should be shown. /// /// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix /// availability for unsafe or display-only fixes. /// /// Note that this option is currently ignored when `hide_severity` is false. pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { fix_applicability: applicability, ..self } } } impl Default for DisplayDiagnosticConfig { fn default() -> DisplayDiagnosticConfig { DisplayDiagnosticConfig { format: DiagnosticFormat::default(), color: false, context: 2, preview: false, hide_severity: false, show_fix_status: false, show_fix_diff: false, fix_applicability: Applicability::Safe, } } } /// The diagnostic output format. #[derive(Copy, Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub enum DiagnosticFormat { /// The default full mode will print "pretty" diagnostics. /// /// That is, color will be used when printing to a `tty`. /// Moreover, diagnostic messages may include additional /// context and annotations on the input to help understand /// the message. #[default] Full, /// Print diagnostics in a concise mode. /// /// This will guarantee that each diagnostic is printed on /// a single line. Only the most important or primary aspects /// of the diagnostic are included. Contextual information is /// dropped. /// /// This may use color when printing to a `tty`. Concise, /// Print diagnostics in the [Azure Pipelines] format. /// /// [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 Azure, /// Print diagnostics in JSON format. /// /// Unlike `json-lines`, this prints all of the diagnostics as a JSON array. #[cfg(feature = "serde")] Json, /// Print diagnostics in JSON format, one per line. /// /// This will print each diagnostic as a separate JSON object on its own line. See the `json` /// format for an array of all diagnostics. See for more details. #[cfg(feature = "serde")] JsonLines, /// Print diagnostics in the JSON format expected by [reviewdog]. /// /// [reviewdog]: https://github.com/reviewdog/reviewdog #[cfg(feature = "serde")] Rdjson, /// Print diagnostics in the format emitted by Pylint. Pylint, /// Print diagnostics in the format expected by JUnit. #[cfg(feature = "junit")] Junit, /// Print diagnostics in the JSON format used by GitLab [Code Quality] reports. /// /// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format #[cfg(feature = "serde")] Gitlab, } /// A representation of the kinds of messages inside a diagnostic. pub enum ConciseMessage<'a> { /// A diagnostic contains a non-empty main message and an empty /// primary annotation message. /// /// This strongly suggests that the diagnostic is using the /// "new" data model. MainDiagnostic(&'a str), /// A diagnostic contains an empty main message and a non-empty /// primary annotation message. /// /// This strongly suggests that the diagnostic is using the /// "old" data model. PrimaryAnnotation(&'a str), /// A diagnostic contains a non-empty main message and a non-empty /// primary annotation message. /// /// This strongly suggests that the diagnostic is using the /// "new" data model. Both { main: &'a str, annotation: &'a str }, /// A diagnostic contains an empty main message and an empty /// primary annotation message. /// /// This indicates that the diagnostic is probably using the old /// model. Empty, } impl std::fmt::Display for ConciseMessage<'_> { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match *self { ConciseMessage::MainDiagnostic(main) => { write!(f, "{main}") } ConciseMessage::PrimaryAnnotation(annotation) => { write!(f, "{annotation}") } ConciseMessage::Both { main, annotation } => { write!(f, "{main}: {annotation}") } ConciseMessage::Empty => Ok(()), } } } /// A diagnostic message string. /// /// This is, for all intents and purposes, equivalent to a `Box`. /// But it does not implement `std::fmt::Display`. Indeed, that it its /// entire reason for existence. It provides a way to pass a string /// directly into diagnostic methods that accept messages without copying /// that string. This works via the `IntoDiagnosticMessage` trait. /// /// In most cases, callers shouldn't need to use this. Instead, there is /// a blanket trait implementation for `IntoDiagnosticMessage` for /// anything that implements `std::fmt::Display`. #[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)] pub struct DiagnosticMessage(Box); impl DiagnosticMessage { /// Returns this message as a borrowed string. pub fn as_str(&self) -> &str { &self.0 } } impl From<&str> for DiagnosticMessage { fn from(s: &str) -> DiagnosticMessage { DiagnosticMessage(s.into()) } } impl From for DiagnosticMessage { fn from(s: String) -> DiagnosticMessage { DiagnosticMessage(s.into()) } } impl From> for DiagnosticMessage { fn from(s: Box) -> DiagnosticMessage { DiagnosticMessage(s) } } impl IntoDiagnosticMessage for DiagnosticMessage { fn into_diagnostic_message(self) -> DiagnosticMessage { self } } /// A trait for values that can be converted into a diagnostic message. /// /// Users of the diagnostic API can largely think of this trait as effectively /// equivalent to `std::fmt::Display`. Indeed, everything that implements /// `Display` also implements this trait. That means wherever this trait is /// accepted, you can use things like `format_args!`. /// /// The purpose of this trait is to provide a means to give arguments _other_ /// than `std::fmt::Display` trait implementations. Or rather, to permit /// the diagnostic API to treat them differently. For example, this lets /// callers wrap a string in a `DiagnosticMessage` and provide it directly /// to any of the diagnostic APIs that accept a message. This will move the /// string and avoid any unnecessary copies. (If we instead required only /// `std::fmt::Display`, then this would potentially result in a copy via the /// `ToString` trait implementation.) pub trait IntoDiagnosticMessage { fn into_diagnostic_message(self) -> DiagnosticMessage; } /// Every `IntoDiagnosticMessage` is accepted, so to is `std::fmt::Display`. impl IntoDiagnosticMessage for T { fn into_diagnostic_message(self) -> DiagnosticMessage { DiagnosticMessage::from(self.to_string()) } } /// A secondary identifier for a lint diagnostic. /// /// For Ruff rules this means the noqa code. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)] #[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] pub struct SecondaryCode(String); impl SecondaryCode { pub fn new(code: String) -> Self { Self(code) } pub fn as_str(&self) -> &str { &self.0 } } impl std::fmt::Display for SecondaryCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0) } } impl std::ops::Deref for SecondaryCode { type Target = str; fn deref(&self) -> &Self::Target { &self.0 } } impl PartialEq<&str> for SecondaryCode { fn eq(&self, other: &&str) -> bool { self.0 == *other } } impl PartialEq for &str { fn eq(&self, other: &SecondaryCode) -> bool { other.eq(self) } } // for `hashbrown::EntryRef` impl From<&SecondaryCode> for SecondaryCode { fn from(value: &SecondaryCode) -> Self { value.clone() } }