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