thistrace 0.1.0

Callsite provenance (file/line/col) for thiserror #[from] conversions via #[track_caller]
Documentation
//! `thistrace` adds callsite provenance (file/line/column) to `thiserror` enums
//! without requiring `map_err(...)` at each callsite.
//!
//! It works by generating `#[track_caller]` `From<T>` impls for `#[from]` conversions,
//! so `?` captures the location where the conversion happened.
//!
//! ## Quickstart
//!
//! ```rust
//! use thistrace::prelude::*;
//!
//! #[traceable]
//! #[derive(Debug, thiserror::Error)]
//! enum AppError {
//!     #[error("io")]
//!     Io(#[from] Origin<std::io::Error>),
//! }
//!
//! fn leaf() -> Result<(), Origin<std::io::Error>> {
//!     Err(origin(std::io::Error::new(std::io::ErrorKind::Other, "boom")))
//! }
//!
//! fn top() -> Result<(), AppError> {
//!     leaf()?; // adds a conversion/bubble frame
//!     Ok(())
//! }
//!
//! let err = top().unwrap_err();
//! let _ = format!("{}", OneLineTrace::new(&err));
//! assert!(!trace_frames(&err).is_empty());
//! ```

use std::fmt;

pub use thistrace_macros::traceable;

pub mod prelude {
    pub use crate::{
        bubble, bubble_into, bubble_err, from_with_trace, map_err_with_trace, origin, rebubble,
        rebubble_err, trace_frames, traceable, Bubbled, DisplayTrace, Frame, HasTrace, OneLineTrace,
        Origin, Trace,
    };
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Frame {
    pub file: &'static str,
    pub line: u32,
    pub column: u32,
}

impl Frame {
    pub fn from_location(location: &'static std::panic::Location<'static>) -> Self {
        Self {
            file: location.file(),
            line: location.line(),
            column: location.column(),
        }
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Trace {
    frames: Vec<Frame>,
}

impl Trace {
    pub fn empty() -> Self {
        Self { frames: Vec::new() }
    }

    pub fn from_frame(frame: Frame) -> Self {
        Self { frames: vec![frame] }
    }

    pub fn push(&mut self, frame: Frame) {
        self.frames.push(frame);
    }

    pub fn frames(&self) -> &[Frame] {
        &self.frames
    }
}

pub trait HasTrace {
    fn trace(&self) -> Option<&Trace>;
}

pub fn trace_frames<T: HasTrace + ?Sized>(err: &T) -> &[Frame] {
    static EMPTY: [Frame; 0] = [];
    err.trace().map(Trace::frames).unwrap_or(&EMPTY)
}

#[track_caller]
pub fn map_err_with_trace<T, E, O, F>(res: Result<T, E>, f: F) -> Result<T, O>
where
    F: FnOnce(E, Trace) -> O,
{
    let loc = std::panic::Location::caller();
    let trace = Trace::from_frame(Frame::from_location(loc));
    res.map_err(|e| f(e, trace))
}

#[macro_export]
macro_rules! from_with_trace {
    ($expr:expr, $variant:path { $($field:ident),* $(,)? }) => {
        $crate::from_with_trace!($expr, $variant { $($field: $field),* })
    };
    ($expr:expr, $variant:path { $($field:ident : _),* $(,)? }) => {
        $crate::from_with_trace!(
            $expr,
            $variant { $($field: ::core::default::Default::default()),* }
        )
    };
    ($expr:expr, $variant:path { $($field:ident : $value:expr),* $(,)? }) => {
        $crate::map_err_with_trace($expr, |source, trace| {
            $variant { source, trace, $($field: $value),* }
        })
    };
}

#[derive(Debug)]
pub struct Origin<E> {
    source: E,
    trace: Trace,
}

impl<E> Origin<E> {
    pub fn new(source: E, trace: Trace) -> Self {
        Self { source, trace }
    }

    pub fn into_inner(self) -> E {
        self.source
    }

    pub fn into_parts(self) -> (E, Trace) {
        (self.source, self.trace)
    }
}

#[track_caller]
pub fn origin<E>(source: E) -> Origin<E> {
    let loc = std::panic::Location::caller();
    let frame = Frame::from_location(loc);
    Origin::new(source, Trace::from_frame(frame))
}

#[derive(Debug)]
pub struct Bubbled<E> {
    source: E,
    trace: Trace,
}

impl<E> Bubbled<E> {
    pub fn new(source: E, trace: Trace) -> Self {
        Self { source, trace }
    }

    pub fn into_parts(self) -> (E, Trace) {
        (self.source, self.trace)
    }
}

#[track_caller]
pub fn bubble<E>(source: E) -> Bubbled<E>
where
    E: std::error::Error + HasTrace,
{
    let loc = std::panic::Location::caller();
    let frame = Frame::from_location(loc);
    let mut trace = source.trace().cloned().unwrap_or_else(Trace::empty);
    trace.push(frame);
    Bubbled::new(source, trace)
}

#[track_caller]
pub fn rebubble<E>(source: Bubbled<E>) -> Bubbled<E>
where
    E: std::error::Error,
{
    let (inner, mut trace) = source.into_parts();
    let loc = std::panic::Location::caller();
    trace.push(Frame::from_location(loc));
    Bubbled::new(inner, trace)
}

#[macro_export]
macro_rules! bubble_err {
    () => {
        |e| $crate::bubble(e)
    };
}

#[macro_export]
macro_rules! rebubble_err {
    () => {
        |e| $crate::rebubble(e)
    };
}

#[macro_export]
macro_rules! bubble_into {
    ($ty:ty) => {
        |e| <$ty as ::core::convert::From<_>>::from($crate::bubble(e))
    };
}

impl<E> HasTrace for Bubbled<E> {
    fn trace(&self) -> Option<&Trace> {
        Some(&self.trace)
    }
}

impl<E> fmt::Display for Bubbled<E>
where
    E: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.source.fmt(f)
    }
}

impl<E> std::error::Error for Bubbled<E>
where
    E: std::error::Error + 'static,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.source()
    }
}

