diff --git a/Cargo.toml b/Cargo.toml index a4680b1..56ab1d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,16 @@ [package] name = "how" -version = "0.3.0" +version = "0.4.0" edition = "2021" [features] default = [] backtrace = [] -termination = [] +clone-with-caveats = [] +termination = ["dep:ansee"] [dependencies] +ansee = { git = "https://git.pfaff.dev/michael/ansee", optional = true } [[example]] name = "output" diff --git a/examples/output.rs b/examples/output.rs index 3639ccd..a5e7a86 100644 --- a/examples/output.rs +++ b/examples/output.rs @@ -1,12 +1,18 @@ use how::*; fn main0() -> Result<()> { - Err(How::new("The engine broke down") - .context("While driving down a road".chain(format!("Where the road is {}", "Main Street"))) - .context( - "While driving to a location".chain(format!("Where the location is {}", "the Mall")), - ) - .context(format!("While in the car with {:?}", ["Mom", "Dad"])))? + Err(How::new( + "The engine broke down" + .with("Remember: you aren't good with cars") + .with("Suggestion: call a tow truck"), + ) + .context( + "While driving down a road" + .with(format!("Where the road is {}", "Main Street")) + .with(format!("And the speed is {}", "86 km/h")), + ) + .context("While driving to a location".with(format!("Where the location is {}", "the Mall"))) + .context(format!("While in the car with {:?}", ["Mom", "Dad"])))? } fn main() -> TerminationResult { diff --git a/src/context.rs b/src/context.rs index 41c08bf..949314a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,10 +1,11 @@ use std::borrow::Cow; use std::fmt; +use std::panic::Location; /// Provides context furthering the explanation of *how* you got to an error. #[derive(Debug, Clone)] -pub struct Context(ContextInner); +pub struct Context(pub(crate) ContextInner); impl fmt::Display for Context { #[inline(always)] @@ -14,15 +15,16 @@ impl fmt::Display for Context { } #[derive(Debug, Clone)] -enum ContextInner { +pub(crate) enum ContextInner { Elem(ContextElem), Compound(Vec), } #[derive(Debug, Clone)] -enum ContextElem { +pub(crate) enum ContextElem { Str(&'static str), String(String), + Location(Location<'static>), } impl fmt::Display for ContextInner { @@ -34,7 +36,7 @@ impl fmt::Display for ContextInner { if let Some(elem) = elems.next() { fmt::Display::fmt(elem, f)?; for elem in elems { - write!(f, "\n{elem}")?; + write!(f, "\n- {elem}")?; } } Ok(()) @@ -48,6 +50,7 @@ impl fmt::Display for ContextElem { match self { Self::Str(s) => f.write_str(s), Self::String(s) => f.write_str(s), + Self::Location(l) => write!(f, "At {l}"), } } } @@ -56,11 +59,11 @@ pub trait IntoContext { fn into_context(self) -> Context; #[inline(always)] - fn chain(self, other: impl IntoContext) -> Context + fn with(self, other: impl IntoContext) -> Context where Self: Sized, { - self.into_context().chain(other) + self.into_context().with(other) } } @@ -70,24 +73,24 @@ impl IntoContext for Context { self } - /// Chains another piece of context that is equal from a hierarchical perspective. + /// Chains another piece of context that is a child from a hierarchical perspective. + #[track_caller] #[inline] - fn chain(self, other: impl IntoContext) -> Self { + fn with(self, other: impl IntoContext) -> Self { + let other = other.into_context().0; Context(ContextInner::Compound(match self.0 { ContextInner::Compound(mut elems) => { - match other.into_context().0 { + match other { ContextInner::Elem(elem) => elems.push(elem), ContextInner::Compound(mut elems1) => elems.append(&mut elems1), }; elems } - ContextInner::Elem(elem) => { - match other.into_context().0 { - ContextInner::Elem(elem1) => vec![elem, elem1], - ContextInner::Compound(mut elems) => { - elems.insert(0, elem); - elems - } + ContextInner::Elem(elem) => match other { + ContextInner::Elem(elem1) => vec![elem, elem1], + ContextInner::Compound(mut elems) => { + elems.insert(0, elem); + elems } }, })) @@ -118,6 +121,20 @@ impl IntoContext for Cow<'static, str> { } } +impl<'a> IntoContext for &'a Location<'static> { + #[inline] + fn into_context(self) -> Context { + Location::into_context(*self) + } +} + +impl IntoContext for Location<'static> { + #[inline] + fn into_context(self) -> Context { + Context(ContextInner::Elem(ContextElem::Location(self))) + } +} + impl IntoContext for F where C: IntoContext, diff --git a/src/explain.rs b/src/explain.rs index a79e26c..64d2d26 100644 --- a/src/explain.rs +++ b/src/explain.rs @@ -21,7 +21,10 @@ where #[inline(always)] #[track_caller] fn context(self, context: impl IntoContext) -> Self::Output { - self.into_result_how().map_err(#[inline(never)] move |e| e.context(context)) + self.into_result_how().map_err( + #[inline(never)] + move |e| e.context(context), + ) } } @@ -34,7 +37,7 @@ impl Explain for Option { // TODO: maybe add a feature for the extra "Option::None" context match self { Some(t) => Ok(t), - None => Err(How::new(context)) + None => Err(How::new(context)), } //self.into_result_how().map_err(#[inline(never)] move |e| e.context(context)) } diff --git a/src/how.rs b/src/how.rs index b1fd665..7962381 100644 --- a/src/how.rs +++ b/src/how.rs @@ -1,94 +1,97 @@ +use core::panic::Location; +#[cfg(feature = "backtrace")] +use std::backtrace::BacktraceStatus; + use crate::*; use crate::report::report_write; /// Does not implement [`std::error::Error`] to allow a [`From`] implementation for all other error types. +/// +/// By default, does not implement [`Clone`] because [`std::backtrace::Backtrace`] does not +/// implement [`Clone`]. However, the `clone-with-caveats` feature may be used to enable a +/// [`Clone`] impl that sets the cloned `backtrace` to [`std::backtrace::Backtrace::disabled`]. pub struct How(Box); struct HowInner { - /// When true, the error will cause branchers to abort. - classified: bool, + location: &'static Location<'static>, #[cfg(feature = "backtrace")] - backtrace: std::backtrace::Backtrace, + backtrace: Backtrace, // TODO: consider storing this vec inline (sharing the allocation with rest of the struct. // Probably move after `backtrace`) context: Vec, } +// will be replaced with std::backtrace::Backtrace if and when it is Clone +#[cfg(feature = "backtrace")] +#[derive(Debug)] +enum Backtrace { + Disabled, + Unsupported, + Other(BacktraceStatus, String), +} + impl How { #[must_use] #[inline(never)] #[track_caller] pub fn new(context: impl IntoContext) -> Self { + let location = Location::caller(); Self(Box::new(HowInner { - classified: false, - context: { - let mut vec = Vec::with_capacity(4); - vec.push(format!("At {}", std::panic::Location::caller()).into_context()); - vec.push(context.into_context()); - vec - }, + location, + context: Vec::with_capacity(4), #[cfg(feature = "backtrace")] - backtrace: std::backtrace::Backtrace::capture(), + backtrace: { + let bt = std::backtrace::Backtrace::capture(); + match bt.status() { + BacktraceStatus::Disabled => Backtrace::Disabled, + BacktraceStatus::Unsupported => Backtrace::Unsupported, + status => Backtrace::Other(status, bt.to_string()), + } + }, })) + .context(context) } #[must_use] pub fn clone_without_backtrace(&self) -> Self { Self(Box::new(HowInner { - classified: self.0.classified, + location: self.0.location, context: self.0.context.clone(), #[cfg(feature = "backtrace")] - backtrace: std::backtrace::Backtrace::disabled(), + backtrace: Backtrace::Disabled, })) } - #[inline] - #[must_use] - pub fn classified(mut self) -> Self { - self.0.classified = true; - self - } - - #[inline] - #[must_use] - pub const fn is_classified(&self) -> bool { - self.0.classified - } - - pub fn location(&self) -> &Context { - // SAFETY: we only ever push values into context, and the constructor ensures that there - // are at least 2 values in context. - let o = self.0.context.get(0); - if cfg!(debug_assertions) { - o.unwrap() - } else { - #[allow(unsafe_code)] - unsafe { o.unwrap_unchecked() } - } + pub fn location(&self) -> &'static Location { + self.0.location } pub fn top(&self) -> &Context { // SAFETY: we only ever push values into context, and the constructor ensures that there - // are at least 2 values in context. - let o = self.0.context.get(1); + // is at least 1 value in context. + let o = self.0.context.iter().next(); if cfg!(debug_assertions) { o.unwrap() } else { #[allow(unsafe_code)] - unsafe { o.unwrap_unchecked() } + unsafe { + o.unwrap_unchecked() + } } } pub fn bottom(&self) -> &Context { // SAFETY: we only ever push values into context, and the constructor ensures that there - // are at least 2 values in context. + // is at least 1 value in context. let o = self.0.context.iter().next_back(); if cfg!(debug_assertions) { o.unwrap() } else { #[allow(unsafe_code)] - unsafe { o.unwrap_unchecked() } + unsafe { + o.unwrap_unchecked() + } } } @@ -98,43 +101,81 @@ impl How { fn fmt_debug_alternate(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let mut b = f.debug_struct(std::any::type_name::()); - let b = b - .field("classified", &self.0.classified) - .field("context", &(&self.0.context)); + b.field("location", &(&self.0.location)); + b.field("context", &(&self.0.context)); #[cfg(feature = "backtrace")] let b = b.field("backtrace", &self.0.backtrace); b.finish() } } +#[cfg(feature = "clone-with-caveats")] +impl Clone for How { + #[inline] + fn clone(&self) -> Self { + self.clone_without_backtrace() + } + + fn clone_from(&mut self, source: &Self) { + self.0.location = source.0.location; + self.0.context.clone_from(&source.0.context); + #[cfg(feature = "backtrace")] + { + self.0.backtrace = Backtrace::Disabled; + } + } +} + impl explain::Sealed for How {} impl Explain for How { type Output = Self; #[inline(always)] + #[track_caller] #[must_use] fn context(mut self, context: impl IntoContext) -> Self { - self.0.context.push(context.into_context()); + use context::*; + let mut context = context.into_context(); + let loc = ContextElem::Location(*Location::caller()); + match context.0 { + ContextInner::Elem(elem) => { + context.0 = ContextInner::Compound(vec![elem, loc]); + } + ContextInner::Compound(ref mut vec) => { + vec.insert(1, loc); + } + } + self.0.context.push(context); self } } impl std::fmt::Display for How { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut opts = ReportOpts::default(); - let mut ctxs = self.0.context.iter().rev(); - let ctx = ctxs.next().expect("`How` created with no context."); - report_write!(f, &opts, "{ctx}")?; - for ctx in ctxs { - report_write!(f, &opts, "\n└ ")?; + let opts = ReportOpts::default(); + for (i, ctx) in self.0.context.iter().enumerate() { + if i != 0 { + f.write_str("\n")?; + } report_write!(f, &opts.indent().next(), "{ctx}")?; - opts = opts.indent(); } #[cfg(feature = "backtrace")] { - opts = opts.indent(); - report_write!(f, &opts, "\n{}", self.0.backtrace)?; + use std::backtrace::BacktraceStatus::*; + let bt = &self.0.backtrace; + match bt { + Backtrace::Unsupported => f.write_str("\nI'd like to show you a backtrace,\n but it's not supported on your platform")?, + Backtrace::Disabled => f.write_str("\nIf you'd like a backtrace,\n try again with RUST_BACKTRACE=1")?, + Backtrace::Other(status, bt) => { + f.write_str(if *status == Captured { + "\nHere is the backtrace:" + } else { + "\nI can't tell if backtraces are working,\n but I'll give it a go:" + })?; + report_write!(f, &opts, "\n{}", bt)?; + } + } } Ok(()) } diff --git a/src/into.rs b/src/into.rs index 20fd6c6..7e0d72f 100644 --- a/src/into.rs +++ b/src/into.rs @@ -29,7 +29,7 @@ where } match self { Ok(t) => Ok(t), - Err(e) => Err(into(e)) + Err(e) => Err(into(e)), } } } @@ -56,7 +56,7 @@ impl IntoResultHow for Option { } match self { Some(t) => Ok(t), - None => Err(into()) + None => Err(into()), } } } diff --git a/src/termination.rs b/src/termination.rs index e141810..166bbf5 100644 --- a/src/termination.rs +++ b/src/termination.rs @@ -14,7 +14,11 @@ impl Termination for TerminationResult { Self::Ok => ExitCode::SUCCESS, Self::Err(e) => { use std::io::Write; - let _ = writeln!(std::io::stderr(), "{e}"); + let _ = writeln!( + std::io::stderr(), + concat!(ansee::styled!(bold, italic, "But, how?"), "\n{}"), + e + ); ExitCode::FAILURE }