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}