From cc324abcc2d46bbf40e46dcd7525f4c2afffaf99 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 4 Mar 2025 12:40:16 -0500 Subject: [PATCH] ruff_db: add new `Diagnostic` type ... with supporting types. This is meant to give us a base to work with in terms of our new diagnostic data model. I expect the representations to be tweaked over time, but I think this is a decent start. I would also like to add doctest examples, but I think it's better if we wait until an initial version of the renderer is done for that. --- crates/ruff_db/src/diagnostic/mod.rs | 287 +++++++++++++++++++++++++++ 1 file changed, 287 insertions(+) diff --git a/crates/ruff_db/src/diagnostic/mod.rs b/crates/ruff_db/src/diagnostic/mod.rs index d6f6801f77..73bc080caa 100644 --- a/crates/ruff_db/src/diagnostic/mod.rs +++ b/crates/ruff_db/src/diagnostic/mod.rs @@ -14,6 +14,293 @@ use crate::files::File; // the APIs in this module. mod old; +/// 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)] +pub struct Diagnostic { + /// The actual diagnostic. + /// + /// We box the diagnostic since it is somewhat big. + inner: Box, +} + +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. + pub fn new<'a>( + id: DiagnosticId, + severity: Severity, + message: impl std::fmt::Display + 'a, + ) -> Diagnostic { + let message = message.to_string().into_boxed_str(); + let inner = Box::new(DiagnosticInner { + id, + severity, + message, + annotations: vec![], + subs: vec![], + #[cfg(debug_assertions)] + printed: false, + }); + Diagnostic { inner } + } + + /// 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) { + 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). + pub fn info<'a>(&mut self, message: impl std::fmt::Display + 'a) { + self.sub(SubDiagnostic::new(Severity::Info, 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) { + self.inner.subs.push(sub); + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +struct DiagnosticInner { + id: DiagnosticId, + severity: Severity, + message: Box, + annotations: Vec, + subs: Vec, + /// This will make the `Drop` impl panic if a `Diagnostic` hasn't + /// been printed to stderr. This is usually a bug, so we want it to + /// be loud. But only when `debug_assertions` is enabled. + #[cfg(debug_assertions)] + printed: bool, +} + +impl Drop for DiagnosticInner { + fn drop(&mut self) { + #[cfg(debug_assertions)] + { + if self.printed || std::thread::panicking() { + return; + } + panic!( + "diagnostic `{id}` with severity `{severity:?}` and message `{message}` \ + did not get printed to stderr before being dropped", + id = self.id, + severity = self.severity, + message = self.message, + ); + } + } +} + +/// 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)] +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. + pub fn new<'a>(severity: Severity, message: impl std::fmt::Display + 'a) -> SubDiagnostic { + let message = message.to_string().into_boxed_str(); + let inner = Box::new(SubDiagnosticInner { + severity, + message, + annotations: vec![], + #[cfg(debug_assertions)] + printed: false, + }); + 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); + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +struct SubDiagnosticInner { + severity: Severity, + message: Box, + annotations: Vec, + /// This will make the `Drop` impl panic if a `SubDiagnostic` hasn't + /// been printed to stderr. This is usually a bug, so we want it to + /// be loud. But only when `debug_assertions` is enabled. + #[cfg(debug_assertions)] + printed: bool, +} + +impl Drop for SubDiagnosticInner { + fn drop(&mut self) { + #[cfg(debug_assertions)] + { + if self.printed || std::thread::panicking() { + return; + } + panic!( + "sub-diagnostic with severity `{severity:?}` and message `{message}` \ + did not get printed to stderr before being dropped", + severity = self.severity, + message = self.message, + ); + } + } +} + +/// 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)] +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, +} + +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, + } + } + + /// 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, + } + } + + /// 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. + pub fn message<'a>(self, message: impl std::fmt::Display + 'a) -> Annotation { + let message = Some(message.to_string().into_boxed_str()); + Annotation { message, ..self } + } +} + /// A string identifier for a lint rule. /// /// This string is used in command line and configuration interfaces. The name should always