error_stack/fmt/mod.rs
1#![expect(deprecated, reason = "We use `Context` to maintain compatibility")]
2
3//! Implementation of formatting, to enable colors and the use of box-drawing characters use the
4//! `pretty-print` feature.
5//!
6//! > **Note:** `error-stack` does not provide any stability guarantees for the [`Debug`] output.
7//!
8//! # Hooks
9//!
10//! The [`Debug`] implementation can be easily extended using hooks. Hooks are functions of the
11//! signature `Fn(&T, &mut HookContext<T>)`, they provide an ergonomic way to partially modify the
12//! output format and enable custom output for types that are not necessarily added via
13//! [`Report::attach`] or are unable to implement [`Display`].
14//!
15//! Hooks can be attached through the central hooking mechanism which `error-stack`
16//! provides via [`Report::install_debug_hook`].
17//!
18//! Hooks are called for contexts which provide additional values through [`Context::provide`] and
19//! attachments which are added via [`Report::attach`] or [`Report::attach_opaque`]. The order of
20//! [`Report::install_debug_hook`] calls determines the order of the rendered output. Note, that
21//! Hooks get called on all values provided by [`Context::provide`], but not on the [`Context`]
22//! object itself. Therefore if you want to call a hook on a [`Context`] to print in addition to its
23//! [`Display`] implementation, you may want to call [`request.provide_ref(self)`] inside of
24//! [`Context::provide`].
25//!
26//! [`request.provide_ref(self)`]: core::error::Request::provide_ref
27//!
28//! Hook functions need to be [`Fn`] and **not** [`FnMut`], which means they are unable to directly
29//! mutate state outside of the closure.
30//! You can still achieve mutable state outside of the scope of your closure through interior
31//! mutability, e.g. by using the [`std::sync`] module like [`Mutex`], [`RwLock`], and [`atomic`]s.
32//!
33//! The type, a hook will be called for, is determined by the type of the first argument to the
34//! closure. This type can either be specified at the closure level or when calling
35//! [`Report::install_debug_hook`].
36//! This type needs to be `'static`, [`Send`], and [`Sync`].
37//!
38//! You can then add additional entries to the body with [`HookContext::push_body`], and entries to
39//! the appendix with [`HookContext::push_appendix`], refer to the documentation of [`HookContext`]
40//! for further information.
41//!
42//! ## Example
43//!
44//! ```rust
45//! # // we only test with nightly, which means that `render()` is unused on earlier version
46//! # #![cfg_attr(not(nightly), allow(dead_code, unused_variables, unused_imports))]
47//! use core::fmt::{Display, Formatter};
48//! use std::io::{Error, ErrorKind};
49//! use error_stack::Report;
50//!
51//! #[derive(Debug)]
52//! struct ErrorCode(u64);
53//!
54//! impl Display for ErrorCode {
55//! fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result {
56//! write!(fmt, "error: {}", self.0)
57//! }
58//! }
59//!
60//! struct Suggestion(&'static str);
61//! struct Warning(&'static str);
62//!
63//! // This hook will never be called, because a later invocation of `install_debug_hook` overwrites
64//! // the hook for the type `ErrorCode`.
65//! Report::install_debug_hook::<ErrorCode>(|_, _| {
66//! unreachable!("will never be called");
67//! });
68//!
69//! // `HookContext` always has a type parameter, which needs to be the same as the type of the
70//! // value, we use `HookContext` here as storage, to store values specific to this hook.
71//! // Here we make use of the auto-incrementing feature.
72//! // The incrementation is type specific, meaning that `context.increment()` for the `Suggestion` hook
73//! // will not influence the counter of the `ErrorCode` or `Warning` hook.
74//! Report::install_debug_hook::<Suggestion>(|Suggestion(value), context| {
75//! let idx = context.increment_counter() + 1;
76//! context.push_body(format!("suggestion {idx}: {value}"));
77//! });
78//!
79//! // Even though we used `attach_printable`, we can still use hooks, `Display` of a type attached
80//! // via `attach_printable` is only ever used when no hook was found.
81//! Report::install_debug_hook::<ErrorCode>(|ErrorCode(value), context| {
82//! context.push_body(format!("error ({value})"));
83//! });
84//!
85//! Report::install_debug_hook::<Warning>(|Warning(value), context| {
86//! let idx = context.increment_counter() + 1;
87//!
88//! // we set a value, which will be removed on non-alternate views
89//! // and is going to be appended to the actual return value.
90//! if context.alternate() {
91//! context.push_appendix(format!("warning {idx}:\n {value}"));
92//! }
93//!
94//! context.push_body(format!("warning ({idx}) occurred"));
95//! });
96//!
97//!
98//! let report = Report::new(Error::from(ErrorKind::InvalidInput))
99//! .attach(ErrorCode(404))
100//! .attach_opaque(Suggestion("try to be connected to the internet."))
101//! .attach_opaque(Suggestion("try better next time!"))
102//! .attach_opaque(Warning("unable to fetch resource"));
103//!
104//! # Report::set_color_mode(error_stack::fmt::ColorMode::Emphasis);
105//! # fn render(value: String) -> String {
106//! # let backtrace = regex::Regex::new(r"backtrace no\. (\d+)\n(?: .*\n)* .*").unwrap();
107//! # let backtrace_info = regex::Regex::new(r"backtrace( with (\d+) frames)? \((\d+)\)").unwrap();
108//! #
109//! # let value = backtrace.replace_all(&value, "backtrace no. $1\n [redacted]");
110//! # let value = backtrace_info.replace_all(value.as_ref(), "backtrace ($3)");
111//! #
112//! # ansi_to_html::convert(value.as_ref()).unwrap()
113//! # }
114//! #
115//! # #[cfg(nightly)]
116//! # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap")].assert_eq(&render(format!("{report:?}")));
117//! #
118//! println!("{report:?}");
119//!
120//! # #[cfg(nightly)]
121//! # expect_test::expect_file![concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap")].assert_eq(&render(format!("{report:#?}")));
122//! #
123//! println!("{report:#?}");
124//! ```
125//!
126//! The output of `println!("{report:?}")`:
127//!
128//! <pre>
129#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt__doc.snap")))]
130//! </pre>
131//!
132//! The output of `println!("{report:#?}")`:
133//!
134//! <pre>
135#![cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/fmt_doc_alt.snap")))]
136//! </pre>
137//!
138//! ## Implementation Details
139//!
140//! Nothing explained here is under any semver guarantee. This section explains the algorithm used
141//! to produce the [`Debug`] output of a [`Report`].
142//!
143//! During the explanation we will make use of two different [`Report`]s, the overview tree (shown
144//! first) only visualizes contexts, while the second, more detailed tree shows attachments and
145//! contexts.
146//!
147//! In the detailed tree the type of [`Frame`] is distinguished using a superscript letter, `ᵃ` is
148//! used to indicate attachments and `ᶜ` is used to indicate contexts. For clarity the overview tree
149//! uses digits, while the detailed tree uses letters for different [`Frame`]s.
150//!
151//! Overview (Context only) Tree:
152//!
153//! ```text
154//! 0
155//! |
156//! 1
157//! / \
158//! 2 6
159//! / \ |
160//! 3 4 7
161//! | |
162//! 5 8
163//! ```
164//!
165//!
166//! Detailed (Context + Attachment) Tree:
167//!
168//! ```text
169//! Aᶜ
170//! |
171//! Bᵃ
172//! / \
173//! Cᵃ Eᵃ
174//! | |
175//! Dᶜ Fᵃ
176//! / \
177//! Gᵃ Iᶜ
178//! |
179//! Hᶜ
180//! ```
181//!
182//! During formatting we distinguish between two cases (for contexts):
183//!
184//! * Lists
185//! * Groups
186//!
187//! in this explanation lists are delimited by `[` and `]`, while groups are delimited by `(` and
188//! `)`.
189//!
190//! While formatting we view the [`Report`]s as a tree of [`Frame`]s, therefore the following
191//! explanation will use terminology associated with trees, every [`Frame`] is a node and can have
192//! `0..n` children, a node that has no children (a leaf) is guaranteed to be a [`Context`].
193//!
194//! A list is a list of nodes where each node in the list is the parent of the next node in the list
195//! and only has a single child. The last node in the list is exempt of that rule of that rule and
196//! can have `0..n` children. In the examples above, `[6, 7, 8]` is considered a list, while `[1,
197//! 6]` is not, because while `1` is a parent of `6`, `1` has more than 1 child.
198//!
199//! A group is a list of nodes where each node shares a common immediate context parent that has
200//! more than `1` child, this means that `(2, 6)` is a group (they share `1` as an immediate context
201//! parent), while `(3, 4, 6)` is not. `(3, 4, 6)` share the same parent with more than 1 child
202//! (`1`), but `1` is not the immediate context parent of `3` and `4` (`2`) is. In the more detailed
203//! example `(Dᶜ, Hᶜ, Iᶜ)` is considered a group because they share the same *immediate* context
204//! parent `Aᶜ`, important to note is that we only refer to immediate context parents, `Fᵃ` is the
205//! immediate parent of `Iᶜ`, but is not a [`Context`], therefore to find the immediate context
206//! parent, we travel up the tree until we encounter our first [`Context`] node. Groups always
207//! contain lists, for the sake of clarity this explanation only shows the first element.
208//!
209//! The rules stated above also implies some additional rules:
210//! * lists are never empty
211//! * lists are nested in groups
212//! * groups are always preceded by a single list
213//! * groups are ordered left to right
214//!
215//! Using the aforementioned delimiters for lists and groups the end result would be:
216//!
217//! Overview Tree: `[0, 1] ([2] ([3], [4, 5]), [6, 7, 8])`
218//! Detailed Tree: `[Aᶜ] ([Dᶜ], [Hᶜ], [Iᶜ])`
219//!
220//! Attachments are not ordered by insertion order but by depth in the tree. The depth in the tree
221//! is the inverse of the insertion order, this means that the [`Debug`] output of all
222//! attachments is reversed from the calling order of [`Report::attach`]. Each context uses the
223//! attachments that are it's parents until the next context node. If attachments are shared between
224//! multiple contexts, they are duplicated and output twice.
225//!
226//! Groups are always preceded by a single list, the only case where this is not true is at the top
227//! level, in that case we opt to output separate trees for each member in the group.
228//!
229//! ### Output Formatting
230//!
231//! Lists are guaranteed to be non-empty and have at least a single context. The context is the
232//! heading of the whole list, while all other contexts are intended. The last entry in that
233//! indentation is (if present) the group that follows, taking the detailed example this means that
234//! the following output would be rendered:
235//!
236//! ```text
237//! Aᶜ
238//! │
239//! ╰┬▶ Dᶜ
240//! │ ├╴Bᵃ
241//! │ ╰╴Cᵃ
242//! │
243//! ├▶ Hᶜ
244//! │ ├╴Bᵃ
245//! │ ├╴Eᵃ
246//! │ ├╴Fᵃ
247//! │ ╰╴Gᵃ
248//! │
249//! ╰▶ Iᶜ
250//! ├╴Bᵃ
251//! ├╴Eᵃ
252//! ╰╴Fᵃ
253//! ```
254//!
255//! Groups are visually represented as an additional distinct indentation for other contexts in the
256//! preceding list, taking the overview tree this means:
257//!
258//! ```text
259//! 0
260//! ├╴Attachment
261//! │
262//! ├─▶ 1
263//! │ ╰╴Attachment
264//! │
265//! ╰┬▶ 2
266//! │ │
267//! │ ╰┬▶ 3
268//! │ │
269//! │ ╰▶ 4
270//! │ │
271//! │ ╰─▶ 5
272//! ╰▶ 6
273//! │
274//! ├─▶ 7
275//! │
276//! ╰─▶ 8
277//! ```
278//!
279//! Attachments have been added to various places to simulate a real use-case with attachments and
280//! to visualise their placement.
281//!
282//! The spacing and characters used are chosen carefully, to reduce indentation and increase visual
283//! legibility in large trees. The indentation of the group following the last entry in the
284//! preceding list is the same. To indicate that the last entry in the preceding list is the parent
285//! a new indentation of the connecting line is used.
286//!
287//! [`Display`]: core::fmt::Display
288//! [`Debug`]: core::fmt::Debug
289//! [`Mutex`]: std::sync::Mutex
290//! [`RwLock`]: std::sync::RwLock
291//! [`Backtrace`]: std::backtrace::Backtrace
292//! [`SpanTrace`]: tracing_error::SpanTrace
293//! [`atomic`]: std::sync::atomic
294//! [`Error::provide`]: core::error::Error::provide
295
296mod charset;
297mod color;
298mod config;
299#[cfg(any(feature = "std", feature = "hooks"))]
300mod hook;
301mod location;
302mod r#override;
303
304use alloc::{
305 borrow::ToOwned,
306 collections::VecDeque,
307 format,
308 string::{String, ToString as _},
309 vec,
310 vec::Vec,
311};
312use core::{
313 fmt::{self, Debug, Display, Formatter},
314 iter::once,
315 mem,
316};
317
318pub use charset::Charset;
319pub use color::ColorMode;
320#[cfg(any(feature = "std", feature = "hooks"))]
321pub use hook::HookContext;
322#[cfg(any(feature = "std", feature = "hooks"))]
323pub(crate) use hook::{Format, Hooks, install_builtin_hooks};
324#[cfg(not(any(feature = "std", feature = "hooks")))]
325use location::LocationAttachment;
326
327use crate::{
328 AttachmentKind, Context, Frame, FrameKind, Report,
329 fmt::{
330 color::{Color, DisplayStyle, Style},
331 config::Config,
332 },
333};
334
335#[derive(Debug, Copy, Clone, Eq, PartialEq)]
336enum Symbol {
337 Vertical,
338 VerticalRight,
339 Horizontal,
340 HorizontalLeft,
341 HorizontalDown,
342 ArrowRight,
343 CurveRight,
344
345 Space,
346}
347
348/// We use symbols during resolution, which is efficient and versatile, but makes the conversion
349/// between [`Instruction`] to [`Symbol`] harder to comprehend for a user.
350///
351/// This macro fixes this by creating a compile-time lookup table to easily map every character in
352/// the [`Display`] of [`Symbol`] to it's corresponding symbol.
353///
354/// # Example
355///
356/// ```ignore
357/// assert_eq!(sym!('├', '┬'), &[
358/// Symbol::VerticalRight,
359/// Symbol::HorizontalDown
360/// ])
361/// ```
362macro_rules! sym {
363 (#char '│') => {
364 Symbol::Vertical
365 };
366
367 (#char '├') => {
368 Symbol::VerticalRight
369 };
370
371 (#char '─') => {
372 Symbol::Horizontal
373 };
374
375 (#char '╴') => {
376 Symbol::HorizontalLeft
377 };
378
379 (#char '┬') => {
380 Symbol::HorizontalDown
381 };
382
383 (#char '▶') => {
384 Symbol::ArrowRight
385 };
386
387 (#char '╰') => {
388 Symbol::CurveRight
389 };
390
391 (#char ' ') => {
392 Symbol::Space
393 };
394
395 ($($char:tt),+) => {
396 &[$(sym!(#char $char)),*]
397 };
398}
399
400impl Symbol {
401 const fn to_str_utf8(self) -> &'static str {
402 match self {
403 Self::Vertical => "\u{2502}", // │
404 Self::VerticalRight => "\u{251c}", // ├
405 Self::Horizontal => "\u{2500}", // ─
406 Self::HorizontalLeft => "\u{2574}", // ╴
407 Self::HorizontalDown => "\u{252c}", // ┬
408 Self::ArrowRight => "\u{25b6}", // ▶
409 Self::CurveRight => "\u{2570}", // ╰
410 Self::Space => " ",
411 }
412 }
413
414 const fn to_str_ascii(self) -> &'static str {
415 match self {
416 Self::Vertical | Self::VerticalRight | Self::CurveRight => "|",
417 Self::Horizontal | Self::HorizontalDown | Self::HorizontalLeft => "-",
418 Self::ArrowRight => ">",
419 Self::Space => " ",
420 }
421 }
422
423 const fn to_str(self, charset: Charset) -> &'static str {
424 match charset {
425 Charset::Utf8 => self.to_str_utf8(),
426 Charset::Ascii => self.to_str_ascii(),
427 }
428 }
429}
430
431struct SymbolDisplay<'a> {
432 inner: &'a [Symbol],
433 charset: Charset,
434}
435
436impl Display for SymbolDisplay<'_> {
437 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
438 for symbol in self.inner {
439 fmt.write_str(symbol.to_str(self.charset))?;
440 }
441
442 Ok(())
443 }
444}
445
446#[derive(Debug, Copy, Clone)]
447enum Position {
448 First,
449 Inner,
450 Final,
451}
452
453#[derive(Debug, Copy, Clone)]
454enum Spacing {
455 // Standard to create a width of 4 characters
456 Full,
457 // Minimal width to create a width of 2/3 characters
458 Minimal,
459}
460
461#[derive(Debug, Copy, Clone)]
462struct Indent {
463 /// Is this used in a group context, if that is the case, then add a single leading space
464 group: bool,
465 /// Should the indent be visible, if that isn't the case it will render a space instead of
466 /// `|`
467 visible: bool,
468 /// Should spacing included, this is the difference between `| ` and `|`
469 spacing: Option<Spacing>,
470}
471
472impl Indent {
473 const fn new(group: bool) -> Self {
474 Self {
475 group,
476 visible: true,
477 spacing: Some(Spacing::Full),
478 }
479 }
480
481 fn spacing(mut self, spacing: impl Into<Option<Spacing>>) -> Self {
482 self.spacing = spacing.into();
483 self
484 }
485
486 const fn visible(mut self, visible: bool) -> Self {
487 self.visible = visible;
488 self
489 }
490
491 const fn group() -> Self {
492 Self::new(true)
493 }
494
495 const fn no_group() -> Self {
496 Self::new(false)
497 }
498
499 const fn prepare(self) -> &'static [Symbol] {
500 match self {
501 Self {
502 group: true,
503 visible: true,
504 spacing: Some(_),
505 } => sym!(' ', '│', ' ', ' '),
506 Self {
507 group: true,
508 visible: true,
509 spacing: None,
510 } => sym!(' ', '│'),
511 Self {
512 group: false,
513 visible: true,
514 spacing: Some(Spacing::Full),
515 } => sym!('│', ' ', ' ', ' '),
516 Self {
517 group: false,
518 visible: true,
519 spacing: Some(Spacing::Minimal),
520 } => sym!('│', ' '),
521 Self {
522 group: false,
523 visible: true,
524 spacing: None,
525 } => sym!('│'),
526 Self {
527 visible: false,
528 spacing: Some(Spacing::Full),
529 ..
530 } => sym!(' ', ' ', ' ', ' '),
531 Self {
532 visible: false,
533 spacing: Some(Spacing::Minimal),
534 ..
535 } => sym!(' ', ' '),
536 Self {
537 visible: false,
538 spacing: None,
539 ..
540 } => &[],
541 }
542 }
543}
544
545impl From<Indent> for Instruction {
546 fn from(indent: Indent) -> Self {
547 Self::Indent(indent)
548 }
549}
550
551/// The display of content is using an instruction style architecture,
552/// where we first render every indentation and action as an [`Instruction`], these instructions
553/// are a lot easier to reason about and enable better manipulation of the stream of data.
554///
555/// Once generation of all data is done, it is interpreted as a String, with glyphs and color added
556/// (if supported and enabled).
557#[derive(Debug)]
558enum Instruction {
559 Value {
560 value: String,
561 style: Style,
562 },
563
564 Group {
565 position: Position,
566 },
567 /// This does not distinguish between first and middle, the true first is handled a bit
568 /// differently.
569 Context {
570 position: Position,
571 },
572 /// `Position::Final` means *that nothing follows*
573 Attachment {
574 position: Position,
575 },
576
577 Indent(Indent),
578}
579
580/// Minimized instruction, which looses information about what it represents and converts it to
581/// only symbols, this then is output instead.
582enum PreparedInstruction<'a> {
583 Symbols(&'a [Symbol]),
584 Content(&'a str, &'a Style),
585}
586
587impl Instruction {
588 #[expect(
589 clippy::match_same_arms,
590 reason = "the match arms are the same intentionally, this makes it more clean which \
591 variant emits which and also keeps it nicely formatted."
592 )]
593 fn prepare(&self) -> PreparedInstruction<'_> {
594 match self {
595 Self::Value { value, style } => PreparedInstruction::Content(value, style),
596
597 Self::Group { position } => match position {
598 Position::First => PreparedInstruction::Symbols(sym!('╰', '┬', '▶', ' ')),
599 Position::Inner => PreparedInstruction::Symbols(sym!(' ', '├', '▶', ' ')),
600 Position::Final => PreparedInstruction::Symbols(sym!(' ', '╰', '▶', ' ')),
601 },
602
603 Self::Context { position } => match position {
604 Position::First => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
605 Position::Inner => PreparedInstruction::Symbols(sym!('├', '─', '▶', ' ')),
606 Position::Final => PreparedInstruction::Symbols(sym!('╰', '─', '▶', ' ')),
607 },
608
609 Self::Attachment { position } => match position {
610 Position::First => PreparedInstruction::Symbols(sym!('├', '╴')),
611 Position::Inner => PreparedInstruction::Symbols(sym!('├', '╴')),
612 Position::Final => PreparedInstruction::Symbols(sym!('╰', '╴')),
613 },
614
615 // Indentation (like `| ` or ` | `)
616 Self::Indent(indent) => PreparedInstruction::Symbols(indent.prepare()),
617 }
618 }
619}
620
621struct InstructionDisplay<'a> {
622 color: ColorMode,
623 charset: Charset,
624
625 instruction: &'a Instruction,
626}
627
628impl Display for InstructionDisplay<'_> {
629 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
630 match self.instruction.prepare() {
631 PreparedInstruction::Symbols(symbols) => {
632 let display = SymbolDisplay {
633 inner: symbols,
634 charset: self.charset,
635 };
636
637 let mut style = Style::new();
638
639 if self.color == ColorMode::Color {
640 style.set_foreground(Color::Red, false);
641 }
642
643 Display::fmt(&style.apply(&display), fmt)?;
644 }
645 PreparedInstruction::Content(value, &style) => Display::fmt(&style.apply(&value), fmt)?,
646 }
647
648 Ok(())
649 }
650}
651
652struct Line(Vec<Instruction>);
653
654impl Line {
655 const fn new() -> Self {
656 Self(Vec::new())
657 }
658
659 fn push(mut self, instruction: Instruction) -> Self {
660 self.0.push(instruction);
661 self
662 }
663
664 fn into_lines(self) -> Lines {
665 let lines = Lines::new();
666 lines.after(self)
667 }
668}
669
670struct LineDisplay<'a> {
671 color: ColorMode,
672 charset: Charset,
673
674 line: &'a Line,
675}
676
677impl Display for LineDisplay<'_> {
678 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
679 for instruction in self.line.0.iter().rev() {
680 Display::fmt(
681 &InstructionDisplay {
682 color: self.color,
683 charset: self.charset,
684 instruction,
685 },
686 fmt,
687 )?;
688 }
689
690 Ok(())
691 }
692}
693
694struct Lines(VecDeque<Line>);
695
696impl Lines {
697 const fn new() -> Self {
698 Self(VecDeque::new())
699 }
700
701 fn into_iter(self) -> alloc::collections::vec_deque::IntoIter<Line> {
702 self.0.into_iter()
703 }
704
705 fn then(mut self, other: Self) -> Self {
706 self.0.extend(other.0);
707 self
708 }
709
710 fn before(mut self, line: Line) -> Self {
711 self.0.push_front(line);
712 self
713 }
714
715 fn after(mut self, line: Line) -> Self {
716 self.0.push_back(line);
717 self
718 }
719
720 fn into_vec(self) -> Vec<Line> {
721 self.0.into_iter().collect()
722 }
723}
724
725impl FromIterator<Line> for Lines {
726 fn from_iter<T: IntoIterator<Item = Line>>(iter: T) -> Self {
727 Self(iter.into_iter().collect())
728 }
729}
730
731/// Collect the current "stack", a stack are the current frames which only have a single
732/// source/parent.
733/// This searches until it finds a stack "split", where a frame has more than a single source.
734fn collect<'a>(root: &'a Frame, prefix: &'a [&Frame]) -> (Vec<&'a Frame>, &'a [Frame]) {
735 let mut stack = vec![];
736 stack.extend(prefix);
737 stack.push(root);
738
739 let mut ptr = Some(root);
740 let mut next: &'a [_] = &[];
741
742 while let Some(current) = ptr.take() {
743 let sources = current.sources();
744
745 match sources {
746 [parent] => {
747 stack.push(parent);
748 ptr = Some(parent);
749 }
750 sources => {
751 next = sources;
752 }
753 }
754 }
755
756 (stack, next)
757}
758
759/// Partition the tree, this looks for the first context frame,
760/// then moves it up the chain and adds it to our results.
761/// Once we reach the end all remaining items on the stack are added to the prefix pile,
762/// which will be used in next iteration.
763fn partition<'a>(stack: &'a [&'a Frame]) -> (Vec<(&'a Frame, Vec<&'a Frame>)>, Vec<&'a Frame>) {
764 let mut result = vec![];
765 let mut queue = vec![];
766
767 for frame in stack {
768 if matches!(frame.kind(), FrameKind::Context(_)) {
769 let frames = mem::take(&mut queue);
770
771 result.push((*frame, frames));
772 } else {
773 queue.push(*frame);
774 }
775 }
776
777 (result, queue)
778}
779
780fn debug_context(context: &dyn Context, mode: ColorMode) -> Lines {
781 context
782 .to_string()
783 .lines()
784 .map(ToOwned::to_owned)
785 .enumerate()
786 .map(|(idx, value)| {
787 if idx == 0 {
788 let mut style = Style::new();
789
790 if mode == ColorMode::Color || mode == ColorMode::Emphasis {
791 style.set_display(DisplayStyle::new().with_bold(true));
792 }
793
794 Line::new().push(Instruction::Value { value, style })
795 } else {
796 Line::new().push(Instruction::Value {
797 value,
798 style: Style::new(),
799 })
800 }
801 })
802 .collect()
803}
804
805struct Opaque(usize);
806
807impl Opaque {
808 const fn new() -> Self {
809 Self(0)
810 }
811
812 const fn increase(&mut self) {
813 self.0 += 1;
814 }
815
816 fn render(self) -> Option<Line> {
817 match self.0 {
818 0 => None,
819 1 => Some(Line::new().push(Instruction::Value {
820 value: "1 additional opaque attachment".to_owned(),
821 style: Style::new(),
822 })),
823 n => Some(Line::new().push(Instruction::Value {
824 value: format!("{n} additional opaque attachments"),
825 style: Style::new(),
826 })),
827 }
828 }
829}
830
831#[cfg_attr(
832 not(any(feature = "std", feature = "hooks")),
833 expect(clippy::needless_pass_by_ref_mut)
834)]
835fn debug_attachments_invoke<'a>(
836 frames: impl IntoIterator<Item = &'a Frame>,
837 config: &mut Config,
838) -> (Opaque, Vec<String>) {
839 let mut opaque = Opaque::new();
840
841 #[cfg(any(feature = "std", feature = "hooks"))]
842 let context = config.context();
843
844 let body = frames
845 .into_iter()
846 .map(|frame| match frame.kind() {
847 #[cfg(any(feature = "std", feature = "hooks"))]
848 FrameKind::Context(_) => Some(
849 if Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context)) {
850 context.take_body()
851 } else {
852 Vec::new()
853 },
854 ),
855 #[cfg(any(feature = "std", feature = "hooks"))]
856 FrameKind::Attachment(AttachmentKind::Printable(attachment)) => Some(
857 if Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context)) {
858 context.take_body()
859 } else {
860 vec![attachment.to_string()]
861 },
862 ),
863 #[cfg(any(feature = "std", feature = "hooks"))]
864 FrameKind::Attachment(AttachmentKind::Opaque(_)) => {
865 Report::invoke_debug_format_hook(|hooks| hooks.call(frame, context))
866 .then(|| context.take_body())
867 }
868 #[cfg(not(any(feature = "std", feature = "hooks")))]
869 FrameKind::Context(_) => Some(vec![]),
870 #[cfg(not(any(feature = "std", feature = "hooks")))]
871 FrameKind::Attachment(AttachmentKind::Printable(attachment)) => {
872 Some(vec![attachment.to_string()])
873 }
874 #[cfg(not(any(feature = "std", feature = "hooks")))]
875 FrameKind::Attachment(AttachmentKind::Opaque(_)) => frame
876 .downcast_ref::<core::panic::Location<'static>>()
877 .map(|location| {
878 vec![LocationAttachment::new(location, config.color_mode()).to_string()]
879 }),
880 })
881 .flat_map(|body| {
882 body.unwrap_or_else(|| {
883 // increase the opaque counter, if we're unable to determine the actual value of
884 // the frame
885 opaque.increase();
886 Vec::new()
887 })
888 })
889 .collect();
890
891 (opaque, body)
892}
893
894fn debug_attachments<'a>(
895 position: Position,
896 frames: impl IntoIterator<Item = &'a Frame>,
897 config: &mut Config,
898) -> Lines {
899 let last = matches!(position, Position::Final);
900
901 let (opaque, entries) = debug_attachments_invoke(frames, config);
902 let opaque = opaque.render();
903
904 // Calculate the expected end length, by adding all values that have would contribute to the
905 // line count later.
906 let len = entries.len() + opaque.as_ref().map_or(0, |_| 1);
907 let lines = entries.into_iter().map(|value| {
908 value
909 .lines()
910 .map(ToOwned::to_owned)
911 .map(|line| {
912 Line::new().push(Instruction::Value {
913 value: line,
914 style: Style::new(),
915 })
916 })
917 .collect::<Vec<_>>()
918 });
919
920 // indentation for every first line, use `Instruction::Attachment`, otherwise use minimal
921 // indent omit that indent when we're the last value
922 lines
923 .chain(opaque.into_iter().map(|line| line.into_lines().into_vec()))
924 .enumerate()
925 .flat_map(|(idx, lines)| {
926 let position = match idx {
927 pos if pos + 1 == len && last => Position::Final,
928 0 => Position::First,
929 _ => Position::Inner,
930 };
931
932 lines.into_iter().enumerate().map(move |(idx, line)| {
933 if idx == 0 {
934 line.push(Instruction::Attachment { position })
935 } else {
936 line.push(
937 Indent::no_group()
938 .visible(!matches!(position, Position::Final))
939 .spacing(Spacing::Minimal)
940 .into(),
941 )
942 }
943 })
944 })
945 .collect()
946}
947
948fn debug_render(head: Lines, contexts: VecDeque<Lines>, sources: Vec<Lines>) -> Lines {
949 let len = sources.len();
950 let sources = sources
951 .into_iter()
952 .enumerate()
953 .map(|(idx, lines)| {
954 let position = match idx {
955 // this is first to make sure that 0 is caught as `Last` instead of `First`
956 pos if pos + 1 == len => Position::Final,
957 0 => Position::First,
958 _ => Position::Inner,
959 };
960
961 lines
962 .into_iter()
963 .enumerate()
964 .map(|(idx, line)| {
965 if idx == 0 {
966 line.push(Instruction::Group { position })
967 } else {
968 line.push(
969 Indent::group()
970 .visible(!matches!(position, Position::Final))
971 .into(),
972 )
973 }
974 })
975 .collect::<Lines>()
976 .before(
977 // add a buffer line for readability
978 Line::new().push(Indent::new(idx != 0).spacing(None).into()),
979 )
980 })
981 .collect::<Vec<_>>();
982
983 let tail = !sources.is_empty();
984 let len = contexts.len();
985
986 // insert the arrows and buffer indentation
987 let contexts = contexts.into_iter().enumerate().flat_map(|(idx, lines)| {
988 let position = match idx {
989 pos if pos + 1 == len && !tail => Position::Final,
990 0 => Position::First,
991 _ => Position::Inner,
992 };
993
994 let mut lines = lines
995 .into_iter()
996 .enumerate()
997 .map(|(idx, line)| {
998 if idx == 0 {
999 line.push(Instruction::Context { position })
1000 } else {
1001 line.push(
1002 Indent::no_group()
1003 .visible(!matches!(position, Position::Final))
1004 .into(),
1005 )
1006 }
1007 })
1008 .collect::<Vec<_>>();
1009
1010 // this is not using `.collect<>().before()`, because somehow that kills rustfmt?!
1011 lines.insert(0, Line::new().push(Indent::no_group().spacing(None).into()));
1012
1013 lines
1014 });
1015
1016 head.into_iter()
1017 .chain(contexts)
1018 .chain(sources.into_iter().flat_map(Lines::into_vec))
1019 .collect()
1020}
1021
1022fn debug_frame(root: &Frame, prefix: &[&Frame], config: &mut Config) -> Vec<Lines> {
1023 let (stack, sources) = collect(root, prefix);
1024 let (stack, prefix) = partition(&stack);
1025
1026 let len = stack.len();
1027 // collect all the contexts that we have partitioned previously and render them
1028 let mut contexts: VecDeque<_> = stack
1029 .into_iter()
1030 .enumerate()
1031 .map(|(idx, (head, mut body))| {
1032 // each "paket" on the stack is made up of a head (guaranteed to be a `Context`) and
1033 // `n` attachments.
1034 // The attachments are rendered as direct descendants of the parent context
1035 let head_context = debug_context(
1036 match head.kind() {
1037 FrameKind::Context(context) => context,
1038 FrameKind::Attachment(_) => unreachable!(),
1039 },
1040 config.color_mode(),
1041 );
1042
1043 // reverse all attachments, to make it more logical relative to the attachment order
1044 body.reverse();
1045 let body = debug_attachments(
1046 // This makes sure that we use `╰─` instead of `├─`,
1047 // this is true whenever we only have a single context and no sources,
1048 // **or** if our idx is larger than `0`, this might sound false,
1049 // but this is because contexts other than the first context create a new
1050 // "indentation", in this indentation we are considered last.
1051 //
1052 // Context A
1053 // ├╴Attachment B
1054 // ├╴Attachment C <- not last, because we are not the only context
1055 // |
1056 // ╰─▶ Context D <- indentation here is handled by `debug_render`!
1057 // ├╴Attachment E
1058 // ╰╴Attachment F <- last because it's the last of the parent context!
1059 if (len == 1 && sources.is_empty()) || idx > 0 {
1060 Position::Final
1061 } else {
1062 Position::Inner
1063 },
1064 once(head).chain(body),
1065 config,
1066 );
1067 head_context.then(body)
1068 })
1069 .collect();
1070
1071 let sources = sources
1072 .iter()
1073 .flat_map(
1074 // if the group is "transparent" (has no context), it will return all it's parents
1075 // rendered this is why we must first flat_map.
1076 |source| debug_frame(source, &prefix, config),
1077 )
1078 .collect::<Vec<_>>();
1079
1080 // if there is no context, this is considered a "transparent" group,
1081 // and just directly returns all sources without modifying them
1082 if contexts.is_empty() {
1083 return sources;
1084 }
1085
1086 // take the first Context, this is our "root", all others are indented
1087 // this unwrap always succeeds due to the call before <3
1088 let head = contexts
1089 .pop_front()
1090 .expect("should always have single context");
1091
1092 // combine everything into a single group
1093 vec![debug_render(head, contexts, sources)]
1094}
1095
1096impl<C: ?Sized> Debug for Report<C> {
1097 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
1098 let mut config = Config::load(fmt.alternate());
1099
1100 let color = config.color_mode();
1101 let charset = config.charset();
1102
1103 #[cfg_attr(not(any(feature = "std", feature = "hooks")), expect(unused_mut))]
1104 let mut lines = self
1105 .current_frames_unchecked()
1106 .iter()
1107 .flat_map(|frame| debug_frame(frame, &[], &mut config))
1108 .enumerate()
1109 .flat_map(|(idx, lines)| {
1110 if idx == 0 {
1111 lines.into_vec()
1112 } else {
1113 lines
1114 .before(
1115 Line::new()
1116 .push(Indent::no_group().visible(false).spacing(None).into()),
1117 )
1118 .into_vec()
1119 }
1120 })
1121 .map(|line| {
1122 LineDisplay {
1123 color,
1124 charset,
1125 line: &line,
1126 }
1127 .to_string()
1128 })
1129 .collect::<Vec<_>>()
1130 .join("\n");
1131
1132 #[cfg(any(feature = "std", feature = "hooks"))]
1133 {
1134 let appendix = config
1135 .context::<Frame>()
1136 .appendix()
1137 .iter()
1138 .map(
1139 // remove all trailing newlines for a more uniform look
1140 |snippet| snippet.trim_end_matches('\n').to_owned(),
1141 )
1142 .collect::<Vec<_>>()
1143 .join("\n\n");
1144
1145 if !appendix.is_empty() {
1146 // 44 is the size for the separation.
1147 lines.reserve(44 + appendix.len());
1148
1149 lines.push_str("\n\n");
1150 if charset == Charset::Utf8 {
1151 lines.push_str(&"\u{2501}".repeat(40)); // ━
1152 } else {
1153 lines.push_str(&"=".repeat(40));
1154 }
1155
1156 lines.push_str("\n\n");
1157 lines.push_str(&appendix);
1158 }
1159 }
1160
1161 fmt.write_str(&lines)
1162 }
1163}
1164
1165impl<C: ?Sized> Display for Report<C> {
1166 fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result {
1167 for (index, frame) in self
1168 .frames()
1169 .filter_map(|frame| match frame.kind() {
1170 FrameKind::Context(context) => Some(context.to_string()),
1171 FrameKind::Attachment(_) => None,
1172 })
1173 .enumerate()
1174 {
1175 if index == 0 {
1176 fmt::Display::fmt(&frame, fmt)?;
1177 if !fmt.alternate() {
1178 break;
1179 }
1180 } else {
1181 write!(fmt, ": {frame}")?;
1182 }
1183 }
1184
1185 Ok(())
1186 }
1187}