Coverage Report

Created: 2025-02-07 03:09

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/bare_err_tree/bare_err_tree/bare_err_tree/src/lib.rs
Line
Count
Source
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
7
/*!
8
`bare_err_tree` is a `no_std` and no `alloc` library to print an [`Error`] with a tree of sources.
9
10
Support for the extra information prints does not change the type or public
11
API (besides a hidden field or deref). It is added via macro or manual
12
implementation of the [`AsErrTree`] trait. End users can then use
13
[`ErrTreeDisplay`] or [`tree_unwrap`] to get better error output, or store as
14
JSON for later reconstruction.
15
16
If none of the [tracking feature flags](#tracking-feature-flags) are enabled,
17
the metadata is set to the [`unit`] type to take zero space.
18
If the print methods are never called, and none of the tracking features are
19
enabled, this library incurs zero runtime cost.
20
Usage of the [`err_tree`] macro incurs a compliation time cost.
21
22
# Feature Flags
23
* `derive`: Enabled by default, provides [`err_tree`] via proc macro.
24
* `json`: Allows for storage to/reconstruction from JSON.
25
* `heap_buffer`: Uses heap to store so state that `FRONT_MAX` (x3 if tracing
26
    is enabled) bytes of the stack aren't statically allocated for this purpose.
27
* `boxed`: Boxes the error package. Addresses ballooning from large tracking
28
    features. Boxing the error itself is likely more efficient, when available.
29
* `unix_color`: Outputs UNIX console codes for emphasis.
30
* `anyhow`: Adds implementation for [`anyhow::Error`].
31
* `eyre`: Adds implementation for [`eyre::Report`].
32
#### Tracking Feature Flags
33
* `source_line`: Tracks the source line of tree errors.
34
* `tracing`: Produces a `tracing` backtrace with [`tracing_error`].
35
36
# Adding [`ErrTree`] Support (Library or Bin)
37
Both libraries and binaries can add type support for [`ErrTree`] prints.
38
The [`err_tree`] macro is recommended, but [`ErrTree`] allows for a manual
39
implementation.
40
41
#### Feature Flags in Libraries
42
Libraries should NOT enable any of the
43
[tracking feature flags](#tracking-feature-flags) by default. Those are tunable
44
for a particular binary's environment and needs. [`ErrTreeDisplay`]/[`tree_unwrap`]
45
should be used sparingly within the library, ideally with a small `FRONT_MAX`
46
to minimize out of stack memory errors.
47
48
# Using [`AsErrTree`] Implementors (Bin)
49
Specify desired tracking features by importing `bare_err_tree` in `Cargo.toml`.
50
(e.g. `bare_err_tree = { version = "*", features = ["source_line"] }`)
51
52
Call [`tree_unwrap`] on the [`Result`] or [`ErrTreeDisplay`] on the [`Error`]
53
with `FRONT_MAX` set to `6 * (maximum tree depth)`. Note that unless
54
`heap_buffer` is enabled, `FRONT_MAX` (x3 if `tracing` is enabled) bytes will be
55
occupied on stack for the duration of a print call. Make sure this falls
56
within platform stack size, and single stack frame size, limits.
57
58
# Credit
59
60
The formatting is borrowed from from [error-stack](https://crates.io/crates/error-stack).
61
Please see the [contributors page](https://github.com/hashintel/hash/graphs/contributors) for appropriate credit.
62
63
# Licensing and Contributing
64
65
All code is licensed under MPL 2.0. See the [FAQ](https://www.mozilla.org/en-US/MPL/2.0/FAQ/)
66
for license questions. The license is non-viral copyleft and does not block this library from
67
being used in closed-source codebases. If you are using this library for a commercial purpose,
68
consider reaching out to `dansecob.dev@gmail.com` to make a financial contribution.
69
70
Contributions are welcome at
71
<https://github.com/Bennett-Petzold/bare_err_tree>.
72
*/
73
74
#![no_std]
75
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
76
#![cfg_attr(coverage, feature(coverage_attribute))]
77
78
#[cfg(any(feature = "heap_buffer", feature = "boxed"))]
79
extern crate alloc;
80
81
#[cfg(feature = "source_line")]
82
use core::panic::Location;
83
84
use core::error::Error;
85
86
mod pkg;
87
pub use pkg::*;
88
pub mod flex;
89
pub use flex::*;
90
mod buffer;
91
mod fmt_logic;
92
use buffer::*;
93
94
#[cfg(feature = "json")]
95
mod json;
96
#[cfg(feature = "json")]
97
pub use json::*;
98
99
#[cfg(feature = "derive")]
100
pub use bare_err_tree_proc::*;
101
102
/// Alternative to [`Result::unwrap`] that formats the error as a tree.
103
///
104
/// `FRONT_MAX` limits the number of leading bytes. Each deeper error requires 6
105
/// bytes to fit "│   ". So for a max depth of 3 errors, `FRONT_MAX` == 18.
106
/// By default, `FRONT_MAX` bytes are allocated on stack. When `heap_buffer` is
107
/// enabled, the bytes are allocated on heap and `FRONT_MAX` only acts as a
108
/// depth limit. When `tracing` is enabled, at most `FRONT_MAX` stack traces
109
/// will be tracked for duplicates.
110
///
111
/// Errors must define [`Error::source`] correctly for the tree to display.
112
/// The derive macros for [`ErrTree`] track extra information and handle
113
/// multiple sources ([`Error::source`] is designed around a single error
114
/// source).
115
#[track_caller]
116
1
pub fn tree_unwrap<const FRONT_MAX: usize, T, E>(res: Result<T, E>) -> T
117
1
where
118
1
    E: AsErrTree,
