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