snafu/report.rs
1use crate::ChainCompat;
2use core::fmt;
3
4#[cfg(all(feature = "std", feature = "rust_1_61"))]
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, but you can if you do not
15/// wish to use the macro.
16///
17/// [`snafu::report`]: macro@crate::report
18///
19/// ## Rust 1.61 and up
20///
21/// Change the return type of the function to [`Report`][] and wrap
22/// the body of your function with [`Report::capture`][].
23///
24/// ## Rust before 1.61
25///
26/// Use [`Report`][] as the error type inside of [`Result`][] and then
27/// call either [`Report::capture_into_result`][] or
28/// [`Report::from_error`][].
29///
30/// ## Nightly Rust
31///
32/// Enabling the [`unstable-try-trait` feature flag][try-ff] will
33/// allow you to use the `?` operator directly:
34///
35/// ```rust
36/// use snafu::{prelude::*, Report};
37///
38/// # #[cfg(all(feature = "unstable-try-trait", feature = "rust_1_61"))]
39/// fn main() -> Report<PlaceholderError> {
40/// let _v = may_fail_with_placeholder_error()?;
41///
42/// Report::ok()
43/// }
44/// # #[cfg(not(all(feature = "unstable-try-trait", feature = "rust_1_61")))] fn main() {}
45/// # #[derive(Debug, Snafu)]
46/// # struct PlaceholderError;
47/// # fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> { Ok(42) }
48/// ```
49///
50/// [try-ff]: crate::guide::feature_flags#unstable-try-trait
51///
52/// ## Interaction with the Provider API
53///
54/// If you return a [`Report`][] from your function and enable the
55/// [`unstable-provider-api` feature flag][provider-ff], additional
56/// capabilities will be added:
57///
58/// 1. If provided, a [`Backtrace`][] will be included in the output.
59/// 1. If provided, a [`ExitCode`][] will be used as the return value.
60///
61/// [provider-ff]: crate::guide::feature_flags#unstable-provider-api
62/// [`Backtrace`]: crate::Backtrace
63/// [`ExitCode`]: std::process::ExitCode
64///
65/// ## Stability of the output
66///
67/// The exact content and format of a displayed `Report` are not
68/// stable, but this type strives to print the error and as much
69/// user-relevant information in an easily-consumable manner
70pub struct Report<E>(Result<(), E>);
71
72impl<E> Report<E> {
73 /// Convert an error into a [`Report`][].
74 ///
75 /// Recommended if you support versions of Rust before 1.61.
76 ///
77 /// ```rust
78 /// use snafu::{prelude::*, Report};
79 ///
80 /// #[derive(Debug, Snafu)]
81 /// struct PlaceholderError;
82 ///
83 /// fn main() -> Result<(), Report<PlaceholderError>> {
84 /// let _v = may_fail_with_placeholder_error().map_err(Report::from_error)?;
85 /// Ok(())
86 /// }
87 ///
88 /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
89 /// Ok(42)
90 /// }
91 /// ```
92 pub fn from_error(error: E) -> Self {
93 Self(Err(error))
94 }
95
96 /// Executes a closure that returns a [`Result`][], converting the
97 /// error variant into a [`Report`][].
98 ///
99 /// Recommended if you support versions of Rust before 1.61.
100 ///
101 /// ```rust
102 /// use snafu::{prelude::*, Report};
103 ///
104 /// #[derive(Debug, Snafu)]
105 /// struct PlaceholderError;
106 ///
107 /// fn main() -> Result<(), Report<PlaceholderError>> {
108 /// Report::capture_into_result(|| {
109 /// let _v = may_fail_with_placeholder_error()?;
110 ///
111 /// Ok(())
112 /// })
113 /// }
114 ///
115 /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
116 /// Ok(42)
117 /// }
118 /// ```
119 pub fn capture_into_result<T>(body: impl FnOnce() -> Result<T, E>) -> Result<T, Self> {
120 body().map_err(Self::from_error)
121 }
122
123 /// Executes a closure that returns a [`Result`][], converting any
124 /// error to a [`Report`][].
125 ///
126 /// Recommended if you only support Rust version 1.61 or above.
127 ///
128 /// ```rust
129 /// use snafu::{prelude::*, Report};
130 ///
131 /// #[derive(Debug, Snafu)]
132 /// struct PlaceholderError;
133 ///
134 /// # #[cfg(feature = "rust_1_61")]
135 /// fn main() -> Report<PlaceholderError> {
136 /// Report::capture(|| {
137 /// let _v = may_fail_with_placeholder_error()?;
138 ///
139 /// Ok(())
140 /// })
141 /// }
142 /// # #[cfg(not(feature = "rust_1_61"))] fn main() {}
143 ///
144 /// fn may_fail_with_placeholder_error() -> Result<u8, PlaceholderError> {
145 /// Ok(42)
146 /// }
147 /// ```
148 pub fn capture(body: impl FnOnce() -> Result<(), E>) -> Self {
149 Self(body())
150 }
151
152 /// A [`Report`][] that indicates no error occurred.
153 pub const fn ok() -> Self {
154 Self(Ok(()))
155 }
156}
157
158impl<E> From<Result<(), E>> for Report<E> {
159 fn from(other: Result<(), E>) -> Self {
160 Self(other)
161 }
162}
163
164impl<E> fmt::Debug for Report<E>
165where
166 E: crate::Error,
167{
168 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169 fmt::Display::fmt(self, f)
170 }
171}
172
173impl<E> fmt::Display for Report<E>
174where
175 E: crate::Error,
176{
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match &self.0 {
179 Err(e) => fmt::Display::fmt(&ReportFormatter(e), f),
180 _ => Ok(()),
181 }
182 }
183}
184
185#[cfg(all(feature = "std", feature = "rust_1_61"))]
186impl<E> Termination for Report<E>
187where
188 E: crate::Error,
189{
190 fn report(self) -> ExitCode {
191 match self.0 {
192 Ok(()) => ExitCode::SUCCESS,
193 Err(e) => {
194 eprintln!("Error: {}", ReportFormatter(&e));
195
196 #[cfg(feature = "unstable-provider-api")]
197 {
198 use crate::error;
199
200 // Broken. https://github.com/rust-lang/rust/pull/114973
201 // error::request_value::<ExitCode>(&e)
202 // .or_else(|| error::request_ref::<ExitCode>(&e).copied())
203
204 error::request_ref::<ExitCode>(&e)
205 .copied()
206 .unwrap_or(ExitCode::FAILURE)
207 }
208
209 #[cfg(not(feature = "unstable-provider-api"))]
210 {
211 ExitCode::FAILURE
212 }
213 }
214 }
215 }
216}
217
218#[cfg(feature = "unstable-try-trait")]
219impl<T, E> core::ops::FromResidual<Result<T, E>> for Report<E> {
220 fn from_residual(residual: Result<T, E>) -> Self {
221 Self(residual.map(drop))
222 }
223}
224
225struct ReportFormatter<'a>(&'a dyn crate::Error);
226
227impl<'a> fmt::Display for ReportFormatter<'a> {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 #[cfg(feature = "std")]
230 {
231 if trace_cleaning_enabled() {
232 self.cleaned_error_trace(f)?;
233 } else {
234 self.error_trace(f)?;
235 }
236 }
237
238 #[cfg(not(feature = "std"))]
239 {
240 self.error_trace(f)?;
241 }
242
243 #[cfg(feature = "unstable-provider-api")]
244 {
245 use crate::error;
246
247 if let Some(bt) = error::request_ref::<crate::Backtrace>(self.0) {
248 writeln!(f, "\nBacktrace:\n{}", bt)?;
249 }
250 }
251
252 Ok(())
253 }
254}
255
256impl<'a> ReportFormatter<'a> {
257 fn error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
258 writeln!(f, "{}", self.0)?;
259
260 let sources = ChainCompat::new(self.0).skip(1);
261 let plurality = sources.clone().take(2).count();
262
263 match plurality {
264 0 => {}
265 1 => writeln!(f, "\nCaused by this error:")?,
266 _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
267 }
268
269 for (i, source) in sources.enumerate() {
270 // Let's use 1-based indexing for presentation
271 let i = i + 1;
272 writeln!(f, "{:3}: {}", i, source)?;
273 }
274
275 Ok(())
276 }
277
278 #[cfg(feature = "std")]
279 fn cleaned_error_trace(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
280 const NOTE: char = '*';
281
282 let mut any_cleaned = false;
283 let mut any_removed = false;
284 let cleaned_messages: Vec<_> = CleanedErrorText::new(self.0)
285 .flat_map(|(_, mut msg, cleaned)| {
286 if msg.is_empty() {
287 any_removed = true;
288 None
289 } else {
290 if cleaned {
291 any_cleaned = true;
292 msg.push(' ');
293 msg.push(NOTE);
294 }
295 Some(msg)
296 }
297 })
298 .collect();
299
300 let mut visible_messages = cleaned_messages.iter();
301
302 let head = match visible_messages.next() {
303 Some(v) => v,
304 None => return Ok(()),
305 };
306
307 writeln!(f, "{}", head)?;
308
309 match cleaned_messages.len() {
310 0 | 1 => {}
311 2 => writeln!(f, "\nCaused by this error:")?,
312 _ => writeln!(f, "\nCaused by these errors (recent errors listed first):")?,
313 }
314
315 for (i, msg) in visible_messages.enumerate() {
316 // Let's use 1-based indexing for presentation
317 let i = i + 1;
318 writeln!(f, "{:3}: {}", i, msg)?;
319 }
320
321 if any_cleaned || any_removed {
322 write!(f, "\nNOTE: ")?;
323
324 if any_cleaned {
325 write!(
326 f,
327 "Some redundant information has been removed from the lines marked with {}. ",
328 NOTE,
329 )?;
330 } else {
331 write!(f, "Some redundant information has been removed. ")?;
332 }
333
334 writeln!(
335 f,
336 "Set {}=1 to disable this behavior.",
337 SNAFU_RAW_ERROR_MESSAGES,
338 )?;
339 }
340
341 Ok(())
342 }
343}
344
345#[cfg(feature = "std")]
346const SNAFU_RAW_ERROR_MESSAGES: &str = "SNAFU_RAW_ERROR_MESSAGES";
347
348#[cfg(feature = "std")]
349fn trace_cleaning_enabled() -> bool {
350 use crate::once_bool::OnceBool;
351 use std::env;
352
353 static DISABLED: OnceBool = OnceBool::new();
354 !DISABLED.get(|| env::var_os(SNAFU_RAW_ERROR_MESSAGES).map_or(false, |v| v == "1"))
355}
356
357/// An iterator over an Error and its sources that removes duplicated
358/// text from the error display strings.
359///
360/// It's common for errors with a `source` to have a `Display`
361/// implementation that includes their source text as well:
362///
363/// ```text
364/// Outer error text: Middle error text: Inner error text
365/// ```
366///
367/// This works for smaller errors without much detail, but can be
368/// annoying when trying to format the error in a more structured way,
369/// such as line-by-line:
370///
371/// ```text
372/// 1. Outer error text: Middle error text: Inner error text
373/// 2. Middle error text: Inner error text
374/// 3. Inner error text
375/// ```
376///
377/// This iterator compares each pair of errors in the source chain,
378/// removing the source error's text from the containing error's text:
379///
380/// ```text
381/// 1. Outer error text
382/// 2. Middle error text
383/// 3. Inner error text
384/// ```
385#[cfg(feature = "alloc")]
386pub struct CleanedErrorText<'a>(Option<CleanedErrorTextStep<'a>>);
387
388#[cfg(feature = "alloc")]
389impl<'a> CleanedErrorText<'a> {
390 /// Constructs the iterator.
391 pub fn new(error: &'a dyn crate::Error) -> Self {
392 Self(Some(CleanedErrorTextStep::new(error)))
393 }
394}
395
396#[cfg(feature = "alloc")]
397impl<'a> Iterator for CleanedErrorText<'a> {
398 /// The original error, the display string and if it has been cleaned
399 type Item = (&'a dyn crate::Error, String, bool);
400
401 fn next(&mut self) -> Option<Self::Item> {
402 use core::mem;
403
404 let mut step = self.0.take()?;
405 let mut error_text = mem::take(&mut step.error_text);
406
407 match step.error.source() {
408 Some(next_error) => {
409 let next_error_text = next_error.to_string();
410
411 let cleaned_text = error_text
412 .trim_end_matches(&next_error_text)
413 .trim_end()
414 .trim_end_matches(':');
415 let cleaned = cleaned_text.len() != error_text.len();
416 let cleaned_len = cleaned_text.len();
417 error_text.truncate(cleaned_len);
418
419 self.0 = Some(CleanedErrorTextStep {
420 error: next_error,
421 error_text: next_error_text,
422 });
423
424 Some((step.error, error_text, cleaned))
425 }
426 None => Some((step.error, error_text, false)),
427 }
428 }
429}
430
431#[cfg(feature = "alloc")]
432struct CleanedErrorTextStep<'a> {
433 error: &'a dyn crate::Error,
434 error_text: String,
435}
436
437#[cfg(feature = "alloc")]
438impl<'a> CleanedErrorTextStep<'a> {
439 fn new(error: &'a dyn crate::Error) -> Self {
440 let error_text = error.to_string();
441 Self { error, error_text }
442 }
443}
444
445#[doc(hidden)]
446pub trait __InternalExtractErrorType {
447 type Err;
448}
449
450impl<T, E> __InternalExtractErrorType for core::result::Result<T, E> {
451 type Err = E;
452}