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!("Error: {}", ReportFormatter(&e));
192
193                #[cfg(feature = "unstable-provider-api")]
194                {
195                    use crate::error;
196
197                    // Broken. https://github.com/rust-lang/rust/pull/114973
198                    // error::request_value::<ExitCode>(&e)
199                    //     .or_else(|| error::request_ref::<ExitCode>(&e).copied())
200
201                    error::request_ref::<ExitCode>(&e)
202                        .copied()
203                        .unwrap_or(ExitCode::FAILURE)
204                }
205
206                #[cfg(not(feature = "unstable-provider-api"))]
207                {
208                    ExitCode::FAILURE
209                }
210            }
211        }
212    }
213}
214
215#[cfg(feature = "unstable-try-trait")]
216impl<T, E> core::ops::FromResidual<Result<T, E>> for Report<E> {
217    fn from_residual(residual: Result<T, E>) -> Self {
218        Self(residual.map(drop))
219    }
220}
221
222struct ReportFormatter<'a>(&'a dyn crate::Error);
223
224impl<'a> fmt::Display for ReportFormatter<'a> {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        #[cfg(feature = "std")]
227        {
228            if trace_cleaning_enabled() {
229                self.cleaned_error_trace(f)?;
230            } else {
231                self.error_trace(f)?;
232            }
233        }
234
235        #[cfg(not(feature = "std"))]
236        {
237            self.error_trace(f)?;
238        }
239
240        #[cfg(feature = "unstable-provider-api")]
241        {
242            use crate::error;
243
244            if let Some(bt) = error::request_ref::<crate::Backtrace>(self.0) {
245                writeln!(f, "\nBacktrace:\n{}", bt)?;
246            }
247        }
248
249        Ok(())
250    }
251}
252
253impl<'a> ReportFormatter<'a> {
254    fn error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
255        writeln!(f, "{}", self.0)?;
256
257        let sources = ChainCompat::new(self.0).skip(1);
258        let plurality = sources.clone().take(2).count();
259
260        match plurality {
261            0 => {}
262            1 => writeln!(f, "\nCaused by this error:")?,
263            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
264        }
265
266        for (i, source) in sources.enumerate() {
267            // Let's use 1-based indexing for presentation
268            let i = i + 1;
269            writeln!(f, "{:3}: {}", i, source)?;
270        }
271
272        Ok(())
273    }
274
275    #[cfg(feature = "std")]
276    fn cleaned_error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
277        const NOTE: char = '*';
278
279        let mut any_cleaned = false;
280        let mut any_removed = false;
281        let cleaned_messages: Vec<_> = CleanedErrorText::new(self.0)
282            .flat_map(|(_, mut msg, cleaned)| {
283                if msg.is_empty() {
284                    any_removed = true;
285                    None
286                } else {
287                    if cleaned {
288                        any_cleaned = true;
289                        msg.push(' ');
290                        msg.push(NOTE);
291                    }
292                    Some(msg)
293                }
294            })
295            .collect();
296
297        let mut visible_messages = cleaned_messages.iter();
298
299        let head = match visible_messages.next() {
300            Some(v) => v,
301            None => return Ok(()),
302        };
303
304        writeln!(f, "{}", head)?;
305
306        match cleaned_messages.len() {
307            0 | 1 => {}
308            2 => writeln!(f, "\nCaused by this error:")?,
309            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
310        }
311
312        for (i, msg) in visible_messages.enumerate() {
313            // Let's use 1-based indexing for presentation
314            let i = i + 1;
315            writeln!(f, "{:3}: {}", i, msg)?;
316        }
317
318        if any_cleaned || any_removed {
319            write!(f, "\nNOTE: ")?;
320
321            if any_cleaned {
322                write!(
323                    f,
324                    "Some redundant information has been removed from the lines marked with {}. ",
325                    NOTE,
326                )?;
327            } else {
328                write!(f, "Some redundant information has been removed. ")?;
329            }
330
331            writeln!(
332                f,
333                "Set {}=1 to disable this behavior.",
334                SNAFU_RAW_ERROR_MESSAGES,
335            )?;
336        }
337
338        Ok(())
339    }
340}
341
342#[cfg(feature = "std")]
343const SNAFU_RAW_ERROR_MESSAGES: &str = "SNAFU_RAW_ERROR_MESSAGES";
344
345#[cfg(feature = "std")]
346fn trace_cleaning_enabled() -> bool {
347    use crate::once_bool::OnceBool;
348    use std::env;
349
350    static DISABLED: OnceBool = OnceBool::new();
351    !DISABLED.get(|| env::var_os(SNAFU_RAW_ERROR_MESSAGES).map_or(false, |v| v == "1"))
352}
353
354/// An iterator over an Error and its sources that removes duplicated
355/// text from the error display strings.
356///
357/// It's common for errors with a `source` to have a `Display`
358/// implementation that includes their source text as well:
359///
360/// ```text
361/// Outer error text: Middle error text: Inner error text
362/// ```
363///
364/// This works for smaller errors without much detail, but can be
365/// annoying when trying to format the error in a more structured way,
366/// such as line-by-line:
367///
368/// ```text
369/// 1. Outer error text: Middle error text: Inner error text
370/// 2. Middle error text: Inner error text
371/// 3. Inner error text
372/// ```
373///
374/// This iterator compares each pair of errors in the source chain,
375/// removing the source error's text from the containing error's text:
376///
377/// ```text
378/// 1. Outer error text
379/// 2. Middle error text
380/// 3. Inner error text
381/// ```
382#[cfg(feature = "std")]
383pub struct CleanedErrorText<'a>(Option<CleanedErrorTextStep<'a>>);
384
385#[cfg(feature = "std")]
386impl<'a> CleanedErrorText<'a> {
387    /// Constructs the iterator.
388    pub fn new(error: &'a dyn crate::Error) -> Self {
389        Self(Some(CleanedErrorTextStep::new(error)))
390    }
391}
392
393#[cfg(feature = "std")]
394impl<'a> Iterator for CleanedErrorText<'a> {
395    /// The original error, the display string and if it has been cleaned
396    type Item = (&'a dyn crate::Error, String, bool);
397
398    fn next(&mut self) -> Option<Self::Item> {
399        use std::mem;
400
401        let mut step = self.0.take()?;
402        let mut error_text = mem::take(&mut step.error_text);
403
404        match step.error.source() {
405            Some(next_error) => {
406                let next_error_text = next_error.to_string();
407
408                let cleaned_text = error_text
409                    .trim_end_matches(&next_error_text)
410                    .trim_end()
411                    .trim_end_matches(':');
412                let cleaned = cleaned_text.len() != error_text.len();
413                let cleaned_len = cleaned_text.len();
414                error_text.truncate(cleaned_len);
415
416                self.0 = Some(CleanedErrorTextStep {
417                    error: next_error,
418                    error_text: next_error_text,
419                });
420
421                Some((step.error, error_text, cleaned))
422            }
423            None => Some((step.error, error_text, false)),
424        }
425    }
426}
427
428#[cfg(feature = "std")]
429struct CleanedErrorTextStep<'a> {
430    error: &'a dyn crate::Error,
431    error_text: String,
432}
433
434#[cfg(feature = "std")]
435impl<'a> CleanedErrorTextStep<'a> {
436    fn new(error: &'a dyn crate::Error) -> Self {
437        let error_text = error.to_string();
438        Self { error, error_text }
439    }
440}
441
442#[doc(hidden)]
443pub trait __InternalExtractErrorType {
444    type Err;
445}
446
447impl<T, E> __InternalExtractErrorType for core::result::Result<T, E> {
448    type Err = E;
449}