Skip to main content

thiserror_ext/
report.rs

1// This module is ported from https://github.com/shepmaster/snafu and then modified.
2// Below is the original license.
3
4// Copyright 2019- Jake Goulding
5//
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10//     http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18use alloc::format;
19use alloc::string::{String, ToString};
20use alloc::vec::Vec;
21use core::fmt;
22
23/// Extension trait for [`Error`] that provides a [`Report`] which formats
24/// the error and its sources in a cleaned-up way.
25///
26/// [`Error`]: core::error::Error
27pub trait AsReport: crate::error_sealed::Sealed {
28    /// Returns a [`Report`] that formats the error and its sources in a
29    /// cleaned-up way.
30    ///
31    /// See the documentation for [`Report`] for what the formatting looks
32    /// like under different options.
33    ///
34    /// # Example
35    /// ```ignore
36    /// use thiserror_ext::AsReport;
37    ///
38    /// let error = fallible_action().unwrap_err();
39    /// println!("{}", error.as_report());
40    /// ```
41    fn as_report(&self) -> Report<'_>;
42
43    /// Converts the error to a [`Report`] and formats it in a compact way.
44    ///
45    /// This is equivalent to `format!("{}", self.as_report())`.
46    ///
47    /// ## Example
48    /// ```text
49    /// outer error: middle error: inner error
50    /// ```
51    fn to_report_string(&self) -> String {
52        format!("{}", self.as_report())
53    }
54
55    /// Converts the error to a [`Report`] and formats it in a compact way,
56    /// including backtraces if available.
57    ///
58    /// This is equivalent to `format!("{:?}", self.as_report())`.
59    ///
60    /// ## Example
61    /// ```text
62    /// outer error: middle error: inner error
63    ///
64    /// Backtrace:
65    ///   ...
66    /// ```
67    fn to_report_string_with_backtrace(&self) -> String {
68        format!("{:?}", self.as_report())
69    }
70
71    /// Converts the error to a [`Report`] and formats it in a pretty way.
72    ///
73    /// This is equivalent to `format!("{:#}", self.as_report())`.
74    ///
75    /// ## Example
76    /// ```text
77    /// outer error
78    ///
79    /// Caused by these errors (recent errors listed first):
80    ///   1: middle error
81    ///   2: inner error
82    /// ```
83    fn to_report_string_pretty(&self) -> String {
84        format!("{:#}", self.as_report())
85    }
86
87    /// Converts the error to a [`Report`] and formats it in a pretty way,
88    ///
89    /// including backtraces if available.
90    ///
91    /// ## Example
92    /// ```text
93    /// outer error
94    ///
95    /// Caused by these errors (recent errors listed first):
96    ///   1: middle error
97    ///   2: inner error
98    ///
99    /// Backtrace:
100    ///   ...
101    /// ```
102    fn to_report_string_pretty_with_backtrace(&self) -> String {
103        format!("{:#?}", self.as_report())
104    }
105}
106
107impl<T: core::error::Error> AsReport for T {
108    fn as_report(&self) -> Report<'_> {
109        Report(self)
110    }
111}
112
113macro_rules! impl_as_report {
114    ($({$ty:ty },)*) => {
115        $(
116            impl AsReport for $ty {
117                fn as_report(&self) -> Report<'_> {
118                    Report(self)
119                }
120            }
121        )*
122    };
123}
124crate::for_dyn_error_types! { impl_as_report }
125
126/// A wrapper around an error that provides a cleaned up error trace for
127/// display and debug formatting.
128///
129/// Constructed using [`AsReport::as_report`].
130///
131/// # Formatting
132///
133/// The report can be formatted using [`fmt::Display`] or [`fmt::Debug`],
134/// which differs based on the alternate flag (`#`).
135///
136/// - Without the alternate flag, the error is formatted in a compact way:
137///   ```text
138///   Outer error text: Middle error text: Inner error text
139///   ```
140///
141/// - With the alternate flag, the error is formatted in a multi-line
142///   format, which is more readable:
143///   ```text
144///   Outer error text
145///
146///   Caused by these errors (recent errors listed first):
147///     1. Middle error text
148///     2. Inner error text
149///   ```
150///
151/// - Additionally, [`fmt::Debug`] provide backtraces if available.
152///
153/// # Error source cleaning
154///
155/// It's common for errors with a `source` to have a `Display`
156/// implementation that includes their source text as well:
157///
158/// ```text
159/// Outer error text: Middle error text: Inner error text
160/// ```
161///
162/// This works for smaller errors without much detail, but can be
163/// annoying when trying to format the error in a more structured way,
164/// such as line-by-line:
165///
166/// ```text
167/// 1. Outer error text: Middle error text: Inner error text
168/// 2. Middle error text: Inner error text
169/// 3. Inner error text
170/// ```
171///
172/// This iterator compares each pair of errors in the source chain,
173/// removing the source error's text from the containing error's text:
174///
175/// ```text
176/// 1. Outer error text
177/// 2. Middle error text
178/// 3. Inner error text
179/// ```
180pub struct Report<'a>(pub &'a dyn core::error::Error);
181
182impl fmt::Display for Report<'_> {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        self.cleaned_error_trace(f, f.alternate())
185    }
186}
187
188impl fmt::Debug for Report<'_> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        self.cleaned_error_trace(f, f.alternate())?;
191
192        #[cfg(feature = "nightly")]
193        {
194            use std::backtrace::{Backtrace, BacktraceStatus};
195
196            if let Some(bt) = std::error::request_ref::<Backtrace>(self.0) {
197                // Hack for testing purposes.
198                // Read the env var could be slow but we short-circuit it in release mode,
199                // so this should be optimized out in production.
200                let force_show_backtrace = cfg!(debug_assertions)
201                    && std::env::var("THISERROR_EXT_TEST_SHOW_USELESS_BACKTRACE").is_ok();
202
203                // If the backtrace is disabled or unsupported, behave as if there's no backtrace.
204                if bt.status() == BacktraceStatus::Captured || force_show_backtrace {
205                    // The alternate mode contains a trailing newline while non-alternate
206                    // mode does not. So we need to add a newline before the backtrace.
207                    if !f.alternate() {
208                        writeln!(f)?;
209                    }
210                    writeln!(f, "\nBacktrace:\n{}", bt)?;
211                }
212            }
213        }
214
215        Ok(())
216    }
217}
218
219impl Report<'_> {
220    fn cleaned_error_trace(&self, f: &mut fmt::Formatter, pretty: bool) -> Result<(), fmt::Error> {
221        let cleaned_messages: Vec<_> = CleanedErrorText::new(self.0)
222            .flat_map(|(_error, msg, _cleaned)| Some(msg).filter(|msg| !msg.is_empty()))
223            .collect();
224
225        let mut visible_messages = cleaned_messages.iter();
226
227        let head = match visible_messages.next() {
228            Some(v) => v,
229            None => return Ok(()),
230        };
231
232        write!(f, "{}", head)?;
233
234        if pretty {
235            match cleaned_messages.len() {
236                0 | 1 => {}
237                2 => {
238                    writeln!(f, "\n\nCaused by:")?;
239                    writeln!(f, "  {}", visible_messages.next().unwrap())?;
240                }
241                _ => {
242                    writeln!(
243                        f,
244                        "\n\nCaused by these errors (recent errors listed first):"
245                    )?;
246                    for (i, msg) in visible_messages.enumerate() {
247                        // Let's use 1-based indexing for presentation
248                        let i = i + 1;
249                        writeln!(f, "{:3}: {}", i, msg)?;
250                    }
251                }
252            }
253        } else {
254            // No newline at the end.
255            for msg in visible_messages {
256                write!(f, ": {}", msg)?;
257            }
258        }
259
260        Ok(())
261    }
262}
263
264/// An iterator over an Error and its sources that removes duplicated
265/// text from the error display strings.
266struct CleanedErrorText<'a>(Option<CleanedErrorTextStep<'a>>);
267
268impl<'a> CleanedErrorText<'a> {
269    /// Constructs the iterator.
270    fn new(error: &'a dyn core::error::Error) -> Self {
271        Self(Some(CleanedErrorTextStep::new(error)))
272    }
273}
274
275impl<'a> Iterator for CleanedErrorText<'a> {
276    /// The original error, the display string and if it has been cleaned
277    type Item = (&'a dyn core::error::Error, String, bool);
278
279    fn next(&mut self) -> Option<Self::Item> {
280        use core::mem;
281
282        let mut step = self.0.take()?;
283        let mut error_text = mem::take(&mut step.error_text);
284
285        match step.error.source() {
286            Some(next_error) => {
287                let next_error_text = next_error.to_string();
288
289                let cleaned_text = error_text
290                    .trim_end_matches(&next_error_text)
291                    .trim_end()
292                    .trim_end_matches(':');
293                let cleaned = cleaned_text.len() != error_text.len();
294                let cleaned_len = cleaned_text.len();
295                error_text.truncate(cleaned_len);
296
297                self.0 = Some(CleanedErrorTextStep {
298                    error: next_error,
299                    error_text: next_error_text,
300                });
301
302                Some((step.error, error_text, cleaned))
303            }
304            None => Some((step.error, error_text, false)),
305        }
306    }
307}
308
309struct CleanedErrorTextStep<'a> {
310    error: &'a dyn core::error::Error,
311    error_text: String,
312}
313
314impl<'a> CleanedErrorTextStep<'a> {
315    fn new(error: &'a dyn core::error::Error) -> Self {
316        let error_text = error.to_string();
317        Self { error, error_text }
318    }
319}