snafu/
report.rs

1use crate::ChainCompat;
2use core::fmt;
3
4#[cfg(all(feature = "std", feature = "rust_1_61"))]
5use std::process::{ExitCode, Termination};
6
7/// Opinionated solution to format an error in a user-friendly
8/// way. Useful as the return type from `main` and test functions.
9///
10/// Most users will use the [`snafu::report`][] procedural macro
11/// instead of directly using this type, but you can if you do not
12/// wish to use the macro.
13///
14/// [`snafu::report`]: macro@crate::report
15///
16/// ## Rust 1.61 and up
17///
18/// Change the return type of the function to [`Report`][] and wrap
19/// the body of your function with [`Report::capture`][].
20///
21/// ## Rust before 1.61
22///
23/// Use [`Report`][] as the error type inside of [`Result`][] and then
24/// call either [`Report::capture_into_result`][] or
25/// [`Report::from_error`][].
26///
27/// ## Nightly Rust
28///
29/// Enabling the [`unstable-try-trait` feature flag][try-ff] will
30/// allow you to use the `?` operator directly:
31///
32/// ```rust
33/// use snafu::{prelude::*, Report};
34///
35/// # #[cfg(all(feature = "unstable-try-trait", feature = "rust_1_61"))]
36/// fn main() -> Report<PlaceholderError> {
37///     let _v = may_fail_with_placeholder_error()?;
38///
39///     Report::ok()
40/// }
41/// # #[cfg(not(all(feature = "unstable-try-trait", feature = "rust_1_61")))] fn main() {}
42/// # #[derive(Debug, Snafu)]
43/// # struct PlaceholderError;
44/// # fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> { Ok(42) }
45/// ```
46///
47/// [try-ff]: crate::guide::feature_flags#unstable-try-trait
48///
49/// ## Interaction with the Provider API
50///
51/// If you return a [`Report`][] from your function and enable the
52/// [`unstable-provider-api` feature flag][provider-ff], additional
53/// capabilities will be added:
54///
55/// 1. If provided, a [`Backtrace`][] will be included in the output.
56/// 1. If provided, a [`ExitCode`][] will be used as the return value.
57///
58/// [provider-ff]: crate::guide::feature_flags#unstable-provider-api
59/// [`Backtrace`]: crate::Backtrace
60/// [`ExitCode`]: std::process::ExitCode
61///
62/// ## Stability of the output
63///
64/// The exact content and format of a displayed `Report` are not
65/// stable, but this type strives to print the error and as much
66/// user-relevant information in an easily-consumable manner
67pub struct Report<E>(Result<(), E>);
68
69impl<E> Report<E> {
70    /// Convert an error into a [`Report`][].
71    ///
72    /// Recommended if you support versions of Rust before 1.61.
73    ///
74    /// ```rust
75    /// use snafu::{prelude::*, Report};
76    ///
77    /// #[derive(Debug, Snafu)]
78    /// struct PlaceholderError;
79    ///
80    /// fn main() -> Result<(), Report<PlaceholderError>> {
81    ///     let _v = may_fail_with_placeholder_error().map_err(Report::from_error)?;
82    ///     Ok(())
83    /// }
84    ///
85    /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
86    ///     Ok(42)
87    /// }
88    /// ```
89    pub fn from_error(error: E) -> Self {
90        Self(Err(error))
91    }
92
93    /// Executes a closure that returns a [`Result`][], converting the
94    /// error variant into a [`Report`][].
95    ///
96    /// Recommended if you support versions of Rust before 1.61.
97    ///
98    /// ```rust
99    /// use snafu::{prelude::*, Report};
100    ///
101    /// #[derive(Debug, Snafu)]
102    /// struct PlaceholderError;
103    ///
104    /// fn main() -> Result<(), Report<PlaceholderError>> {
105    ///     Report::capture_into_result(|| {
106    ///         let _v = may_fail_with_placeholder_error()?;
107    ///
108    ///         Ok(())
109    ///     })
110    /// }
111    ///
112    /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
113    ///     Ok(42)
114    /// }
115    /// ```
116    pub fn capture_into_result<T>(body: impl FnOnce() -> Result<T, E>) -> Result<T, Self> {
117        body().map_err(Self::from_error)
118    }
119
120    /// Executes a closure that returns a [`Result`][], converting any
121    /// error to a [`Report`][].
122    ///
123    /// Recommended if you only support Rust version 1.61 or above.
124    ///
125    /// ```rust
126    /// use snafu::{prelude::*, Report};
127    ///
128    /// #[derive(Debug, Snafu)]
129    /// struct PlaceholderError;
130    ///
131    /// # #[cfg(feature = "rust_1_61")]
132    /// fn main() -> Report<PlaceholderError> {
133    ///     Report::capture(|| {
134    ///         let _v = may_fail_with_placeholder_error()?;
135    ///
136    ///         Ok(())
137    ///     })
138    /// }
139    /// # #[cfg(not(feature = "rust_1_61"))] fn main() {}
140    ///
141    /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
142    ///     Ok(42)
143    /// }
144    /// ```
145    pub fn capture(body: impl FnOnce() -> Result<(), E>) -> Self {
146        Self(body())
147    }
148
149    /// A [`Report`][] that indicates no error occurred.
150    pub const fn ok() -> Self {
151        Self(Ok(()))
152    }
153}
154
155impl<E> From<Result<(), E>> for Report<E> {
156    fn from(other: Result<(), E>) -> Self {
157        Self(other)
158    }
159}
160
161impl<E> fmt::Debug for Report<E>
162where
163    E: crate::Error,
164{
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        fmt::Display::fmt(self, f)
167    }
168}
169
170impl<E> fmt::Display for Report<E>
171where
172    E: crate::Error,
173{
174    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175        match &self.0 {
176            Err(e) => fmt::Display::fmt(&ReportFormatter(e), f),
177            _ => Ok(()),
178        }
179    }
180}
181
182#[cfg(all(feature = "std", feature = "rust_1_61"))]
183impl<E> Termination for Report<E>
184where
185    E: crate::Error,
186{
187    fn report(self) -> ExitCode {
188        match self.0 {
189            Ok(()) => ExitCode::SUCCESS,
190            Err(e) => {
191                eprintln!("{}", ReportFormatter(&e));
192
193                #[cfg(feature = "unstable-provider-api")]
194                {
195                    use core::any;
196
197                    any::request_value::<ExitCode>(&e)
198                        .or_else(|| any::request_ref::<ExitCode>(&e).copied())
199                        .unwrap_or(ExitCode::FAILURE)
200                }
201
202                #[cfg(not(feature = "unstable-provider-api"))]
203                {
204                    ExitCode::FAILURE
205                }
206            }
207        }
208    }
209}
210
211#[cfg(feature = "unstable-try-trait")]
212impl<T, E> core::ops::FromResidual<Result<T, E>> for Report<E> {
213    fn from_residual(residual: Result<T, E>) -> Self {
214        Self(residual.map(drop))
215    }
216}
217
218struct ReportFormatter<'a>(&'a dyn crate::Error);
219
220impl<'a> fmt::Display for ReportFormatter<'a> {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        #[cfg(feature = "std")]
223        {
224            if trace_cleaning_enabled() {
225                self.cleaned_error_trace(f)?;
226            } else {
227                self.error_trace(f)?;
228            }
229        }
230
231        #[cfg(not(feature = "std"))]
232        {
233            self.error_trace(f)?;
234        }
235
236        #[cfg(feature = "unstable-provider-api")]
237        {
238            use core::any;
239
240            if let Some(bt) = any::request_ref::<crate::Backtrace>(self.0) {
241                writeln!(f, "\nBacktrace:\n{}", bt)?;
242            }
243        }
244
245        Ok(())
246    }
247}
248
249impl<'a> ReportFormatter<'a> {
250    fn error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
251        writeln!(f, "{}", self.0)?;
252
253        let sources = ChainCompat::new(self.0).skip(1);
254        let plurality = sources.clone().take(2).count();
255
256        match plurality {
257            0 => {}
258            1 => writeln!(f, "\nCaused by this error:")?,
259            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
260        }
261
262        for (i, source) in sources.enumerate() {
263            // Let's use 1-based indexing for presentation
264            let i = i + 1;
265            writeln!(f, "{:3}: {}", i, source)?;
266        }
267
268        Ok(())
269    }
270
271    #[cfg(feature = "std")]
272    fn cleaned_error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
273        const NOTE: char = '*';
274
275        let mut any_cleaned = false;
276        let mut any_removed = false;
277        let cleaned_messages: Vec<_> = CleanedErrorText::new(self.0)
278            .flat_map(|(_, mut msg, cleaned)| {
279                if msg.is_empty() {
280                    any_removed = true;
281                    None
282                } else {
283                    if cleaned {
284                        any_cleaned = true;
285                        msg.push(' ');
286                        msg.push(NOTE);
287                    }
288                    Some(msg)
289                }
290            })
291            .collect();
292
293        let mut visible_messages = cleaned_messages.iter();
294
295        let head = match visible_messages.next() {
296            Some(v) => v,
297            None => return Ok(()),
298        };
299
300        writeln!(f, "{}", head)?;
301
302        match cleaned_messages.len() {
303            0 | 1 => {}
304            2 => writeln!(f, "\nCaused by this error:")?,
305            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
306        }
307
308        for (i, msg) in visible_messages.enumerate() {
309            // Let's use 1-based indexing for presentation
310            let i = i + 1;
311            writeln!(f, "{:3}: {}", i, msg)?;
312        }
313
314        if any_cleaned || any_removed {
315            write!(f, "\nNOTE: ")?;
316
317            if any_cleaned {
318                write!(
319                    f,
320                    "Some redundant information has been removed from the lines marked with {}. ",
321                    NOTE,
322                )?;
323            } else {
324                write!(f, "Some redundant information has been removed. ")?;
325            }
326
327            writeln!(
328                f,
329                "Set {}=1 to disable this behavior.",
330                SNAFU_RAW_ERROR_MESSAGES,
331            )?;
332        }
333
334        Ok(())
335    }
336}
337
338#[cfg(feature = "std")]
339const SNAFU_RAW_ERROR_MESSAGES: &str = "SNAFU_RAW_ERROR_MESSAGES";
340
341#[cfg(feature = "std")]
342fn trace_cleaning_enabled() -> bool {
343    use crate::once_bool::OnceBool;
344    use std::env;
345
346    static DISABLED: OnceBool = OnceBool::new();
347    !DISABLED.get(|| env::var_os(SNAFU_RAW_ERROR_MESSAGES).map_or(false, |v| v == "1"))
348}
349
350/// An iterator over an Error and its sources that removes duplicated
351/// text from the error display strings.
352///
353/// It's common for errors with a `source` to have a `Display`
354/// implementation that includes their source text as well:
355///
356/// ```text
357/// Outer error text: Middle error text: Inner error text
358/// ```
359///
360/// This works for smaller errors without much detail, but can be
361/// annoying when trying to format the error in a more structured way,
362/// such as line-by-line:
363///
364/// ```text
365/// 1. Outer error text: Middle error text: Inner error text
366/// 2. Middle error text: Inner error text
367/// 3. Inner error text
368/// ```
369///
370/// This iterator compares each pair of errors in the source chain,
371/// removing the source error's text from the containing error's text:
372///
373/// ```text
374/// 1. Outer error text
375/// 2. Middle error text
376/// 3. Inner error text
377/// ```
378#[cfg(feature = "std")]
379pub struct CleanedErrorText<'a>(Option<CleanedErrorTextStep<'a>>);
380
381#[cfg(feature = "std")]
382impl<'a> CleanedErrorText<'a> {
383    /// Constructs the iterator.
384    pub fn new(error: &'a dyn crate::Error) -> Self {
385        Self(Some(CleanedErrorTextStep::new(error)))
386    }
387}
388
389#[cfg(feature = "std")]
390impl<'a> Iterator for CleanedErrorText<'a> {
391    /// The original error, the display string and if it has been cleaned
392    type Item = (&'a dyn crate::Error, String, bool);
393
394    fn next(&mut self) -> Option<Self::Item> {
395        use std::mem;
396
397        let mut step = self.0.take()?;
398        let mut error_text = mem::replace(&mut step.error_text, Default::default());
399
400        match step.error.source() {
401            Some(next_error) => {
402                let next_error_text = next_error.to_string();
403
404                let cleaned_text = error_text
405                    .trim_end_matches(&next_error_text)
406                    .trim_end()
407                    .trim_end_matches(':');
408                let cleaned = cleaned_text.len() != error_text.len();
409                let cleaned_len = cleaned_text.len();
410                error_text.truncate(cleaned_len);
411
412                self.0 = Some(CleanedErrorTextStep {
413                    error: next_error,
414                    error_text: next_error_text,
415                });
416
417                Some((step.error, error_text, cleaned))
418            }
419            None => Some((step.error, error_text, false)),
420        }
421    }
422}
423
424#[cfg(feature = "std")]
425struct CleanedErrorTextStep<'a> {
426    error: &'a dyn crate::Error,
427    error_text: String,
428}
429
430#[cfg(feature = "std")]
431impl<'a> CleanedErrorTextStep<'a> {
432    fn new(error: &'a dyn crate::Error) -> Self {
433        let error_text = error.to_string();
434        Self { error, error_text }
435    }
436}
437
438#[doc(hidden)]
439pub trait __InternalExtractErrorType {
440    type Err;
441}
442
443impl<T, E> __InternalExtractErrorType for core::result::Result<T, E> {
444    type Err = E;
445}