/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 | | } |