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