| /*! |
| 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<Box<dyn Display + 'a>> { |
| 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<Severity> { |
| 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<Box<dyn Display + 'a>> { |
| None |
| } |
| |
| /// URL to visit for a more detailed explanation/help about this |
| /// `Diagnostic`. |
| fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> { |
| 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<Box<dyn Iterator<Item = LabeledSpan> + '_>> { |
| None |
| } |
| |
| /// Additional related `Diagnostic`s. |
| fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + '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<dyn Diagnostic>, |
| Box<dyn Diagnostic + Send>, |
| Box<dyn Diagnostic + Send + Sync> |
| } |
| |
| impl<T: Diagnostic + Send + Sync + 'static> From<T> |
| for Box<dyn Diagnostic + Send + Sync + 'static> |
| { |
| fn from(diag: T) -> Self { |
| Box::new(diag) |
| } |
| } |
| |
| impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + Send + 'static> { |
| fn from(diag: T) -> Self { |
| Box::<dyn Diagnostic + Send + Sync>::from(diag) |
| } |
| } |
| |
| impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'static> { |
| fn from(diag: T) -> Self { |
| Box::<dyn Diagnostic + Send + Sync>::from(diag) |
| } |
| } |
| |
| impl From<&str> for Box<dyn Diagnostic> { |
| fn from(s: &str) -> Self { |
| From::from(String::from(s)) |
| } |
| } |
| |
| impl<'a> From<&str> for Box<dyn Diagnostic + Send + Sync + 'a> { |
| fn from(s: &str) -> Self { |
| From::from(String::from(s)) |
| } |
| } |
| |
| impl From<String> for Box<dyn Diagnostic> { |
| fn from(s: String) -> Self { |
| let err1: Box<dyn Diagnostic + Send + Sync> = From::from(s); |
| let err2: Box<dyn Diagnostic> = err1; |
| err2 |
| } |
| } |
| |
| impl From<String> for Box<dyn Diagnostic + Send + Sync> { |
| 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<Box<dyn std::error::Error + Send + Sync>> for Box<dyn Diagnostic + Send + Sync> { |
| fn from(s: Box<dyn std::error::Error + Send + Sync>) -> Self { |
| #[derive(thiserror::Error)] |
| #[error(transparent)] |
| struct BoxedDiagnostic(Box<dyn std::error::Error + Send + Sync>); |
| 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<Box<dyn SpanContents<'a> + '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<String>, |
| span: SourceSpan, |
| } |
| |
| impl LabeledSpan { |
| /// Makes a new labeled span. |
| pub const fn new(label: Option<String>, 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<String>, span: impl Into<SourceSpan>) -> 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<SourceSpan>, label: impl Into<String>) -> 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<String>) -> 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<SourceSpan>) -> 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<String>, |
| } |
| |
| 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<std::ops::Range<ByteOffset>> for SourceSpan { |
| fn from(range: std::ops::Range<ByteOffset>) -> Self { |
| Self { |
| offset: range.start.into(), |
| length: range.len(), |
| } |
| } |
| } |
| |
| impl From<SourceOffset> for SourceSpan { |
| fn from(offset: SourceOffset) -> Self { |
| Self { offset, length: 0 } |
| } |
| } |
| |
| impl From<ByteOffset> 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<str>, 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<ByteOffset> 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)) |
| } |