rich-err 0.1.0

A highly detailed error type for compilers, tracebacks, etc.
Documentation
#![doc = include_str!("../README.md")]

pub mod error_code;
pub mod label;
pub mod note;
pub mod span;

pub use error_code::{ErrorCode, ToErrorCode};
pub use label::Label;
pub use note::Note;
pub use span::Span;

use std::{borrow::Cow, path::Path};

/// A highly detailed error type designed for:
///
/// - Compilers (or anything similar)
/// - Tracebacks
/// - Any situation where a source file would provide valuable context for the user
///
/// This serves as a sort of wrapper around [`ariadne`], although it is missing much of
/// [`ariadne`]'s functionality. The point is to have a simple interface for constructing error
/// reports that works well enough for sufficiently simple applications.
///
/// As a word of caution, this higher level of detail comes at the cost of higher memory usage. The
/// `struct` alone is around 7 times larger than a [`String`], and some features may incur extra
/// heap allocations as well. Thus, it is not advisable to use rich errors unless an ordinary error
/// is truly insufficient.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RichError {
    /// The [error code](ErrorCode).
    pub code: ErrorCode,
    /// The error message.
    ///
    /// This is meant to be a description of the general error specified by `code` rather than a
    /// verbose description of precisely what failed. More details should be sent through `label`
    /// and/or `notes`.
    pub message: String,
    /// The broad area in the source file that caused the error.
    ///
    /// The exact location of the error can be specified via `label`.
    pub broad_span: Span,
    /// An additional error message associated with a specific area of the source code.
    pub label: Option<Label>,
    /// An arbitrary number of notes that provide additional context and/or assistance to the user.
    pub notes: Vec<Note>,
}

pub type RichResult<T> = Result<T, RichError>;

impl RichError {
    /// Constructs a new rich error.
    ///
    /// To add additional metadata to this message, use the relevant builder methods.
    pub fn new<E>(err: E, broad_span: Span) -> Self
    where
        E: ToErrorCode + ToString,
    {
        Self {
            code: err.code(),
            message: err.to_string(),
            broad_span,
            label: None,
            notes: Vec::new(),
        }
    }

    /// Adds an ordinary note to this error.
    pub fn with_note<S>(mut self, message: S) -> Self
    where
        S: ToString,
    {
        self.notes.push(Note::new_note(message));
        self
    }

    /// Adds a note with a help message to this error.
    pub fn with_help<S>(mut self, message: S) -> Self
    where
        S: ToString,
    {
        self.notes.push(Note::new_help(message));
        self
    }

    /// Adds a label to this error.
    ///
    /// If a label was already present, this overwrites it.
    pub fn with_label(mut self, label: Label) -> Self {
        self.label = Some(label);
        self
    }

    /// Specifies the exact location of the error within the broader span.
    ///
    /// This is similar to creating a blank [label](Label) with the provided span, but it also
    /// handles the case where `label` was already set. In that case, `label`'s message will be
    /// preserved while its span is overwritten.
    pub fn with_narrow_span(mut self, span: Span) -> Self {
        let new_label = Label::new(span);
        self.label = Some(match self.label {
            Some(label) => new_label.with_message(label.message),
            None => new_label,
        });
        self
    }

    /// Reports the error to the user.
    ///
    /// This is a relatively expensive operation, so depending on the use case, it might be wise to
    /// either report all errors at once or have a separate thread report errors as they crop up.
    pub fn report<P>(self, source: &str, source_path: P) -> std::io::Result<()>
    where
        P: AsRef<Path>,
    {
        let source_name = match source_path.as_ref().file_name() {
            Some(name) => name.to_string_lossy(),
            None => Cow::Owned("<source>".to_string()),
        };

        let mut builder = ariadne::Report::build(
            ariadne::ReportKind::Error,
            (source_name.clone(), self.broad_span),
        )
        .with_config(ariadne::Config::new())
        .with_code(self.code)
        .with_message(self.message);

        for note in self.notes {
            note.add_to(&mut builder);
        }

        if let Some(label) = self.label {
            label.add_to(&mut builder, source_name.clone());
        }

        builder
            .finish()
            .eprint((source_name, ariadne::Source::from(source)))
    }
}