Skip to main content

thistrace/
lib.rs

1//! `thistrace` adds callsite provenance (file/line/column) to `thiserror` enums
2//! without requiring `map_err(...)` at each callsite.
3//!
4//! It works by generating `#[track_caller]` `From<T>` impls for `#[from]` conversions,
5//! so `?` captures the location where the conversion happened.
6//!
7//! ## Quickstart
8//!
9//! ```rust
10//! use thistrace::prelude::*;
11//!
12//! #[traceable]
13//! #[derive(Debug, thiserror::Error)]
14//! enum AppError {
15//!     #[error("io")]
16//!     Io(#[from] Origin<std::io::Error>),
17//! }
18//!
19//! fn leaf() -> Result<(), Origin<std::io::Error>> {
20//!     Err(origin(std::io::Error::new(std::io::ErrorKind::Other, "boom")))
21//! }
22//!
23//! fn top() -> Result<(), AppError> {
24//!     leaf()?; // adds a conversion/bubble frame
25//!     Ok(())
26//! }
27//!
28//! let err = top().unwrap_err();
29//! let _ = format!("{}", OneLineTrace::new(&err));
30//! assert!(!trace_frames(&err).is_empty());
31//! ```
32
33use std::fmt;
34
35pub use thistrace_macros::traceable;
36
37pub mod prelude {
38    pub use crate::{
39        bubble, bubble_into, bubble_err, from_with_trace, map_err_with_trace, origin, rebubble,
40        rebubble_err, trace_frames, traceable, Bubbled, DisplayTrace, Frame, HasTrace, OneLineTrace,
41        Origin, Trace,
42    };
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
46pub struct Frame {
47    pub file: &'static str,
48    pub line: u32,
49    pub column: u32,
50}
51
52impl Frame {
53    pub fn from_location(location: &'static std::panic::Location<'static>) -> Self {
54        Self {
55            file: location.file(),
56            line: location.line(),
57            column: location.column(),
58        }
59    }
60}
61
62#[derive(Clone, Debug, Default, Eq, PartialEq)]
63pub struct Trace {
64    frames: Vec<Frame>,
65}
66
67impl Trace {
68    pub fn empty() -> Self {
69        Self { frames: Vec::new() }
70    }
71
72    pub fn from_frame(frame: Frame) -> Self {
73        Self { frames: vec![frame] }
74    }
75
76    pub fn push(&mut self, frame: Frame) {
77        self.frames.push(frame);
78    }
79
80    pub fn frames(&self) -> &[Frame] {
81        &self.frames
82    }
83}
84
85pub trait HasTrace {
86    fn trace(&self) -> Option<&Trace>;
87}
88
89pub fn trace_frames<T: HasTrace + ?Sized>(err: &T) -> &[Frame] {
90    static EMPTY: [Frame; 0] = [];
91    err.trace().map(Trace::frames).unwrap_or(&EMPTY)
92}
93
94#[track_caller]
95pub fn map_err_with_trace<T, E, O, F>(res: Result<T, E>, f: F) -> Result<T, O>
96where
97    F: FnOnce(E, Trace) -> O,
98{
99    let loc = std::panic::Location::caller();
100    let trace = Trace::from_frame(Frame::from_location(loc));
101    res.map_err(|e| f(e, trace))
102}
103
104#[macro_export]
105macro_rules! from_with_trace {
106    ($expr:expr, $variant:path { $($field:ident),* $(,)? }) => {
107        $crate::from_with_trace!($expr, $variant { $($field: $field),* })
108    };
109    ($expr:expr, $variant:path { $($field:ident : _),* $(,)? }) => {
110        $crate::from_with_trace!(
111            $expr,
112            $variant { $($field: ::core::default::Default::default()),* }
113        )
114    };
115    ($expr:expr, $variant:path { $($field:ident : $value:expr),* $(,)? }) => {
116        $crate::map_err_with_trace($expr, |source, trace| {
117            $variant { source, trace, $($field: $value),* }
118        })
119    };
120}
121
122#[derive(Debug)]
123pub struct Origin<E> {
124    source: E,
125    trace: Trace,
126}
127
128impl<E> Origin<E> {
129    pub fn new(source: E, trace: Trace) -> Self {
130        Self { source, trace }
131    }
132
133    pub fn into_inner(self) -> E {
134        self.source
135    }
136
137    pub fn into_parts(self) -> (E, Trace) {
138        (self.source, self.trace)
139    }
140}
141
142#[track_caller]
143pub fn origin<E>(source: E) -> Origin<E> {
144    let loc = std::panic::Location::caller();
145    let frame = Frame::from_location(loc);
146    Origin::new(source, Trace::from_frame(frame))
147}
148
149#[derive(Debug)]
150pub struct Bubbled<E> {
151    source: E,
152    trace: Trace,
153}
154
155impl<E> Bubbled<E> {
156    pub fn new(source: E, trace: Trace) -> Self {
157        Self { source, trace }
158    }
159
160    pub fn into_parts(self) -> (E, Trace) {
161        (self.source, self.trace)
162    }
163}
164
165#[track_caller]
166pub fn bubble<E>(source: E) -> Bubbled<E>
167where
168    E: std::error::Error + HasTrace,
169{
170    let loc = std::panic::Location::caller();
171    let frame = Frame::from_location(loc);
172    let mut trace = source.trace().cloned().unwrap_or_else(Trace::empty);
173    trace.push(frame);
174    Bubbled::new(source, trace)
175}
176
177#[track_caller]
178pub fn rebubble<E>(source: Bubbled<E>) -> Bubbled<E>
179where
180    E: std::error::Error,
181{
182    let (inner, mut trace) = source.into_parts();
183    let loc = std::panic::Location::caller();
184    trace.push(Frame::from_location(loc));
185    Bubbled::new(inner, trace)
186}
187
188#[macro_export]
189macro_rules! bubble_err {
190    () => {
191        |e| $crate::bubble(e)
192    };
193}
194
195#[macro_export]
196macro_rules! rebubble_err {
197    () => {
198        |e| $crate::rebubble(e)
199    };
200}
201
202#[macro_export]
203macro_rules! bubble_into {
204    ($ty:ty) => {
205        |e| <$ty as ::core::convert::From<_>>::from($crate::bubble(e))
206    };
207}
208
209impl<E> HasTrace for Bubbled<E> {
210    fn trace(&self) -> Option<&Trace> {
211        Some(&self.trace)
212    }
213}
214
215impl<E> fmt::Display for Bubbled<E>
216where
217    E: fmt::Display,
218{
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        self.source.fmt(f)
221    }
222}
223
224impl<E> std::error::Error for Bubbled<E>
225where
226    E: std::error::Error + 'static,
227{
228    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
229        self.source.source()
230    }
231}
232
233impl<E> HasTrace for Origin<E> {
234    fn trace(&self) -> Option<&Trace> {
235        Some(&self.trace)
236    }
237}
238
239impl<E> fmt::Display for Origin<E>
240where
241    E: fmt::Display,
242{
243    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244        self.source.fmt(f)
245    }
246}
247
248impl<E> std::error::Error for Origin<E>
249where
250    E: std::error::Error + 'static,
251{
252    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
253        self.source.source()
254    }
255}
256
257pub struct DisplayTrace<'a, E: ?Sized> {
258    err: &'a E,
259}
260
261impl<'a, E: ?Sized> DisplayTrace<'a, E> {
262    pub fn new(err: &'a E) -> Self {
263        Self { err }
264    }
265}
266
267impl<E> fmt::Display for DisplayTrace<'_, E>
268where
269    E: std::error::Error + HasTrace,
270{
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        write!(f, "{}", self.err)?;
273
274        if let Some(trace) = self.err.trace() {
275            writeln!(f)?;
276            for frame in trace.frames() {
277                writeln!(
278                    f,
279                    "  at {}:{}:{}",
280                    frame.file, frame.line, frame.column
281                )?;
282            }
283        }
284
285        let mut cur: Option<&(dyn std::error::Error + 'static)> = self.err.source();
286        while let Some(e) = cur {
287            writeln!(f, "\ncaused by: {e}")?;
288            cur = e.source();
289        }
290
291        Ok(())
292    }
293}
294
295pub struct OneLineTrace<'a, E: ?Sized> {
296    err: &'a E,
297}
298
299impl<'a, E: ?Sized> OneLineTrace<'a, E> {
300    pub fn new(err: &'a E) -> Self {
301        Self { err }
302    }
303}
304
305impl<E> fmt::Display for OneLineTrace<'_, E>
306where
307    E: std::error::Error + HasTrace,
308{
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        write!(f, "{}", self.err)?;
311
312        if let Some(trace) = self.err.trace() {
313            write!(f, " [trace")?;
314            for frame in trace.frames() {
315                write!(f, " {}:{}:{}", frame.file, frame.line, frame.column)?;
316            }
317            write!(f, " ]")?;
318        }
319
320        let mut cur: Option<&(dyn std::error::Error + 'static)> = self.err.source();
321        while let Some(e) = cur {
322            write!(f, " | caused by: {e}")?;
323            cur = e.source();
324        }
325
326        Ok(())
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn trace_from_location_copies_fields() {
336        #[track_caller]
337        fn capture() -> Frame {
338            Frame::from_location(std::panic::Location::caller())
339        }
340
341        let frame = capture();
342        assert!(frame.file.ends_with("lib.rs"));
343        assert!(frame.line > 0);
344        assert!(frame.column > 0);
345    }
346}