impl<E> HasTrace for Origin<E> {
    fn trace(&self) -> Option<&Trace> {
        Some(&self.trace)
    }
}

impl<E> fmt::Display for Origin<E>
where
    E: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.source.fmt(f)
    }
}

impl<E> std::error::Error for Origin<E>
where
    E: std::error::Error + 'static,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.source()
    }
}

pub struct DisplayTrace<'a, E: ?Sized> {
    err: &'a E,
}

impl<'a, E: ?Sized> DisplayTrace<'a, E> {
    pub fn new(err: &'a E) -> Self {
        Self { err }
    }
}

impl<E> fmt::Display for DisplayTrace<'_, E>
where
    E: std::error::Error + HasTrace,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.err)?;

        if let Some(trace) = self.err.trace() {
            writeln!(f)?;
            for frame in trace.frames() {
                writeln!(
                    f,
                    "  at {}:{}:{}",
                    frame.file, frame.line, frame.column
                )?;
            }
        }

        let mut cur: Option<&(dyn std::error::Error + 'static)> = self.err.source();
        while let Some(e) = cur {
            writeln!(f, "\ncaused by: {e}")?;
            cur = e.source();
        }

        Ok(())
    }
}

pub struct OneLineTrace<'a, E: ?Sized> {
    err: &'a E,
}

impl<'a, E: ?Sized> OneLineTrace<'a, E> {
    pub fn new(err: &'a E) -> Self {
        Self { err }
    }
}

impl<E> fmt::Display for OneLineTrace<'_, E>
where
    E: std::error::Error + HasTrace,
{
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.err)?;

        if let Some(trace) = self.err.trace() {
            write!(f, " [trace")?;
            for frame in trace.frames() {
                write!(f, " {}:{}:{}", frame.file, frame.line, frame.column)?;
            }
            write!(f, " ]")?;
        }

        let mut cur: Option<&(dyn std::error::Error + 'static)> = self.err.source();
        while let Some(e) = cur {
            write!(f, " | caused by: {e}")?;
            cur = e.source();
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn trace_from_location_copies_fields() {
        #[track_caller]
        fn capture() -> Frame {
            Frame::from_location(std::panic::Location::caller())
        }

        let frame = capture();
        assert!(frame.file.ends_with("lib.rs"));
        assert!(frame.line > 0);
        assert!(frame.column > 0);
    }
}