Skip to main content

snafu/
report.rs

1use crate::ChainCompat;
2use core::{fmt, panic::Location};
3
4#[cfg(feature = "std")]
5use std::process::{ExitCode, Termination};
6
7#[cfg(feature = "alloc")]
8use alloc::string::{String, ToString};
9
10/// Opinionated solution to format an error in a user-friendly
11/// way. Useful as the return type from `main` and test functions.
12///
13/// Most users will use the [`snafu::report`][] procedural macro
14/// instead of directly using this type. To use it directly, change
15/// the return type of the function to [`Report`][] and wrap the body
16/// of your function with [`Report::capture`][].
17///
18/// [`snafu::report`]: macro@crate::report
19///
20/// ## Nightly Rust
21///
22/// Enabling the [`unstable-try-trait` feature flag][try-ff] will
23/// allow you to use the `?` operator directly:
24///
25/// ```rust
26/// use snafu::{prelude::*, Report};
27///
28/// # #[cfg(feature = "unstable-try-trait")]
29/// fn main() -> Report<PlaceholderError> {
30///     let _v = may_fail_with_placeholder_error()?;
31///
32///     Report::ok()
33/// }
34/// # #[cfg(not(feature = "unstable-try-trait"))] fn main() {}
35/// # #[derive(Debug, Snafu)]
36/// # struct PlaceholderError;
37/// # fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> { Ok(42) }
38/// ```
39///
40/// [try-ff]: crate::guide::feature_flags#unstable-try-trait
41///
42/// ## Interaction with the Provider API
43///
44/// If you return a [`Report`][] from your function and enable the
45/// [`unstable-provider-api` feature flag][provider-ff], additional
46/// capabilities will be added:
47///
48/// 1. If provided, a [`Location`][] will be appended to each error
49///    message.
50/// 1. If provided, a [`Backtrace`][] will be included in the output.
51/// 1. If provided, a [`ExitCode`][] will be used as the return value.
52///
53/// [provider-ff]: crate::guide::feature_flags#unstable-provider-api
54/// [`Location`]: crate::Location
55/// [`Backtrace`]: crate::Backtrace
56/// [`ExitCode`]: std::process::ExitCode
57///
58/// ## Stability of the output
59///
60/// The exact content and format of a displayed `Report` are not
61/// stable, but this type strives to print the error and as much
62/// user-relevant information in an easily-consumable manner
63pub struct Report<E>(Result<(), E>);
64
65impl<E> Report<E> {
66    /// Convert an error into a [`Report`][].
67    ///
68    /// ```rust
69    /// use snafu::{prelude::*, Report};
70    ///
71    /// #[derive(Debug, Snafu)]
72    /// struct PlaceholderError;
73    ///
74    /// fn main() -> Result<(), Report<PlaceholderError>> {
75    ///     let _v = may_fail_with_placeholder_error().map_err(Report::from_error)?;
76    ///     Ok(())
77    /// }
78    ///
79    /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
80    ///     Ok(42)
81    /// }
82    /// ```
83    pub fn from_error(error: E) -> Self {
84        Self(Err(error))
85    }
86
87    /// Executes a closure that returns a [`Result`][], converting any
88    /// error to a [`Report`][].
89    ///
90    /// ```rust
91    /// use snafu::{prelude::*, Report};
92    ///
93    /// #[derive(Debug, Snafu)]
94    /// struct PlaceholderError;
95    ///
96    /// fn main() -> Report<PlaceholderError> {
97    ///     Report::capture(|| {
98    ///         let _v = may_fail_with_placeholder_error()?;
99    ///
100    ///         Ok(())
101    ///     })
102    /// }
103    ///
104    /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
105    ///     Ok(42)
106    /// }
107    /// ```
108    pub fn capture(body: impl FnOnce() -> Result<(), E>) -> Self {
109        Self(body())
110    }
111
112    /// A [`Report`][] that indicates no error occurred.
113    pub const fn ok() -> Self {
114        Self(Ok(()))
115    }
116}
117
118impl<E> From<Result<(), E>> for Report<E> {
119    fn from(other: Result<(), E>) -> Self {
120        Self(other)
121    }
122}
123
124impl<E> fmt::Debug for Report<E>
125where
126    E: crate::Error,
127{
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        fmt::Display::fmt(self, f)
130    }
131}
132
133impl<E> fmt::Display for Report<E>
134where
135    E: crate::Error,
136{
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match &self.0 {
139            Err(e) => fmt::Display::fmt(&ReportFormatter(e), f),
140            _ => Ok(()),
141        }
142    }
143}
144
145#[cfg(feature = "std")]
146impl<E> Termination for Report<E>
147where
148    E: crate::Error,
149{
150    fn report(self) -> ExitCode {
151        match self.0 {
152            Ok(()) => ExitCode::SUCCESS,
153            Err(e) => {
154                std::eprintln!("Error: {}", ReportFormatter(&e));
155
156                #[cfg(feature = "unstable-provider-api")]
157                {
158                    use crate::error;
159
160                    ChainCompat::new(&e)
161                        .find_map(|e| {
162                            error::request_value(e).or_else(|| error::request_ref(e).copied())
163                        })
164                        .unwrap_or(ExitCode::FAILURE)
165                }
166
167                #[cfg(not(feature = "unstable-provider-api"))]
168                {
169                    ExitCode::FAILURE
170                }
171            }
172        }
173    }
174}
175
176fn request_location(e: &dyn crate::Error) -> Option<&Location<'static>> {
177    #[cfg(feature = "unstable-provider-api")]
178    {
179        use crate::error;
180
181        error::request_ref::<&'static Location>(e)
182            .copied()
183            .or_else(|| error::request_ref::<Location>(e))
184            .or_else(|| error::request_value::<&'static Location>(e))
185    }
186
187    #[cfg(not(feature = "unstable-provider-api"))]
188    {
189        let _e = e;
190        None
191    }
192}
193
194#[cfg(feature = "unstable-try-trait")]
195impl<T, E> core::ops::FromResidual<Result<T, E>> for Report<E> {
196    fn from_residual(residual: Result<T, E>) -> Self {
197        Self(residual.map(drop))
198    }
199}
200
201struct ReportFormatter<'a>(&'a dyn crate::Error);
202
203impl<'a> fmt::Display for ReportFormatter<'a> {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        #[cfg(feature = "std")]
206        {
207            if trace_cleaning_enabled() {
208                self.cleaned_error_trace(f)?;
209            } else {
210                self.error_trace(f)?;
211            }
212        }
213
214        #[cfg(not(feature = "std"))]
215        {
216            self.error_trace(f)?;
217        }
218
219        #[cfg(feature = "unstable-provider-api")]
220        {
221            if let Some(bt) = crate::backtraces(self.0).last() {
222                writeln!(f, "\nBacktrace:\n{}", bt)?;
223            }
224        }
225
226        Ok(())
227    }
228}
229
230impl<'a> ReportFormatter<'a> {
231    fn error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
232        writeln!(f, "{}", AddLocation(self.0))?;
233
234        let sources = ChainCompat::new(self.0).skip(1);
235        let plurality = sources.clone().take(2).count();
236
237        match plurality {
238            0 => {}
239            1 => writeln!(f, "\nCaused by this error:")?,
240            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
241        }
242
243        for (i, source) in sources.enumerate() {
244            // Let's use 1-based indexing for presentation
245            let i = i + 1;
246            writeln!(f, "{:3}: {}", i, AddLocation(source))?;
247        }
248
249        Ok(())
250    }
251
252    #[cfg(feature = "std")]
253    fn cleaned_error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
254        use alloc::vec::Vec;
255
256        const NOTE: char = '*';
257
258        let mut any_cleaned = false;
259        let mut any_removed = false;
260        let cleaned_messages: Vec<_> = CleanedErrorText::new(self.0)
261            .flat_map(|(e, mut msg, cleaned)| {
262                if msg.is_empty() {
263                    any_removed = true;
264                    None
265                } else {
266                    if let Some(l) = request_location(e) {
267                        use core::fmt::Write;
268                        write!(msg, " ({})", l).unwrap();
269                    }
270
271                    if cleaned {
272                        any_cleaned = true;
273                        msg.push(' ');
274                        msg.push(NOTE);
275                    }
276                    Some(msg)
277                }
278            })
279            .collect();
280
281        let mut visible_messages = cleaned_messages.iter();
282
283        let head = match visible_messages.next() {
284            Some(v) => v,
285            None => return Ok(()),
286        };
287
288        writeln!(f, "{}", head)?;
289
290        match cleaned_messages.len() {
291            0 | 1 => {}
292            2 => writeln!(f, "\nCaused by this error:")?,
293            _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
294        }
295
296        for (i, msg) in visible_messages.enumerate() {
297            // Let's use 1-based indexing for presentation
298            let i = i + 1;
299            writeln!(f, "{:3}: {}", i, msg)?;
300        }
301
302        if any_cleaned || any_removed {
303            write!(f, "\nNOTE: ")?;
304
305            if any_cleaned {
306                write!(
307                    f,
308                    "Some redundant information has been removed from the lines marked with {}. ",
309                    NOTE,
310                )?;
311            } else {
312                write!(f, "Some redundant information has been removed. ")?;
313            }
314
315            writeln!(
316                f,
317                "Set {}=1 to disable this behavior.",
318                SNAFU_RAW_ERROR_MESSAGES,
319            )?;
320        }
321
322        Ok(())
323    }
324}
325
326struct AddLocation<E>(E);
327
328impl<E: crate::Error> fmt::Display for AddLocation<E> {
329    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
330        write!(f, "{}", self.0)?;
331        if let Some(l) = request_location(&self.0) {
332            write!(f, " ({})", l)?;
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 = "alloc")]
379pub struct CleanedErrorText<'a>(Option<CleanedErrorTextStep<'a>>);
380
381#[cfg(feature = "alloc")]
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 = "alloc")]
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 core::mem;
396
397        let mut step = self.0.take()?;
398        let mut error_text = mem::take(&mut step.error_text);
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 = "alloc")]
425struct CleanedErrorTextStep<'a> {
426    error: &'a dyn crate::Error,
427    error_text: String,
428}
429
430#[cfg(feature = "alloc")]
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}