Skip to main content

suzunari_error/
stack_error.rs

1use crate::Location;
2use core::error::Error;
3
4/// Error trait extension that adds source code location tracking.
5///
6/// Types implementing this trait carry a `Location` at each level of the
7/// error chain, enabling `StackReport` to produce stack-trace-like output.
8///
9/// # Design note: no `Send + Sync` supertrait
10///
11/// This trait requires only `Error`, not `Send + Sync + 'static` (unlike
12/// anyhow/eyre). This aligns with snafu, which does not impose `Send + Sync`
13/// on error types. `BoxedStackError` adds `Send + Sync` bounds for the
14/// thread-safe trait object case.
15///
16/// # Design note: unsealed trait
17///
18/// This trait is intentionally unsealed — external crates may implement it
19/// for custom wrapper types (e.g., similar to `BoxedStackError`). Future
20/// method additions must provide default implementations to avoid breaking
21/// downstream impls.
22///
23/// # Deriving
24///
25/// Use `#[suzunari_error]` (recommended) or `#[derive(StackError)]` directly.
26/// Both resolve the location field via `#[stack(location)]` or by detecting a
27/// `Location`-typed field. Manual impl is only needed for wrapper types like
28/// `BoxedStackError`.
29///
30/// # Example
31///
32/// ```
33/// use suzunari_error::*;
34///
35/// #[suzunari_error]
36/// #[suzu(display("fetch failed for {url}"))]
37/// struct FetchError {
38///     url: String,
39///     source: std::io::Error,
40/// }
41///
42/// fn fetch(url: &str) -> Result<(), FetchError> {
43///     std::fs::read(url).context(FetchSnafu { url })?;
44///     Ok(())
45/// }
46///
47/// let err = fetch("/nonexistent").unwrap_err();
48///
49/// // StackError methods:
50/// assert!(err.location().file().ends_with(".rs"));
51/// assert_eq!(err.type_name(), "FetchError");
52/// assert!(err.stack_source().is_none()); // io::Error is not StackError
53/// assert_eq!(err.depth(), 1);            // 1 cause in the chain
54/// ```
55pub trait StackError: Error {
56    /// Returns the location where this error was constructed.
57    #[must_use]
58    fn location(&self) -> Location;
59
60    /// Returns a human-readable type name for display in stack traces.
61    ///
62    /// The derive macro generates this as a `&'static str` literal:
63    /// - Structs: `"StructName"`
64    /// - Enum variants: `"EnumName::VariantName"`
65    ///
66    /// Generic type parameters are not included. This is intended for display
67    /// purposes only — do not parse or match against it programmatically.
68    #[must_use]
69    fn type_name(&self) -> &'static str;
70
71    /// Returns the source error as a StackError, if available.
72    ///
73    /// This enables StackReport to traverse the error chain with
74    /// location info. The derive macro generates this automatically
75    /// using autoref specialization (see the `__private` module).
76    ///
77    /// # Contract
78    /// If `stack_source()` returns `Some(s)`, then `Error::source()`
79    /// must also return `Some(e)` where `e` and `s` refer to the same
80    /// underlying error value (i.e., `s` is a `&dyn StackError` view
81    /// of the `&dyn Error` returned by `source()`). The derive macro
82    /// upholds this automatically; manual impls must ensure consistency.
83    ///
84    /// Violating this contract causes `StackReport` to produce incomplete
85    /// output in release builds (the `debug_assert!` that checks this is
86    /// stripped). In debug builds, a panic will occur instead.
87    #[must_use]
88    fn stack_source(&self) -> Option<&dyn StackError> {
89        None
90    }
91
92    /// Returns the number of errors in the `Error::source()` chain (excluding self).
93    ///
94    /// Traverses the full `Error::source()` chain (not `stack_source()`),
95    /// counting both `StackError` and non-`StackError` causes.
96    ///
97    /// Note: this count may differ from the number of lines in `StackReport`
98    /// output, which also shows the top-level error on the first line.
99    #[must_use]
100    fn depth(&self) -> usize {
101        // successors() can't be used here due to trait object lifetime constraints:
102        // source() returns Option<&dyn Error> with a lifetime tied to &self,
103        // but `successors` requires the closure output lifetime to match its input.
104        let mut count = 0;
105        let mut current = self.source();
106        while let Some(e) = current {
107            count += 1;
108            current = e.source();
109        }
110        count
111    }
112}
113
114#[cfg(feature = "alloc")]
115mod alloc_impls {
116    use super::*;
117    use alloc::boxed::Box;
118    use alloc::sync::Arc;
119
120    /// Delegates all methods to the inner `T`.
121    ///
122    /// Requires `T: Sized`; `Box<dyn StackError>` needs a separate impl
123    /// because `core` provides `impl Error for Box<T: Error + ?Sized>` but
124    /// we still need to manually route `StackError` methods.
125    impl<T: StackError> StackError for Box<T> {
126        fn location(&self) -> Location {
127            self.as_ref().location()
128        }
129        fn type_name(&self) -> &'static str {
130            self.as_ref().type_name()
131        }
132        fn stack_source(&self) -> Option<&dyn StackError> {
133            self.as_ref().stack_source()
134        }
135    }
136    /// Delegates all methods to the inner `T` via `Arc::as_ref`.
137    impl<T: ?Sized + StackError> StackError for Arc<T> {
138        fn location(&self) -> Location {
139            self.as_ref().location()
140        }
141        fn type_name(&self) -> &'static str {
142            self.as_ref().type_name()
143        }
144        fn stack_source(&self) -> Option<&dyn StackError> {
145            self.as_ref().stack_source()
146        }
147    }
148
149    /// Routes `Error::source` through the trait object.
150    impl Error for Box<dyn StackError> {
151        fn source(&self) -> Option<&(dyn Error + 'static)> {
152            Error::source(Box::as_ref(self))
153        }
154    }
155
156    /// Delegates all methods through the `dyn StackError` trait object.
157    impl StackError for Box<dyn StackError> {
158        fn location(&self) -> Location {
159            self.as_ref().location()
160        }
161        fn type_name(&self) -> &'static str {
162            self.as_ref().type_name()
163        }
164        fn stack_source(&self) -> Option<&dyn StackError> {
165            self.as_ref().stack_source()
166        }
167    }
168
169    /// Routes `Error::source` through the thread-safe trait object.
170    impl Error for Box<dyn StackError + Send + Sync> {
171        fn source(&self) -> Option<&(dyn Error + 'static)> {
172            Error::source(Box::as_ref(self))
173        }
174    }
175
176    /// Delegates all methods through the `dyn StackError + Send + Sync` trait object.
177    impl StackError for Box<dyn StackError + Send + Sync> {
178        fn location(&self) -> Location {
179            self.as_ref().location()
180        }
181        fn type_name(&self) -> &'static str {
182            self.as_ref().type_name()
183        }
184        fn stack_source(&self) -> Option<&dyn StackError> {
185            self.as_ref().stack_source()
186        }
187    }
188}
189
190#[cfg(all(test, feature = "alloc"))]
191mod tests {
192    // Tests use raw #[derive(Snafu)] + manual impl to test StackError trait
193    // independently of proc-macro layer. .build() is snafu's standard test pattern.
194    use super::*;
195    use crate::StackReport;
196    use alloc::boxed::Box;
197    use alloc::format;
198    use alloc::string::String;
199    use alloc::sync::Arc;
200    use snafu::prelude::*;
201
202    #[derive(Debug, Snafu)]
203    #[snafu(display("Simple test error: {}", message))]
204    struct SimpleError {
205        message: String,
206        #[snafu(implicit)]
207        location: Location,
208    }
209    impl StackError for SimpleError {
210        fn location(&self) -> Location {
211            self.location
212        }
213        fn type_name(&self) -> &'static str {
214            "SimpleError"
215        }
216    }
217
218    #[derive(Debug, Snafu)]
219    #[snafu(display("Wrapper error: {}", message))]
220    struct WrapperError {
221        message: String,
222        source: Box<dyn StackError + Send + Sync>,
223        #[snafu(implicit)]
224        location: Location,
225    }
226    impl StackError for WrapperError {
227        fn location(&self) -> Location {
228            self.location
229        }
230        fn type_name(&self) -> &'static str {
231            "WrapperError"
232        }
233        fn stack_source(&self) -> Option<&dyn StackError> {
234            // Box<dyn StackError + Send + Sync> implements StackError
235            Some(self.source.as_ref())
236        }
237    }
238
239    #[test]
240    fn test_basic_location() {
241        let error = SimpleSnafu {
242            message: "Something went wrong",
243        }
244        .build();
245        assert_eq!(error.location().file(), file!());
246        assert!(error.location().line() > 0);
247        assert!(format!("{}", error).contains("Simple test error"));
248        assert!(format!("{}", error).contains("Something went wrong"));
249
250        handle_stack_error(error)
251    }
252
253    #[test]
254    fn test_error_boxing() {
255        let concrete_error = SimpleSnafu {
256            message: "Original error",
257        }
258        .build();
259        let boxed_error: Box<dyn StackError> = Box::new(concrete_error);
260
261        assert_eq!(boxed_error.location().file(), file!());
262        assert!(boxed_error.location().line() > 0);
263        assert!(format!("{}", boxed_error).contains("Simple test error"));
264        assert!(format!("{}", boxed_error).contains("Original error"));
265
266        handle_stack_error(boxed_error)
267    }
268
269    #[test]
270    fn test_error_chaining() {
271        fn gen_root_error() -> Result<(), Box<dyn StackError + Send + Sync + 'static>> {
272            let root_error = SimpleSnafu {
273                message: "Root cause",
274            }
275            .build();
276            Err(Box::new(root_error))
277        }
278        let root_error = gen_root_error();
279        let root_location = root_error.unwrap_err().location().line();
280
281        let wrapper_error = gen_root_error()
282            .context(WrapperSnafu {
283                message: "Something failed",
284            })
285            .unwrap_err();
286
287        assert!(wrapper_error.location().file().ends_with("stack_error.rs"));
288        assert_ne!(wrapper_error.location().line(), root_location);
289
290        let report = format!("{:?}", StackReport::from(wrapper_error));
291        let file = file!();
292        assert!(report.contains("Error: WrapperError: Wrapper error: Something failed"));
293        assert!(report.contains(&format!(", at {file}:")));
294        assert!(report.contains("Caused by"));
295        assert!(report.contains("1| SimpleError: Simple test error: Root cause"));
296    }
297
298    #[test]
299    fn test_arc_errors() {
300        let error = SimpleSnafu {
301            message: "Arc-wrapped error",
302        }
303        .build();
304        let original_location = error.location().line();
305        let arc_error = Arc::new(error);
306
307        assert_eq!(arc_error.location().line(), original_location);
308
309        let cloned_arc = arc_error.clone();
310        assert_eq!(cloned_arc.location().line(), original_location);
311
312        handle_stack_error(arc_error);
313
314        let arc_error: Arc<dyn StackError> = Arc::new(SimpleSnafu { message: "Simple" }.build());
315        handle_stack_error(arc_error);
316    }
317
318    #[test]
319    fn test_from_implementation() {
320        let concrete_error = SimpleSnafu {
321            message: "Converted error",
322        }
323        .build();
324        let original_location = concrete_error.location().line();
325        let boxed_error: Box<dyn StackError + Send + Sync + 'static> = Box::new(concrete_error);
326
327        assert_eq!(boxed_error.location().line(), original_location);
328        handle_stack_error(boxed_error);
329    }
330
331    #[test]
332    fn test_depth_one() {
333        fn gen_root() -> Result<(), Box<dyn StackError + Send + Sync + 'static>> {
334            let root = SimpleSnafu { message: "root" }.build();
335            Err(Box::new(root))
336        }
337        let wrapper = gen_root()
338            .context(WrapperSnafu { message: "wrapper" })
339            .unwrap_err();
340        // WrapperError has one source (SimpleError), so depth == 1
341        assert_eq!(wrapper.depth(), 1);
342    }
343
344    #[test]
345    fn test_box_concrete_stack_error() {
346        // Box<T: Sized + StackError> blanket impl
347        let concrete = SimpleSnafu {
348            message: "boxed concrete",
349        }
350        .build();
351        let original_line = concrete.location().line();
352        let boxed: Box<SimpleError> = Box::new(concrete);
353
354        assert_eq!(boxed.location().line(), original_line);
355        assert_eq!(boxed.type_name(), "SimpleError");
356        assert!(boxed.stack_source().is_none());
357        handle_stack_error(boxed);
358    }
359
360    fn handle_stack_error<T: StackError>(_: T) {}
361
362    // --- GAP-08: Box<dyn StackError> (non-Send-Sync) Error and StackError impls ---
363    #[test]
364    fn test_box_dyn_stack_error_non_send_sync() {
365        let concrete = SimpleSnafu {
366            message: "boxed non-send-sync",
367        }
368        .build();
369        let original_line = concrete.location().line();
370        let boxed: Box<dyn StackError> = Box::new(concrete);
371
372        // StackError methods should work
373        assert_eq!(boxed.location().line(), original_line);
374        assert_eq!(boxed.type_name(), "SimpleError");
375        assert!(boxed.stack_source().is_none());
376
377        // Error impl should work
378        let err: &dyn Error = &boxed;
379        assert!(format!("{err}").contains("boxed non-send-sync"));
380    }
381}