119
1
{
120
1
    match res {
121
0
        Ok(x) => x,
122
1
        Err(tree) => {
123
1
            panic!(
124
1
                "Panic origin at: {:#?}\n{}",
125
1
                core::panic::Location::caller(),
126
1
                ErrTreeDisplay::<_, FRONT_MAX>::new(tree)
127
1
            );
128
        }
129
    }
130
0
}
131
132
/// Produces [`ErrTree`] formatted output for an error.
133
///
134
/// `FRONT_MAX` limits the number of leading bytes. Each deeper error requires 6
135
/// bytes to fit "│   ". So for a max depth of 3 errors, `FRONT_MAX` == 18.
136
/// By default, `FRONT_MAX` bytes are allocated on stack. When `heap_buffer` is
137
/// enabled, the bytes are allocated on stack and `FRONT_MAX` only acts as a
138
/// depth limit. When `tracing` is enabled, at most `FRONT_MAX` stack traces
139
/// will be tracked for duplicates.
140
///
141
/// Errors must define [`Error::source`] correctly for the tree to display.
142
/// The derive macros for [`ErrTree`] track extra information and handle
143
/// multiple sources ([`Error::source`] is designed around a single error
144
/// source).
145
///
146
/// ```rust
147
1
/// # use std::{
148
/// #   panic::Location,
149
/// #   error::Error,
150
/// #   fmt::{self, Write, Display, Formatter},
151
/// #   string::String,
152
/// #   io::self,
153
/// # };
154
/// use bare_err_tree::{ErrTreeDisplay};
155
///
156
/// println!("{}", ErrTreeDisplay::<_, 60>(&io::Error::last_os_error() as &dyn Error));
157
1
/// ```
158
1
pub struct ErrTreeDisplay<E: AsErrTree, const FRONT_MAX: usize = 60>(pub E);
159
160
/// Intermediate struct for printing created by [`AsErrTree`].
161
///
162
/// Only allowing construction through [`Self::with_pkg`] and [`Self::no_pkg`]
163
/// allows arbitrary combinations of metadata tracking without changing
164
/// construction syntax. Sources are stored under three layers of indirection
165
/// to allow for maximum type and size flexibility without generics or heap
166
/// allocation.
167
///
168
/// See [`tree`] to reduce [`Self::with_pkg`] boilerplate.
169
///
170
/// # Manual Implementation Example
171
/// ```
172
/// # #![cfg_attr(coverage, feature(coverage_attribute))]
173
1
/// # use std::{
174
/// #   panic::Location,
175
/// #   error::Error,
176
/// #   fmt::{Display, Formatter},
177
/// # };
178
/// use bare_err_tree::{ErrTree, ErrTreePkg, AsErrTree, WrapErr};
179
///
180
/// #[derive(Debug)]
181
/// pub struct HighLevelIo {
182
///     source: std::io::Error,
183
///     _pkg: ErrTreePkg,
184
/// }
185
///
186
/// impl HighLevelIo {
187
/// #   #[cfg_attr(coverage, coverage(off))]
188
///     #[track_caller]
189
///     pub fn new(source: std::io::Error) -> Self {
190
///         Self {
191
///             source,
192
///             _pkg: ErrTreePkg::new(),
193
///         }
194
///     }
195
/// }
196
///
197
/// impl AsErrTree for HighLevelIo {
198
/// #   #[cfg_attr(coverage, coverage(off))]
199
///     fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
200
///         // Cast to ErrTree adapter via Error
201
///         let source = WrapErr::tree(&self.source);
202
///         // Convert to a single item iterator
203
///         let source_iter = &mut core::iter::once(source);
204
///
205
///         // Call the formatting function
206
///         (func)(ErrTree::with_pkg(self, source_iter, &self._pkg));
207
///     }
208
/// }
209
///
210
/// impl Error for HighLevelIo {
211
/// #   #[cfg_attr(coverage, coverage(off))]
212
///     fn source(&self) -> Option<&(dyn Error + 'static)> {
213
///         Some(&self.source)
214
///     }
215
/// }
216
/// impl Display for HighLevelIo {
217
///     # /*
218
///     ...
219
///     # */
220
/// #   #[cfg_attr(coverage, coverage(off))]
221
///     # fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
222
///         # write!(f, "High level IO error!")
223
///     # }
224
/// }
225
/// ```
226
1
pub struct ErrTree<'a> {
227
    inner: &'a dyn Error,
