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}