aboutsummaryrefslogtreecommitdiff
path: root/src/protocol.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/protocol.rs')
-rw-r--r--src/protocol.rs685
1 files changed, 685 insertions, 0 deletions
diff --git a/src/protocol.rs b/src/protocol.rs
new file mode 100644
index 0000000..7531db1
--- /dev/null
+++ b/src/protocol.rs
@@ -0,0 +1,685 @@
+/*!
+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
+ }
+}
+
+impl std::error::Error for Box<dyn Diagnostic> {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ (**self).source()
+ }
+
+ fn cause(&self) -> Option<&dyn std::error::Error> {
+ self.source()
+ }
+}
+
+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))
+}