228
    sources: IterBuffer<&'a mut dyn Iterator<Item = &'a dyn AsErrTree>>,
229
    #[cfg(feature = "source_line")]
230
    location: Option<&'a Location<'a>>,
231
    #[cfg(feature = "tracing")]
232
    trace: Option<&'a tracing_error::SpanTrace>,
233
}
234
235
impl<'a> ErrTree<'a> {
236
    /// Common constructor, with metadata.
237
2
    pub fn with_pkg(
238
2
        inner: &'a dyn Error,
239
2
        sources: &'a mut dyn Iterator<Item = &'a dyn AsErrTree>,
240
2
        #[allow(unused)] pkg: &'a ErrTreePkg,
241
2
    ) -> Self {
242
2
        Self {
243
2
            inner,
244
2
            sources: sources.into(),
245
2
            #[cfg(feature = "source_line")]
246
2
            location: Some(pkg.location()),
247
2
            #[cfg(feature = "tracing")]
248
2
            trace: Some(pkg.trace()),
249
2
        }
250
2
    }
251
252
    /// Constructor for when metadata needs to be hidden.
253
10
    pub fn no_pkg(
254
10
        inner: &'a dyn Error,
255
10
        sources: &'a mut dyn Iterator<Item = &'a dyn AsErrTree>,
256
10
    ) -> Self {
257
10
        Self {
258
10
            inner,
259
10
            sources: sources.into(),
260
10
            #[cfg(feature = "source_line")]
261
10
            location: None,
262
10
            #[cfg(feature = "tracing")]
263
10
            trace: None,
264
10
        }
265
10
    }
266
267
    /// Consumes this tree to return its sources
268
1
    pub fn sources(self) -> impl Iterator<Item = &'a dyn AsErrTree> {
269
1
        self.sources
270
1
    }
271
}
272
273
/// Defines an [`Error`]'s temporary view as an [`ErrTree`] for printing.
274
///
275
/// This can be defined with [`err_tree`], manually (see [`ErrTree`]), or with
276
/// the default `dyn` implementation. The `dyn` implementation does not track
277
/// any more information than standard library errors or track multiple sources.
278
///
279
/// Implementors must call `func` with a properly constructed [`ErrTree`].
280
pub trait AsErrTree {
281
    /// Constructs the [`ErrTree`] internally and calls `func` on it.
282
    fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>));
