bare_err_tree/
fmt_logic.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7use core::{
8    fmt::{self, Display, Formatter, Write},
9    str::{self, Chars},
10};
11
12use crate::{AsErrTree, ErrTree, ErrTreeDisplay};
13
14impl<E: AsErrTree, const FRONT_MAX: usize> ErrTreeDisplay<E, FRONT_MAX> {
15    pub fn new(tree: E) -> Self {
16        Self(tree)
17    }
18}
19
20impl<E: AsErrTree, const FRONT_MAX: usize> Display for ErrTreeDisplay<E, FRONT_MAX> {
21    #[track_caller]
22    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
23        let mut res = Ok(());
24        self.0.as_err_tree(&mut |tree| {
25            res = fmt_tree::<FRONT_MAX, _, _>(tree, f);
26        });
27        res
28    }
29}
30
31pub(crate) fn fmt_tree<const FRONT_MAX: usize, T, W>(tree: T, f: &mut W) -> fmt::Result
32where
33    T: ErrTreeFormattable,
34    W: fmt::Write + ?Sized,
35{
36    #[cfg(not(feature = "heap_buffer"))]
37    let mut front_lines = [0; FRONT_MAX];
38
39    #[cfg(feature = "heap_buffer")]
40    let mut front_lines = alloc::vec![0; FRONT_MAX].into_boxed_slice();
41
42    #[cfg(all(not(feature = "heap_buffer"), feature = "tracing"))]
43    let mut found_traces: [_; FRONT_MAX] = core::array::from_fn(|_| None);
44
45    #[cfg(all(feature = "heap_buffer", feature = "tracing"))]
46    let mut found_traces = core::iter::repeat_with(|| None)
47        .take(FRONT_MAX)
48        .collect::<alloc::vec::Vec<_>>()
49        .into_boxed_slice();
50
51    ErrTreeFmt::<FRONT_MAX, _> {
52        tree,
53        scratch_fill: 0,
54        front_lines: &mut front_lines,
55
56        #[cfg(feature = "tracing")]
57        found_traces: &mut found_traces,
58    }
59    .fmt(f)
60}
61
62#[cfg(feature = "tracing")]
63pub(crate) struct TraceSpan<T: Eq, CharIter> {
64    pub identifier: T,
65    pub target: CharIter,
66    pub name: CharIter,
67    pub fields: CharIter,
68    pub location: Option<(CharIter, u32)>,
69}
70
71pub(crate) trait ErrTreeFormattable {
72    fn apply_msg<W: fmt::Write>(&self, f: W) -> fmt::Result;
73
74    type Source<'a>: ErrTreeFormattable<TraceSpanId = Self::TraceSpanId>;
75
76    #[allow(unused)]
77    fn sources_empty(&mut self) -> bool;
78
79    fn apply_to_leading_sources<F>(&mut self, func: F) -> fmt::Result
80    where
81        F: FnMut(Self::Source<'_>) -> fmt::Result;
82    fn apply_to_last_source<F>(&mut self, func: F) -> fmt::Result
83    where
84        F: FnMut(Self::Source<'_>) -> fmt::Result;
85    #[cfg(feature = "source_line")]
86    fn has_source_line(&self) -> bool;
87    #[cfg(feature = "source_line")]
88    fn apply_source_line<W: fmt::Write>(&self, f: W) -> fmt::Result;
89
90    #[cfg(feature = "tracing")]
91    fn trace_empty(&self) -> bool;
92
93    type TraceSpanId: Eq;
94    type TraceSpanIter<'a>: IntoIterator<Item = char>;
95
96    #[cfg(feature = "tracing")]
97    fn apply_trace<F>(&self, func: F) -> fmt::Result
98    where
99        F: FnMut(TraceSpan<Self::TraceSpanId, Self::TraceSpanIter<'_>>) -> fmt::Result;
100}
101
102impl<T> ErrTreeFormattable for &mut T
103where
104    T: ErrTreeFormattable,
105{
106    fn apply_msg<W: fmt::Write>(&self, f: W) -> fmt::Result {
107        T::apply_msg(self, f)
108    }
109
110    type Source<'a> = T::Source<'a>;
111    fn sources_empty(&mut self) -> bool {
112        T::sources_empty(self)
113    }
114    fn apply_to_leading_sources<F>(&mut self, func: F) -> fmt::Result
115    where
116        F: FnMut(Self::Source<'_>) -> fmt::Result,
117    {
118        T::apply_to_leading_sources(self, func)
119    }
120    fn apply_to_last_source<F>(&mut self, func: F) -> fmt::Result
121    where
122        F: FnMut(Self::Source<'_>) -> fmt::Result,
123    {
124        T::apply_to_last_source(self, func)
125    }
126
127    #[cfg(feature = "source_line")]
128    fn has_source_line(&self) -> bool {
129        T::has_source_line(self)
130    }
131    #[cfg(feature = "source_line")]
132    fn apply_source_line<W: fmt::Write>(&self, f: W) -> fmt::Result {
133        T::apply_source_line(self, f)
134    }
135
136    #[cfg(feature = "tracing")]
137    fn trace_empty(&self) -> bool {
138        T::trace_empty(self)
139    }
140
141    type TraceSpanId = T::TraceSpanId;
142    type TraceSpanIter<'a> = T::TraceSpanIter<'a>;
143
144    #[cfg(feature = "tracing")]
145    fn apply_trace<F>(&self, func: F) -> fmt::Result
146    where
147        F: FnMut(TraceSpan<Self::TraceSpanId, Self::TraceSpanIter<'_>>) -> fmt::Result,
148    {
149        T::apply_trace(self, func)
150    }
151}
152
153impl ErrTreeFormattable for ErrTree<'_> {
154    fn apply_msg<W: fmt::Write>(&self, mut f: W) -> fmt::Result {
155        write!(f, "{}", self.inner)
156    }
157
158    type Source<'a> = ErrTree<'a>;
159    fn sources_empty(&mut self) -> bool {
160        self.sources.is_empty()
161    }
162    fn apply_to_leading_sources<F>(&mut self, mut func: F) -> fmt::Result
163    where
164        F: FnMut(Self::Source<'_>) -> fmt::Result,
165    {
166        if let Some(initial_slice) = self.sources.take_stored_and_next() {
167            let mut initial_iter = initial_slice.as_slice().iter().cloned();
168            if let Some(mut source) = initial_iter.next() {
169                for next_source in initial_iter.chain(self.sources.by_ref()) {
170                    let mut res = Ok(());
171                    source.as_err_tree(&mut |tree| res = (func)(tree));
172                    res?;
173                    source = next_source
174                }
175            }
176        }
177        Ok(())
178    }
179    fn apply_to_last_source<F>(&mut self, mut func: F) -> fmt::Result
180    where
181        F: FnMut(Self::Source<'_>) -> fmt::Result,
182    {
183        let _ = self.sources.by_ref().last();
184        if let Some(source) = self.sources.take_stored() {
185            let mut res = Ok(());
186            source.as_err_tree(&mut |tree| res = (func)(tree));
187            res?;
188        }
189        Ok(())
190    }
191
192    #[cfg(feature = "source_line")]
193    fn has_source_line(&self) -> bool {
194        self.location.is_some()
195    }
196
197    #[cfg(feature = "source_line")]
198    fn apply_source_line<W: fmt::Write>(&self, mut f: W) -> fmt::Result {
199        if let Some(loc) = self.location {
200            write!(f, "{}", loc)?;
201        }
202        Ok(())
203    }
204
205    #[cfg(feature = "tracing")]
206    fn trace_empty(&self) -> bool {
207        let mut empty = true;
208        if let Some(trace) = &self.trace {
209            trace.with_spans(|_, _| {
210                empty = false;
211                true
212            });
213        }
214        empty
215    }
216
217    #[cfg(not(feature = "tracing"))]
218    type TraceSpanId = ();
219
220    #[cfg(feature = "tracing")]
221    type TraceSpanId = tracing_core::callsite::Identifier;
222
223    type TraceSpanIter<'a> = Chars<'a>;
224
225    #[cfg(feature = "tracing")]
226    fn apply_trace<F>(&self, mut func: F) -> fmt::Result
227    where
228        F: FnMut(TraceSpan<Self::TraceSpanId, Self::TraceSpanIter<'_>>) -> fmt::Result,
229    {
230        if let Some(trace) = &self.trace {
231            let mut res = Ok(());
232            trace.with_spans(|metadata, fields| {
233                res = (func)(TraceSpan {
234                    identifier: metadata.callsite(),
235                    target: metadata.target().chars(),
236                    name: metadata.name().chars(),
237                    fields: fields.chars(),
238                    location: metadata
239                        .file()
240                        .and_then(|file| metadata.line().map(|line| (file.chars(), line))),
241                });
242                res.is_ok()
243            });
244            res
245        } else {
246            Ok(())
247        }
248    }
249}
250
251pub(crate) struct ErrTreeFmt<'a, const FRONT_MAX: usize, T: ErrTreeFormattable> {
252    pub tree: T,
253    pub scratch_fill: usize,
254    /// Most be initialized large enough to fit 6 x (max depth) bytes
255    pub front_lines: &'a mut [u8],
256
257    #[cfg(feature = "tracing")]
258    pub found_traces: &'a mut [Option<T::TraceSpanId>],
259}
260
261/// Workaround for lack of `const` in [`core::cmp::max`].
262#[cfg_attr(coverage, coverage(off))]
263const fn max_const(lhs: usize, rhs: usize) -> usize {
264    if lhs >= rhs {
265        lhs
266    } else {
267        rhs
268    }
269}
270
271const CONTINUING: &str = "│   ";
272const DANGLING: &str = "    ";
273const MAX_CELL_LEN: usize = max_const(CONTINUING.len(), DANGLING.len());
274
275impl<const FRONT_MAX: usize, T: ErrTreeFormattable> ErrTreeFmt<'_, FRONT_MAX, T> {
276    /// The front lines
277    #[inline]
278    fn front_lines_str(front_lines: &[u8], scratch_fill: usize) -> &str {
279        str::from_utf8(&front_lines[..scratch_fill])
280            .expect("All characters are static and guaranteed to be valid UTF-8")
281    }
282
283    /// Preamble arrow connections
284    #[inline]
285    fn write_front_lines<W>(front_lines: &[u8], f: &mut W, scratch_fill: usize) -> fmt::Result
286    where
287        W: fmt::Write + ?Sized,
288    {
289        f.write_char('\n')?;
290        f.write_str(Self::front_lines_str(front_lines, scratch_fill))
291    }
292
293    /// Push in the correct fill characters
294    #[inline]
295    fn add_front_line(front_lines: &mut [u8], last: bool, scratch_fill: usize) {
296        let chars: &str = if last { DANGLING } else { CONTINUING };
297
298        front_lines[scratch_fill..scratch_fill + chars.len()].copy_from_slice(chars.as_bytes());
299    }
300    #[cfg(feature = "tracing")]
301    /// There is tracing after if the trace is nonempty
302    fn tracing_after(&self) -> bool {
303        !self.tree.trace_empty()
304    }
305
306    #[cfg(not(feature = "tracing"))]
307    fn tracing_after(&self) -> bool {
308        false
309    }
310
311    #[cfg(feature = "source_line")]
312    fn source_line<W>(&mut self, f: &mut W, tracing_after: bool) -> fmt::Result
313    where
314        W: fmt::Write + ?Sized,
315    {
316        if self.tree.has_source_line() {
317            Self::write_front_lines(self.front_lines, f, self.scratch_fill)?;
318
319            if !tracing_after && self.tree.sources_empty() {
320                f.write_str("╰─ ")?;
321            } else {
322                f.write_str("├─ ")?;
323            }
324            if cfg!(feature = "unix_color") {
325                f.write_str("at \x1b[3m")?;
326                self.tree.apply_source_line(&mut *f)?;
327                f.write_str("\x1b[0m")?;
328            } else {
329                f.write_str("at ")?;
330                self.tree.apply_source_line(f)?;
331            }
332        }
333
334        Ok(())
335    }
336
337    /// Simple implementation of pretty formatting
338    #[cfg(feature = "tracing")]
339    fn tracing_field_fmt<I, W>(
340        f: &mut W,
341        front_lines: &[u8],
342        fields: I,
343        scratch_fill: usize,
344    ) -> fmt::Result
345    where
346        I: IntoIterator<Item = char>,
347        W: fmt::Write + ?Sized,
348    {
349        let mut depth = 0;
350        let mut in_quote = false;
351
352        const START_CHARS: [char; 3] = ['{', '[', '('];
353        const END_CHARS: [char; 3] = ['}', ']', ')'];
354        const ESC: char = '\\';
355
356        let push_front = |f: &mut W, depth| {
357            Self::write_front_lines(front_lines, f, scratch_fill)?;
358            f.write_str("│    ")?;
359            for _ in 0..depth {
360                f.write_str("  ")?;
361            }
362            Ok(())
363        };
364
365        push_front(f, depth)?;
366        let mut prev = '\0';
367        for c in fields {
368            let mut space_except = false;
369
370            if in_quote {
371                if prev == '"' {
372                    in_quote = false;
373                    if c == ' ' {
374                        space_except = true;
375                    }
376                }
377            } else {
378                match prev {
379                    x if START_CHARS.contains(&x) => {
380                        depth += 1;
381                        push_front(f, depth)?;
382                        if c == ' ' {
383                            space_except = true;
384                        }
385                    }
386                    ',' => {
387                        push_front(f, depth)?;
388                        if c == ' ' {
389                            space_except = true;
390                        }
391                    }
392                    '"' => in_quote = true,
393                    x => {
394                        if END_CHARS.contains(&c) {
395                            depth -= 1;
396                            push_front(f, depth)?;
397                        } else if c == ' ' && END_CHARS.contains(&x) {
398                            space_except = true;
399                            if depth == 0 {
400                                push_front(f, depth)?;
401                            }
402                        }
403                    }
404                }
405            }
406
407            // Special case for escaping
408            prev = if prev == ESC { '\0' } else { c };
409
410            if !space_except {
411                f.write_char(c)?;
412            }
413        }
414
415        Ok(())
416    }
417
418    #[cfg(feature = "tracing")]
419    fn tracing<W>(&mut self, f: &mut W) -> fmt::Result
420    where
421        W: fmt::Write + ?Sized,
422    {
423        if !self.tree.trace_empty() {
424            Self::write_front_lines(self.front_lines, f, self.scratch_fill)?;
425            write!(f, "│")?;
426
427            #[cfg(all(not(feature = "heap_buffer"), feature = "tracing"))]
428            let mut repeated: [_; FRONT_MAX] = core::array::from_fn(|_| None);
429
430            #[cfg(all(feature = "heap_buffer", feature = "tracing"))]
431            let mut repeated = core::iter::repeat_with(|| None)
432                .take(FRONT_MAX)
433                .collect::<alloc::vec::Vec<_>>()
434                .into_boxed_slice();
435
436            let mut repeated_idx = 0;
437
438            self.tree.apply_trace(|trace_span| {
439                let pos_dup = self
440                    .found_traces
441                    .iter()
442                    .take_while(|x| x.is_some())
443                    .flatten()
444                    .position(|c| *c == trace_span.identifier);
445
446                if let Some(pos_dup) = pos_dup {
447                    repeated[repeated_idx] = Some(pos_dup);
448                    repeated_idx += 1;
449                } else {
450                    let depth = self.found_traces.partition_point(|x| x.is_some());
451                    if depth < self.found_traces.len() {
452                        self.found_traces[depth] = Some(trace_span.identifier);
453                    }
454
455                    Self::write_front_lines(self.front_lines, f, self.scratch_fill)?;
456                    write!(f, "├─ tracing frame {} => ", depth)?;
457                    //depth, trace_span.target, trace_span.name
458                    for c in trace_span.target {
459                        f.write_char(c)?
460                    }
461                    f.write_str("::")?;
462                    for c in trace_span.name {
463                        f.write_char(c)?
464                    }
465
466                    let mut fields = trace_span.fields.into_iter().peekable();
467                    if fields.peek().is_some() {
468                        write!(f, " with")?;
469                        Self::tracing_field_fmt(f, self.front_lines, fields, self.scratch_fill)?;
470                    }
471
472                    if let Some((file, line)) = trace_span.location {
473                        Self::write_front_lines(self.front_lines, f, self.scratch_fill)?;
474                        f.write_str("│        at ")?;
475                        for c in file {
476                            f.write_char(c)?
477                        }
478                        f.write_char(':')?;
479                        write!(f, "{line}")?;
480                    };
481                };
482
483                Ok(())
484            })?;
485
486            if repeated_idx > 0 {
487                Self::write_front_lines(self.front_lines, f, self.scratch_fill)?;
488                if self.tree.sources_empty() {
489                    f.write_str("╰─ ")?;
490                } else {
491                    f.write_str("├─ ")?;
492                }
493
494                write!(f, "{} duplicate tracing frame(s): [", repeated_idx)?;
495
496                for idx in 0..repeated_idx - 1 {
497                    write!(f, "{}, ", repeated[idx].expect("Previously set as Some"))?;
498                }
499
500                write!(
501                    f,
502                    "{}]",
503                    repeated[repeated_idx - 1].expect("Previously set as Some")
504                )?;
505            }
506        }
507        Ok(())
508    }
509
510    #[allow(unused_mut)]
511    fn fmt<W>(mut self, f: &mut W) -> fmt::Result
512    where
513        W: fmt::Write + ?Sized,
514    {
515        self.tree.apply_msg(LeadingLineFormatter::new(
516            &mut *f,
517            Self::front_lines_str(self.front_lines, self.scratch_fill),
518        ))?;
519
520        #[cfg_attr(
521            not(any(feature = "source_line", feature = "tracing")),
522            expect(unused_variables, reason = "only used to track for a tracing line")
523        )]
524        let tracing_after = self.tracing_after();
525
526        #[cfg(feature = "source_line")]
527        self.source_line(f, tracing_after)?;
528
529        #[cfg(feature = "tracing")]
530        self.tracing(f)?;
531
532        let mut source_fmt =
533            |front_lines: &mut [u8],
534             scratch_fill: usize,
535             #[cfg(feature = "tracing")] found_traces: &mut [Option<T::TraceSpanId>],
536             source: T::Source<'_>,
537             last: bool| {
538                Self::write_front_lines(front_lines, f, scratch_fill)?;
539                f.write_char('│')?;
540                Self::write_front_lines(front_lines, f, scratch_fill)?;
541
542                if last {
543                    f.write_str("╰─▶ ")?;
544                } else {
545                    f.write_str("├─▶ ")?;
546                }
547
548                let additional_scratch = if last {
549                    DANGLING.len()
550                } else {
551                    CONTINUING.len()
552                };
553
554                ErrTreeFmt::<FRONT_MAX, _> {
555                    tree: source,
556                    scratch_fill: scratch_fill + additional_scratch,
557                    front_lines,
558
559                    #[cfg(feature = "tracing")]
560                    found_traces,
561                }
562                .fmt(f)
563            };
564
565        if self.scratch_fill + MAX_CELL_LEN >= FRONT_MAX {
566            // Stop printing deeper in the stack past this point
567            writeln!(f, "{:.<1$}", "", MAX_CELL_LEN)?;
568        } else {
569            // Normal operation
570
571            Self::add_front_line(self.front_lines, false, self.scratch_fill);
572            self.tree.apply_to_leading_sources(|source| {
573                source_fmt(
574                    self.front_lines,
575                    self.scratch_fill,
576                    #[cfg(feature = "tracing")]
577                    self.found_traces,
578                    source,
579                    false,
580                )
581            })?;
582
583            self.tree.apply_to_last_source(|source| {
584                Self::add_front_line(self.front_lines, true, self.scratch_fill);
585                source_fmt(
586                    self.front_lines,
587                    self.scratch_fill,
588                    #[cfg(feature = "tracing")]
589                    self.found_traces,
590                    source,
591                    true,
592                )
593            })?;
594        };
595
596        Ok(())
597    }
598}
599
600/// Injects the newline leader
601struct LeadingLineFormatter<'a, F> {
602    formatter: F,
603    leading: &'a str,
604}
605
606impl<'a, F> LeadingLineFormatter<'a, F> {
607    pub fn new(formatter: F, leading: &'a str) -> Self {
608        Self { formatter, leading }
609    }
610}
611
612impl<F: Write> Write for LeadingLineFormatter<'_, F> {
613    fn write_str(&mut self, s: &str) -> fmt::Result {
614        if s.chars().all(|c| c != '\n') {
615            self.formatter.write_str(s)?
616        } else {
617            for c in s.chars() {
618                self.write_char(c)?;
619            }
620        }
621        Ok(())
622    }
623
624    fn write_char(&mut self, c: char) -> fmt::Result {
625        self.formatter.write_char(c)?;
626
627        if c == '\n' {
628            self.formatter.write_str(self.leading)?;
629            self.formatter.write_str("│ ")?;
630        }
631
632        Ok(())
633    }
634}