/*! This module defines the core of the miette protocol: a series of types and traits that you can implement to get access to miette's (and related library's) full reporting and such features. */ use std::{ fmt::{self, Display}, fs, panic::Location, }; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; use crate::MietteError; /// Adds rich metadata to your Error that can be used by /// [`Report`](crate::Report) to print really nice and human-friendly error /// messages. pub trait Diagnostic: std::error::Error { /// Unique diagnostic code that can be used to look up more information /// about this `Diagnostic`. Ideally also globally unique, and documented /// in the toplevel crate's documentation for easy searching. Rust path /// format (`foo::bar::baz`) is recommended, but more classic codes like /// `E0123` or enums will work just fine. fn code<'a>(&'a self) -> Option> { None } /// Diagnostic severity. This may be used by /// [`ReportHandler`](crate::ReportHandler)s to change the display format /// of this diagnostic. /// /// If `None`, reporters should treat this as [`Severity::Error`]. fn severity(&self) -> Option { None } /// Additional help text related to this `Diagnostic`. Do you have any /// advice for the poor soul who's just run into this issue? fn help<'a>(&'a self) -> Option> { None } /// URL to visit for a more detailed explanation/help about this /// `Diagnostic`. fn url<'a>(&'a self) -> Option> { None } /// Source code to apply this `Diagnostic`'s [`Diagnostic::labels`] to. fn source_code(&self) -> Option<&dyn SourceCode> { None } /// Labels to apply to this `Diagnostic`'s [`Diagnostic::source_code`] fn labels(&self) -> Option + '_>> { None } /// Additional related `Diagnostic`s. fn related<'a>(&'a self) -> Option + 'a>> { None } /// The cause of the error. fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { None } } macro_rules! box_impls { ($($box_type:ty),*) => { $( impl std::error::Error for $box_type { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { (**self).source() } fn cause(&self) -> Option<&dyn std::error::Error> { self.source() } } )* } } box_impls! { Box, Box, Box } impl From for Box { fn from(diag: T) -> Self { Box::new(diag) } } impl From for Box { fn from(diag: T) -> Self { Box::::from(diag) } } impl From for Box { fn from(diag: T) -> Self { Box::::from(diag) } } impl From<&str> for Box { fn from(s: &str) -> Self { From::from(String::from(s)) } } impl<'a> From<&str> for Box { fn from(s: &str) -> Self { From::from(String::from(s)) } } impl From for Box { fn from(s: String) -> Self { let err1: Box = From::from(s); let err2: Box = err1; err2 } } impl From for Box { fn from(s: String) -> Self { struct StringError(String); impl std::error::Error for StringError {} impl Diagnostic for StringError {} impl Display for StringError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Display::fmt(&self.0, f) } } // Purposefully skip printing "StringError(..)" impl fmt::Debug for StringError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } Box::new(StringError(s)) } } impl From> for Box { fn from(s: Box) -> Self { #[derive(thiserror::Error)] #[error(transparent)] struct BoxedDiagnostic(Box); impl fmt::Debug for BoxedDiagnostic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) } } impl Diagnostic for BoxedDiagnostic {} Box::new(BoxedDiagnostic(s)) } } /** [`Diagnostic`] severity. Intended to be used by [`ReportHandler`](crate::ReportHandler)s to change the way different [`Diagnostic`]s are displayed. Defaults to [`Severity::Error`]. */ #[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum Severity { /// Just some help. Here's how you could be doing it better. Advice, /// Warning. Please take note. Warning, /// Critical failure. The program cannot continue. /// This is the default severity, if you don't specify another one. Error, } impl Default for Severity { fn default() -> Self { Severity::Error } } #[cfg(feature = "serde")] #[test] fn test_serialize_severity() { use serde_json::json; assert_eq!(json!(Severity::Advice), json!("Advice")); assert_eq!(json!(Severity::Warning), json!("Warning")); assert_eq!(json!(Severity::Error), json!("Error")); } #[cfg(feature = "serde")] #[test] fn test_deserialize_severity() { use serde_json::json; let severity: Severity = serde_json::from_value(json!("Advice")).unwrap(); assert_eq!(severity, Severity::Advice); let severity: Severity = serde_json::from_value(json!("Warning")).unwrap(); assert_eq!(severity, Severity::Warning); let severity: Severity = serde_json::from_value(json!("Error")).unwrap(); assert_eq!(severity, Severity::Error); } /** Represents readable source code of some sort. This trait is able to support simple `SourceCode` types like [`String`]s, as well as more involved types like indexes into centralized `SourceMap`-like types, file handles, and even network streams. If you can read it, you can source it, and it's not necessary to read the whole thing--meaning you should be able to support `SourceCode`s which are gigabytes or larger in size. */ pub trait SourceCode: Send + Sync { /// Read the bytes for a specific span from this SourceCode, keeping a /// certain number of lines before and after the span as context. fn read_span<'a>( &'a self, span: &SourceSpan, context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError>; } /// A labeled [`SourceSpan`]. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct LabeledSpan { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] label: Option, span: SourceSpan, } impl LabeledSpan { /// Makes a new labeled span. pub const fn new(label: Option, offset: ByteOffset, len: usize) -> Self { Self { label, span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)), } } /// Makes a new labeled span using an existing span. pub fn new_with_span(label: Option, span: impl Into) -> Self { Self { label, span: span.into(), } } /// Makes a new label at specified span /// /// # Examples /// ``` /// use miette::LabeledSpan; /// /// let source = "Cpp is the best"; /// let label = LabeledSpan::at(0..3, "should be Rust"); /// assert_eq!( /// label, /// LabeledSpan::new(Some("should be Rust".to_string()), 0, 3) /// ) /// ``` pub fn at(span: impl Into, label: impl Into) -> Self { Self::new_with_span(Some(label.into()), span) } /// Makes a new label that points at a specific offset. /// /// # Examples /// ``` /// use miette::LabeledSpan; /// /// let source = "(2 + 2"; /// let label = LabeledSpan::at_offset(4, "expected a closing parenthesis"); /// assert_eq!( /// label, /// LabeledSpan::new(Some("expected a closing parenthesis".to_string()), 4, 0) /// ) /// ``` pub fn at_offset(offset: ByteOffset, label: impl Into) -> Self { Self::new(Some(label.into()), offset, 0) } /// Makes a new label without text, that underlines a specific span. /// /// # Examples /// ``` /// use miette::LabeledSpan; /// /// let source = "You have an eror here"; /// let label = LabeledSpan::underline(12..16); /// assert_eq!(label, LabeledSpan::new(None, 12, 4)) /// ``` pub fn underline(span: impl Into) -> Self { Self::new_with_span(None, span) } /// Gets the (optional) label string for this `LabeledSpan`. pub fn label(&self) -> Option<&str> { self.label.as_deref() } /// Returns a reference to the inner [`SourceSpan`]. pub const fn inner(&self) -> &SourceSpan { &self.span } /// Returns the 0-based starting byte offset. pub const fn offset(&self) -> usize { self.span.offset() } /// Returns the number of bytes this `LabeledSpan` spans. pub const fn len(&self) -> usize { self.span.len() } /// True if this `LabeledSpan` is empty. pub const fn is_empty(&self) -> bool { self.span.is_empty() } } #[cfg(feature = "serde")] #[test] fn test_serialize_labeled_span() { use serde_json::json; assert_eq!( json!(LabeledSpan::new(None, 0, 0)), json!({ "span": { "offset": 0, "length": 0 } }) ); assert_eq!( json!(LabeledSpan::new(Some("label".to_string()), 0, 0)), json!({ "label": "label", "span": { "offset": 0, "length": 0 } }) ) } #[cfg(feature = "serde")] #[test] fn test_deserialize_labeled_span() { use serde_json::json; let span: LabeledSpan = serde_json::from_value(json!({ "label": null, "span": { "offset": 0, "length": 0 } })) .unwrap(); assert_eq!(span, LabeledSpan::new(None, 0, 0)); let span: LabeledSpan = serde_json::from_value(json!({ "span": { "offset": 0, "length": 0 } })) .unwrap(); assert_eq!(span, LabeledSpan::new(None, 0, 0)); let span: LabeledSpan = serde_json::from_value(json!({ "label": "label", "span": { "offset": 0, "length": 0 } })) .unwrap(); assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0)) } /** Contents of a [`SourceCode`] covered by [`SourceSpan`]. Includes line and column information to optimize highlight calculations. */ pub trait SpanContents<'a> { /// Reference to the data inside the associated span, in bytes. fn data(&self) -> &'a [u8]; /// [`SourceSpan`] representing the span covered by this `SpanContents`. fn span(&self) -> &SourceSpan; /// An optional (file?) name for the container of this `SpanContents`. fn name(&self) -> Option<&str> { None } /// The 0-indexed line in the associated [`SourceCode`] where the data /// begins. fn line(&self) -> usize; /// The 0-indexed column in the associated [`SourceCode`] where the data /// begins, relative to `line`. fn column(&self) -> usize; /// Total number of lines covered by this `SpanContents`. fn line_count(&self) -> usize; } /** Basic implementation of the [`SpanContents`] trait, for convenience. */ #[derive(Clone, Debug)] pub struct MietteSpanContents<'a> { // Data from a [`SourceCode`], in bytes. data: &'a [u8], // span actually covered by this SpanContents. span: SourceSpan, // The 0-indexed line where the associated [`SourceSpan`] _starts_. line: usize, // The 0-indexed column where the associated [`SourceSpan`] _starts_. column: usize, // Number of line in this snippet. line_count: usize, // Optional filename name: Option, } impl<'a> MietteSpanContents<'a> { /// Make a new [`MietteSpanContents`] object. pub const fn new( data: &'a [u8], span: SourceSpan, line: usize, column: usize, line_count: usize, ) -> MietteSpanContents<'a> { MietteSpanContents { data, span, line, column, line_count, name: None, } } /// Make a new [`MietteSpanContents`] object, with a name for its 'file'. pub const fn new_named( name: String, data: &'a [u8], span: SourceSpan, line: usize, column: usize, line_count: usize, ) -> MietteSpanContents<'a> { MietteSpanContents { data, span, line, column, line_count, name: Some(name), } } } impl<'a> SpanContents<'a> for MietteSpanContents<'a> { fn data(&self) -> &'a [u8] { self.data } fn span(&self) -> &SourceSpan { &self.span } fn line(&self) -> usize { self.line } fn column(&self) -> usize { self.column } fn line_count(&self) -> usize { self.line_count } fn name(&self) -> Option<&str> { self.name.as_deref() } } /// Span within a [`SourceCode`] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SourceSpan { /// The start of the span. offset: SourceOffset, /// The total length of the span length: usize, } impl SourceSpan { /// Create a new [`SourceSpan`]. pub const fn new(start: SourceOffset, length: SourceOffset) -> Self { Self { offset: start, length: length.offset(), } } /// The absolute offset, in bytes, from the beginning of a [`SourceCode`]. pub const fn offset(&self) -> usize { self.offset.offset() } /// Total length of the [`SourceSpan`], in bytes. pub const fn len(&self) -> usize { self.length } /// Whether this [`SourceSpan`] has a length of zero. It may still be useful /// to point to a specific point. pub const fn is_empty(&self) -> bool { self.length == 0 } } impl From<(ByteOffset, usize)> for SourceSpan { fn from((start, len): (ByteOffset, usize)) -> Self { Self { offset: start.into(), length: len, } } } impl From<(SourceOffset, SourceOffset)> for SourceSpan { fn from((start, len): (SourceOffset, SourceOffset)) -> Self { Self::new(start, len) } } impl From> for SourceSpan { fn from(range: std::ops::Range) -> Self { Self { offset: range.start.into(), length: range.len(), } } } impl From for SourceSpan { fn from(offset: SourceOffset) -> Self { Self { offset, length: 0 } } } impl From for SourceSpan { fn from(offset: ByteOffset) -> Self { Self { offset: offset.into(), length: 0, } } } #[cfg(feature = "serde")] #[test] fn test_serialize_source_span() { use serde_json::json; assert_eq!( json!(SourceSpan::from(0)), json!({ "offset": 0, "length": 0}) ) } #[cfg(feature = "serde")] #[test] fn test_deserialize_source_span() { use serde_json::json; let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap(); assert_eq!(span, SourceSpan::from(0)) } /** "Raw" type for the byte offset from the beginning of a [`SourceCode`]. */ pub type ByteOffset = usize; /** Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`] */ #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct SourceOffset(ByteOffset); impl SourceOffset { /// Actual byte offset. pub const fn offset(&self) -> ByteOffset { self.0 } /// Little utility to help convert 1-based line/column locations into /// miette-compatible Spans /// /// This function is infallible: Giving an out-of-range line/column pair /// will return the offset of the last byte in the source. pub fn from_location(source: impl AsRef, loc_line: usize, loc_col: usize) -> Self { let mut line = 0usize; let mut col = 0usize; let mut offset = 0usize; for char in source.as_ref().chars() { if line + 1 >= loc_line && col + 1 >= loc_col { break; } if char == '\n' { col = 0; line += 1; } else { col += 1; } offset += char.len_utf8(); } SourceOffset(offset) } /// Returns an offset for the _file_ location of wherever this function is /// called. If you want to get _that_ caller's location, mark this /// function's caller with `#[track_caller]` (and so on and so forth). /// /// Returns both the filename that was given and the offset of the caller /// as a [`SourceOffset`]. /// /// Keep in mind that this fill only work if the file your Rust source /// file was compiled from is actually available at that location. If /// you're shipping binaries for your application, you'll want to ignore /// the Err case or otherwise report it. #[track_caller] pub fn from_current_location() -> Result<(String, Self), MietteError> { let loc = Location::caller(); Ok(( loc.file().into(), fs::read_to_string(loc.file()) .map(|txt| Self::from_location(txt, loc.line() as usize, loc.column() as usize))?, )) } } impl From for SourceOffset { fn from(bytes: ByteOffset) -> Self { SourceOffset(bytes) } } #[test] fn test_source_offset_from_location() { let source = "f\n\noo\r\nbar"; assert_eq!(SourceOffset::from_location(source, 1, 1).offset(), 0); assert_eq!(SourceOffset::from_location(source, 1, 2).offset(), 1); assert_eq!(SourceOffset::from_location(source, 2, 1).offset(), 2); assert_eq!(SourceOffset::from_location(source, 3, 1).offset(), 3); assert_eq!(SourceOffset::from_location(source, 3, 2).offset(), 4); assert_eq!(SourceOffset::from_location(source, 3, 3).offset(), 5); assert_eq!(SourceOffset::from_location(source, 3, 4).offset(), 6); assert_eq!(SourceOffset::from_location(source, 4, 1).offset(), 7); assert_eq!(SourceOffset::from_location(source, 4, 2).offset(), 8); assert_eq!(SourceOffset::from_location(source, 4, 3).offset(), 9); assert_eq!(SourceOffset::from_location(source, 4, 4).offset(), 10); // Out-of-range assert_eq!( SourceOffset::from_location(source, 5, 1).offset(), source.len() ); } #[cfg(feature = "serde")] #[test] fn test_serialize_source_offset() { use serde_json::json; assert_eq!(json!(SourceOffset::from(0)), 0) } #[cfg(feature = "serde")] #[test] fn test_deserialize_source_offset() { let offset: SourceOffset = serde_json::from_str("0").unwrap(); assert_eq!(offset, SourceOffset::from(0)) }