283
}
284
285
/// Displays with [`Error::source`] as the child.
286
///
287
/// Does not provide any of the extra tracking information or handle multiple
288
/// sources.
289
impl AsErrTree for dyn Error {
290
4
    fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
291
4
        match self.source() {
292
1
            Some(e) => (func)(ErrTree::no_pkg(
293
1
                self,
294
1
                &mut core::iter::once(&e as &dyn AsErrTree),
295
1
            )),
296
3
            None => (func)(ErrTree::no_pkg(self, &mut core::iter::empty())),
297
        }
298
4
    }
299
}
300
301
#[cfg(feature = "anyhow")]
302
impl AsErrTree for anyhow::Error {
303
    fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
304
        let this: &dyn Error = self.as_ref();
305
        this.as_err_tree(func)
306
    }
307
}
308
309
#[cfg(feature = "eyre")]
310
impl AsErrTree for eyre::Report {
311
    fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
312
        let this: &dyn Error = self.as_ref();
313
        this.as_err_tree(func)
314
    }
315
}
316
317
impl<T: ?Sized + AsErrTree> AsErrTree for &T {
318
4
    fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
319
4
        T::as_err_tree(self, func)
320
4
    }
321
}
322
323
/// Boilerplate reducer for manual [`ErrTree`].
324
///
325
/// Expands out to [`ErrTree::with_pkg`] with `$x` as source(s).
326
/// Preface with `dyn` to use the generic `dyn` [`Error`] rendering.
327
///
328
/// ```
329
/// # #![cfg_attr(coverage, feature(coverage_attribute))]
330
1
/// # use std::{
331
/// #   panic::Location,
332
/// #   error::Error,
333
/// #   fmt::{Display, Formatter},
334
/// # };
335
/// use bare_err_tree::{tree, ErrTree, ErrTreePkg, AsErrTree};
336
///
337
/// #[derive(Debug)]
338
/// struct Foo(std::io::Error, ErrTreePkg);
339
///
340
/// impl AsErrTree for Foo {
341
/// #   #[cfg_attr(coverage, coverage(off))]
342
///     fn as_err_tree(&self, func: &mut dyn FnMut(ErrTree<'_>)) {
343
///         // Equivalent to:
344
///         // (func)(bare_err_tree::ErrTree::with_pkg(
345
///         //     &self,
346
///         //     core::iter::once(bare_err_tree::ErrTreeConv::from(&self.0 as &dyn Error))
347
///         //     self.1
348
///         // )
349
///         tree!(dyn, func, self, self.1, &self.0)
350
///     }
351
/// }
352
///
353
/// impl Error for Foo {
354
/// #   #[cfg_attr(coverage, coverage(off))]
355
///     fn source(&self) -> Option<&(dyn Error + 'static)> {
356
///         Some(&self.0)
357
///     }
358
/// }
359
/// impl Display for Foo {
360
///     # /*
361
///     ...
362
///     # */
363
/// #   #[cfg_attr(coverage, coverage(off))]
364
///     # fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
365
///         # write!(f, "")
366
///     # }
367
/// }
368
/// ```
369
1
#[macro_export]
370
macro_rules! tree {
371
    (dyn, $func:expr, $inner:expr, $pkg:expr, $( $x:expr ),* ) => {
372
        ($func)(bare_err_tree::ErrTree::with_pkg(
373
            &$inner,
374
            &mut core::iter::empty()$( .chain(
375
                core::iter::once(
376
                    bare_err_tree::WrapErr::tree(&$x)
377
                )
378
            ) )*,
379
            &$pkg,
380
        ))
381
    };
382
    ($func:expr, $inner:expr, $pkg:expr, $( $x:expr ),* ) => {
383
        ($func)(bare_err_tree::ErrTree::with_pkg(
384
            &$inner,
385
            &mut core::iter::empty()$( .chain(
386
                core::iter::once($x)
387
            ) )*,
388
            &$pkg,
389
        ))
390
    };
391
}