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}