diff --git a/Cargo.toml b/Cargo.toml index 18a3d33f..276117d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,11 +25,14 @@ exclude = [ features = [ "std", "backtraces", "futures", "guide" ] [features] -default = ["std"] +default = ["std", "rust_1_46"] # Implement the `std::error::Error` trait. std = [] +# Add support for `#[track_caller]` +rust_1_46 = ["snafu-derive/rust_1_46"] + # Makes the backtrace type live backtraces = ["std", "backtrace"] diff --git a/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.rs b/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.rs index fbf1af82..ce233fa7 100644 --- a/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.rs +++ b/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.rs @@ -22,4 +22,16 @@ mod backtrace { } } +mod implicit { + use snafu::prelude::*; + + #[derive(Debug, Snafu)] + enum EnumError { + AVariant { + #[snafu(implicit(false))] + not_location: u8, + }, + } +} + fn main() {} diff --git a/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.stderr b/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.stderr index 1b88add9..038a56a3 100644 --- a/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.stderr +++ b/compatibility-tests/compile-fail/tests/ui/attribute-misuse-opt-out-wrong-field.stderr @@ -9,3 +9,9 @@ error: `backtrace(false)` attribute is only valid on a field named "backtrace", | 19 | #[snafu(backtrace(false))] | ^^^^^^^^^^^^^^^^ + +error: `implicit(false)` attribute is only valid on a field named "location", not on other fields + --> $DIR/attribute-misuse-opt-out-wrong-field.rs:31:21 + | +31 | #[snafu(implicit(false))] + | ^^^^^^^^^^^^^^^ diff --git a/compatibility-tests/futures/src/lib.rs b/compatibility-tests/futures/src/lib.rs index 7c22f7e7..4a8d1f62 100644 --- a/compatibility-tests/futures/src/lib.rs +++ b/compatibility-tests/futures/src/lib.rs @@ -1,5 +1,7 @@ #![cfg(test)] +mod location; + mod api { use futures::{stream, StreamExt, TryStream}; use snafu::prelude::*; diff --git a/compatibility-tests/futures/src/location.rs b/compatibility-tests/futures/src/location.rs new file mode 100644 index 00000000..cd70063d --- /dev/null +++ b/compatibility-tests/futures/src/location.rs @@ -0,0 +1,254 @@ +use futures::{executor::block_on, prelude::*}; +use snafu::{location, prelude::*, Location}; + +#[derive(Debug, Copy, Clone, Snafu)] +struct InnerError { + location: Location, +} + +#[derive(Debug, Copy, Clone, Snafu)] +struct WrappedError { + source: InnerError, + location: Location, +} + +#[derive(Debug, Snafu)] +struct ManuallyWrappedError { + source: InnerError, + #[snafu(implicit(false))] + location: Location, +} + +#[derive(Debug, Snafu)] +#[snafu(display("{}", message))] +#[snafu(whatever)] +pub struct MyWhatever { + #[snafu(source(from(Box, Some)))] + source: Option>, + message: String, + location: Location, +} + +mod try_future { + use super::*; + + #[test] + fn location_macro_uses_creation_location() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = error_future.with_context(|| ManuallyWrappedSnafu { + location: location!(), + }); + let wrapped_error = wrapped_error_future.await.unwrap_err(); + + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn async_block_uses_creation_location() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = async { error_future.await.context(WrappedSnafu) }; + let wrapped_error = wrapped_error_future.await.unwrap_err(); + + assert_eq!( + wrapped_error.location.line, + base_line + 2, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_context_poll() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = error_future.context(WrappedSnafu); + let wrapped_error = wrapped_error_future.await.unwrap_err(); + + // `.await` calls our implementation of `poll`, so the + // location corresponds to that line. + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_with_context_poll() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = error_future.with_context(|| WrappedSnafu); + let wrapped_error = wrapped_error_future.await.unwrap_err(); + + // `.await` calls our implementation of `poll`, so the + // location corresponds to that line. + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_whatever_context_poll() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = error_future.whatever_context("bang"); + let wrapped_error: MyWhatever = wrapped_error_future.await.unwrap_err(); + + // `.await` calls our implementation of `poll`, so the + // location corresponds to that line. + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_with_whatever_context_poll() { + block_on(async { + let base_line = line!(); + let error_future = async { InnerSnafu.fail::<()>() }; + let wrapped_error_future = error_future.with_whatever_context(|_| "bang"); + let wrapped_error: MyWhatever = wrapped_error_future.await.unwrap_err(); + + // `.await` calls our implementation of `poll`, so the + // location corresponds to that line. + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } +} + +mod try_stream { + use super::*; + + #[test] + fn location_macro_uses_creation_location() { + block_on(async { + let base_line = line!(); + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.with_context(|| ManuallyWrappedSnafu { + location: location!(), + }); + let wrapped_error = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + assert_eq!( + wrapped_error.location.line, + base_line + 3, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn async_block_uses_creation_location() { + block_on(async { + let base_line = line!(); + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.map(|r| r.context(WrappedSnafu)); + let wrapped_error = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + assert_eq!( + wrapped_error.location.line, + base_line + 2, + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_context_poll() { + block_on(async { + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.context(WrappedSnafu); + let wrapped_error = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + // `StreamExt::next` doesn't have `[track_caller]`, so the + // location is inside the futures library. + assert!( + wrapped_error.location.file.contains("/futures-util-"), + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_with_context_poll() { + block_on(async { + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.with_context(|| WrappedSnafu); + let wrapped_error = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + // `StreamExt::next` doesn't have `[track_caller]`, so the + // location is inside the futures library. + assert!( + wrapped_error.location.file.contains("/futures-util-"), + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_whatever_context_poll() { + block_on(async { + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.whatever_context("bang"); + let wrapped_error: MyWhatever = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + // `StreamExt::next` doesn't have `[track_caller]`, so the + // location is inside the futures library. + assert!( + wrapped_error.location.file.contains("/futures-util-"), + "Actual location: {}", + wrapped_error.location, + ); + }); + } + + #[test] + fn track_caller_is_applied_on_with_whatever_context_poll() { + block_on(async { + let error_stream = stream::repeat(InnerSnafu.fail::<()>()); + let mut wrapped_error_stream = error_stream.with_whatever_context(|_| "bang"); + let wrapped_error: MyWhatever = wrapped_error_stream.next().await.unwrap().unwrap_err(); + + // `StreamExt::next` doesn't have `[track_caller]`, so the + // location is inside the futures library. + assert!( + wrapped_error.location.file.contains("/futures-util-"), + "Actual location: {}", + wrapped_error.location, + ); + }); + } +} diff --git a/snafu-derive/Cargo.toml b/snafu-derive/Cargo.toml index 8507ecde..b60dd607 100644 --- a/snafu-derive/Cargo.toml +++ b/snafu-derive/Cargo.toml @@ -11,6 +11,7 @@ repository = "https://github.com/shepmaster/snafu" license = "MIT OR Apache-2.0" [features] +rust_1_46 = [] unstable-backtraces-impl-std = [] [lib] diff --git a/snafu-derive/src/lib.rs b/snafu-derive/src/lib.rs index 4b1776dc..895e6087 100644 --- a/snafu-derive/src/lib.rs +++ b/snafu-derive/src/lib.rs @@ -506,6 +506,11 @@ const ATTR_IMPLICIT: OnlyValidOn = OnlyValidOn { valid_on: "enum variant or struct fields with a name", }; +const ATTR_IMPLICIT_FALSE: WrongField = WrongField { + attribute: "implicit(false)", + valid_field: "location", +}; + const ATTR_VISIBILITY: OnlyValidOn = OnlyValidOn { attribute: "visibility", valid_on: "an enum, enum variants, or a struct with named fields", @@ -704,6 +709,7 @@ fn field_container( // exclude fields even if they have the "source" or "backtrace" name. let mut source_opt_out = false; let mut backtrace_opt_out = false; + let mut implicit_opt_out = false; let mut field_errors = errors.scoped(ErrorLocation::OnField); @@ -753,6 +759,10 @@ fn field_container( Att::Implicit(tokens, v) => { if v { implicit_attrs.add((), tokens); + } else if name == "location" { + implicit_opt_out = true; + } else { + field_errors.add(tokens, ATTR_IMPLICIT_FALSE); } } Att::Visibility(tokens, ..) => field_errors.add(tokens, ATTR_VISIBILITY), @@ -789,7 +799,8 @@ fn field_container( } }); - let implicit_attr = implicit_attr.is_some(); + let implicit_attr = + implicit_attr.is_some() || (field.name == "location" && !implicit_opt_out); if let Some((maybe_transformation, location)) = source_attr { let Field { name, ty, .. } = field; diff --git a/snafu-derive/src/shared.rs b/snafu-derive/src/shared.rs index ab406a37..a28467a3 100644 --- a/snafu-derive/src/shared.rs +++ b/snafu-derive/src/shared.rs @@ -172,10 +172,13 @@ pub mod context_selector { let transfer_user_fields = self.transfer_user_fields(); let construct_implicit_fields = self.construct_implicit_fields(); + let track_caller = track_caller(); + quote! { impl<#(#user_field_generics,)*> #parameterized_selector_name { #[doc = "Consume the selector and return the associated error"] #[must_use] + #track_caller #visibility fn build<#(#original_generics_without_defaults,)*>(self) -> #parameterized_error_name where #(#extended_where_clauses),* @@ -187,6 +190,7 @@ pub mod context_selector { } #[doc = "Consume the selector and return a `Result` with the associated error"] + #track_caller #visibility fn fail<#(#original_generics_without_defaults,)* __T>(self) -> ::core::result::Result<__T, #parameterized_error_name> where #(#extended_where_clauses),* @@ -216,6 +220,8 @@ pub mod context_selector { None => (quote! { #crate_root::NoneError }, quote! {}), }; + let track_caller = track_caller(); + quote! { impl<#(#original_generics_without_defaults,)* #(#user_field_generics,)*> #crate_root::IntoError<#parameterized_error_name> for #parameterized_selector_name where @@ -224,6 +230,7 @@ pub mod context_selector { { type Source = #source_ty; + #track_caller fn into_error(self, error: Self::Source) -> #parameterized_error_name { #error_constructor_name { #transfer_source_field @@ -264,10 +271,13 @@ pub mod context_selector { let message_field_name = &message_field.name; + let track_caller = track_caller(); + quote! { impl #crate_root::FromString for #parameterized_error_name { type Source = #source_ty; + #track_caller fn without_source(message: String) -> Self { #error_constructor_name { #empty_source_field @@ -276,6 +286,7 @@ pub mod context_selector { } } + #track_caller fn with_source(error: Self::Source, message: String) -> Self { #error_constructor_name { #transfer_source_field @@ -297,11 +308,14 @@ pub mod context_selector { let (source_field_type, transfer_source_field) = build_source_info(source_field); + let track_caller = track_caller(); + quote! { impl<#(#original_generics_without_defaults,)* #(#user_field_generics,)*> ::core::convert::From<#source_field_type> for #parameterized_error_name where #(#where_clauses),* { + #track_caller fn from(error: #source_field_type) -> Self { #error_constructor_name { #transfer_source_field @@ -324,6 +338,14 @@ pub mod context_selector { quote! { #source_field_name: (#source_transformation)(error), }, ) } + + fn track_caller() -> proc_macro2::TokenStream { + if cfg!(feature = "rust_1_46") { + quote::quote! { #[track_caller] } + } else { + quote::quote! {} + } + } } pub mod display { diff --git a/src/futures/try_future.rs b/src/futures/try_future.rs index d929b922..15fef215 100644 --- a/src/futures/try_future.rs +++ b/src/futures/try_future.rs @@ -230,17 +230,24 @@ where { type Output = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll { let this = self.project(); let inner = this.inner; let context = this.context; - inner.try_poll(ctx).map_err(|error| { - context - .take() - .expect("Cannot poll Context after it resolves") - .into_error(error) - }) + // https://github.com/rust-lang/rust/issues/74042 + match inner.try_poll(ctx) { + Poll::Ready(Ok(v)) => Poll::Ready(Ok(v)), + Poll::Ready(Err(error)) => { + let error = context + .take() + .expect("Cannot poll Context after it resolves") + .into_error(error); + Poll::Ready(Err(error)) + } + Poll::Pending => Poll::Pending, + } } } @@ -266,18 +273,26 @@ where { type Output = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll { let this = self.project(); let inner = this.inner; let context = this.context; - inner.try_poll(ctx).map_err(|error| { - let context = context - .take() - .expect("Cannot poll WithContext after it resolves"); + // https://github.com/rust-lang/rust/issues/74042 + match inner.try_poll(ctx) { + Poll::Ready(Ok(v)) => Poll::Ready(Ok(v)), + Poll::Ready(Err(error)) => { + let context = context + .take() + .expect("Cannot poll WithContext after it resolves"); + + let error = context().into_error(error); - context().into_error(error) - }) + Poll::Ready(Err(error)) + } + Poll::Pending => Poll::Pending, + } } } @@ -305,17 +320,25 @@ where { type Output = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll { let this = self.project(); let inner = this.inner; let context = this.context; - inner.try_poll(ctx).map_err(|error| { - let context = context - .take() - .expect("Cannot poll WhateverContext after it resolves"); - FromString::with_source(error.into(), context.into()) - }) + // https://github.com/rust-lang/rust/issues/74042 + match inner.try_poll(ctx) { + Poll::Ready(Ok(v)) => Poll::Ready(Ok(v)), + Poll::Ready(Err(error)) => { + let context = context + .take() + .expect("Cannot poll WhateverContext after it resolves"); + let error = FromString::with_source(error.into(), context.into()); + + Poll::Ready(Err(error)) + } + Poll::Pending => Poll::Pending, + } } } @@ -345,17 +368,25 @@ where { type Output = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll { let this = self.project(); let inner = this.inner; let context = this.context; - inner.try_poll(ctx).map_err(|error| { - let context = context - .take() - .expect("Cannot poll WhateverContext after it resolves"); - let context = context(&error); - FromString::with_source(error.into(), context.into()) - }) + // https://github.com/rust-lang/rust/issues/74042 + match inner.try_poll(ctx) { + Poll::Ready(Ok(v)) => Poll::Ready(Ok(v)), + Poll::Ready(Err(error)) => { + let context = context + .take() + .expect("Cannot poll WhateverContext after it resolves"); + let context = context(&error); + let error = FromString::with_source(error.into(), context.into()); + + Poll::Ready(Err(error)) + } + Poll::Pending => Poll::Pending, + } } } diff --git a/src/futures/try_stream.rs b/src/futures/try_stream.rs index 8bea4ac5..946ddde7 100644 --- a/src/futures/try_stream.rs +++ b/src/futures/try_stream.rs @@ -233,6 +233,7 @@ where { type Item = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll_next(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll> { let this = self.project(); let inner = this.inner; @@ -272,6 +273,7 @@ where { type Item = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll_next(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll> { let this = self.project(); let inner = this.inner; @@ -313,6 +315,7 @@ where { type Item = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll_next(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll> { let this = self.project(); let inner = this.inner; @@ -356,6 +359,7 @@ where { type Item = Result; + #[cfg_attr(feature = "rust_1_46", track_caller)] fn poll_next(self: Pin<&mut Self>, ctx: &mut TaskContext) -> Poll> { let this = self.project(); let inner = this.inner; diff --git a/src/guide/compatibility.md b/src/guide/compatibility.md index dd320c0b..90c99573 100644 --- a/src/guide/compatibility.md +++ b/src/guide/compatibility.md @@ -2,5 +2,12 @@ SNAFU is tested and compatible back to Rust 1.34, released on 2019-05-14. Compatibility is controlled by Cargo feature flags. -(There are currently no features that require Rust beyond 1.34, -and therefore no version-specific feature flags.) + +## `rust_1_46` + +**default**: enabled + +When enabled, SNAFU will assume that it's safe to target features +available in Rust 1.46. Notably, the `#[track_caller]` feature is +needed to allow [`Location`][crate::Location] to automatically discern +the source code location. diff --git a/src/lib.rs b/src/lib.rs index 707eec6b..b11f18d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,8 @@ //! } //! ``` +use core::fmt; + pub mod prelude { //! Traits and macros used by most projects. Add `use //! snafu::prelude::*` to your code to quickly get started with @@ -606,37 +608,53 @@ pub trait ResultExt: Sized { } impl ResultExt for Result { + #[cfg_attr(feature = "rust_1_46", track_caller)] fn context(self, context: C) -> Result where C: IntoError, E2: Error + ErrorCompat, { - self.map_err(|error| context.into_error(error)) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Ok(v) => Ok(v), + Err(error) => Err(context.into_error(error)), + } } + #[cfg_attr(feature = "rust_1_46", track_caller)] fn with_context(self, context: F) -> Result where F: FnOnce() -> C, C: IntoError, E2: Error + ErrorCompat, { - self.map_err(|error| { - let context = context(); - context.into_error(error) - }) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Ok(v) => Ok(v), + Err(error) => { + let context = context(); + Err(context.into_error(error)) + } + } } #[cfg(any(feature = "std", test))] + #[cfg_attr(feature = "rust_1_46", track_caller)] fn whatever_context(self, context: S) -> Result where S: Into, E2: FromString, E: Into, { - self.map_err(|e| FromString::with_source(e.into(), context.into())) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Ok(v) => Ok(v), + Err(error) => Err(FromString::with_source(error.into(), context.into())), + } } #[cfg(any(feature = "std", test))] + #[cfg_attr(feature = "rust_1_46", track_caller)] fn with_whatever_context(self, context: F) -> Result where F: FnOnce(&E) -> S, @@ -644,10 +662,14 @@ impl ResultExt for Result { E2: FromString, E: Into, { - self.map_err(|e| { - let context = context(&e); - FromString::with_source(e.into(), context.into()) - }) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Ok(t) => Ok(t), + Err(e) => { + let context = context(&e); + Err(FromString::with_source(e.into(), context.into())) + } + } } } @@ -811,43 +833,61 @@ pub trait OptionExt: Sized { } impl OptionExt for Option { + #[cfg_attr(feature = "rust_1_46", track_caller)] fn context(self, context: C) -> Result where C: IntoError, E: Error + ErrorCompat, { - self.ok_or_else(|| context.into_error(NoneError)) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Some(v) => Ok(v), + None => Err(context.into_error(NoneError)), + } } + #[cfg_attr(feature = "rust_1_46", track_caller)] fn with_context(self, context: F) -> Result where F: FnOnce() -> C, C: IntoError, E: Error + ErrorCompat, { - self.ok_or_else(|| context().into_error(NoneError)) + // https://github.com/rust-lang/rust/issues/74042 + match self { + Some(v) => Ok(v), + None => Err(context().into_error(NoneError)), + } } #[cfg(any(feature = "std", test))] + #[cfg_attr(feature = "rust_1_46", track_caller)] fn whatever_context(self, context: S) -> Result where S: Into, E: FromString, { - self.ok_or_else(|| FromString::without_source(context.into())) + match self { + Some(v) => Ok(v), + None => Err(FromString::without_source(context.into())), + } } #[cfg(any(feature = "std", test))] + #[cfg_attr(feature = "rust_1_46", track_caller)] fn with_whatever_context(self, context: F) -> Result where F: FnOnce() -> S, S: Into, E: FromString, { - self.ok_or_else(|| { - let context = context(); - FromString::without_source(context.into()) - }) + match self { + Some(v) => Ok(v), + None => { + let context = context(); + Err(FromString::without_source(context.into())) + } + } } } @@ -1105,6 +1145,152 @@ impl AsBacktrace for Backtrace { } } +/// The source code location where the error was reported. +/// +/// To use it, add a field `location: Location` to your error. This +/// will automatically register it as [implicitly generated +/// data][implicit]. +/// +/// [implicit]: Snafu#controlling-implicitly-generated-data +/// +/// ## Limitations +/// +/// ### Rust 1.46 +/// +/// You need to enable the [`rust_1_46` feature flag][flag] for +/// implicit location capture. If you cannot enable that, you can +/// still use the [`location!`] macro at the expense of more typing. +/// +/// [flag]: guide::compatibility#rust_1_46 +/// +/// ### Disabled context selectors +/// +/// If you have [disabled the context selector][disabled], SNAFU will +/// not be able to capture an accurate location. +/// +/// As a workaround, re-enable the context selector. +/// +/// [disabled]: Snafu#disabling-the-context-selector +/// +/// ### Asynchronous code +/// +/// When using SNAFU's +#[cfg_attr(feature = "futures", doc = " [`TryFutureExt`][futures::TryFutureExt]")] +#[cfg_attr(not(feature = "futures"), doc = " `TryFutureExt`")] +/// or +#[cfg_attr(feature = "futures", doc = " [`TryStreamExt`][futures::TryStreamExt]")] +#[cfg_attr(not(feature = "futures"), doc = " `TryStreamExt`")] +/// extension traits, the automatically captured location will +/// correspond to where the future or stream was **polled**, not where +/// it was created. Additionally, many `Future` or `Stream` +/// combinators do not forward the caller's location to their +/// closures, causing the recorded location to be inside of the future +/// combinator's library. +/// +/// There are two workarounds: +/// 1. Use the [`location!`] macro +/// 1. Use [`ResultExt`] instead +/// +/// ```rust +/// # #[cfg(feature = "futures")] { +/// # use snafu::{prelude::*, Location, location}; +/// // Non-ideal: will report where `wrapped_error_future` is `.await`ed. +/// # let error_future = async { AnotherSnafu.fail::<()>() }; +/// let wrapped_error_future = error_future.context(ImplicitLocationSnafu); +/// +/// // Better: will report the location of `.context`. +/// # let error_future = async { AnotherSnafu.fail::<()>() }; +/// let wrapped_error_future = async { error_future.await.context(ImplicitLocationSnafu) }; +/// +/// // Better: Will report the location of `location!` +/// # let error_future = async { AnotherSnafu.fail::<()>() }; +/// let wrapped_error_future = error_future.with_context(|| ExplicitLocationSnafu { +/// location: location!(), +/// }); +/// +/// # #[derive(Debug, Snafu)] struct AnotherError; +/// #[derive(Debug, Snafu)] +/// struct ImplicitLocationError { +/// source: AnotherError, +/// location: Location, +/// } +/// +/// #[derive(Debug, Snafu)] +/// struct ExplicitLocationError { +/// source: AnotherError, +/// #[snafu(implicit(false))] +/// location: Location, +/// } +/// # } +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct Location { + /// The file where the error was reported + pub file: &'static str, + /// The line where the error was reported + pub line: u32, + /// The column where the error was reported + pub column: u32, + + // Use `#[non_exhaustive]` when we upgrade to Rust 1.40 + _other: (), +} + +impl Location { + /// Constructs a `Location` using the given information + pub fn new(file: &'static str, line: u32, column: u32) -> Self { + Self { + file, + line, + column, + _other: (), + } + } +} + +#[cfg(feature = "rust_1_46")] +impl Default for Location { + #[track_caller] + fn default() -> Self { + let loc = core::panic::Location::caller(); + Self { + file: loc.file(), + line: loc.line(), + column: loc.column(), + _other: (), + } + } +} + +#[cfg(feature = "rust_1_46")] +impl GenerateImplicitData for Location { + #[inline] + #[track_caller] + fn generate() -> Self { + Self::default() + } +} + +impl fmt::Display for Location { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{file}:{line}:{column}", + file = self.file, + line = self.line, + column = self.column, + ) + } +} + +/// Constructs a [`Location`] using the current file, line, and column. +#[macro_export] +macro_rules! location { + () => { + Location::new(file!(), line!(), column!()) + }; +} + /// A basic error type that you can use as a first step to better /// error handling. /// diff --git a/tests/location.rs b/tests/location.rs new file mode 100644 index 00000000..52b6efef --- /dev/null +++ b/tests/location.rs @@ -0,0 +1,239 @@ +use snafu::{prelude::*, Location}; + +mod basics { + use super::*; + + #[derive(Debug, Snafu)] + enum Error { + #[snafu(display("Created at {}", location))] + Usage { location: Location }, + } + + #[test] + fn location_tracks_file_line_and_column() { + let one = UsageSnafu.build(); + let two = UsageSnafu.build(); + + assert_eq!(one.to_string(), "Created at tests/location.rs:14:30"); + assert_eq!(two.to_string(), "Created at tests/location.rs:15:30"); + } +} + +mod opt_out { + use super::*; + + #[derive(Debug, Snafu)] + enum Error { + #[snafu(display("Created at {}", location))] + Usage { + #[snafu(implicit(false))] + location: String, + }, + } + + #[test] + fn opting_out_of_automatic_implicit_data() { + let error = UsageSnafu { location: "junk" }.build(); + + assert_eq!(error.to_string(), "Created at junk"); + } +} + +mod track_caller { + use super::*; + + #[derive(Debug, Copy, Clone, Snafu)] + struct InnerError { + location: Location, + } + + #[derive(Debug, Snafu)] + struct WrapNoUserFieldsError { + source: InnerError, + location: Location, + } + + #[derive(Debug, Snafu)] + #[snafu(context(false))] + struct WrapNoContext { + source: InnerError, + location: Location, + } + + #[derive(Debug, Snafu)] + #[snafu(display("{}", message))] + #[snafu(whatever)] + pub struct MyWhatever { + #[snafu(source(from(Box, Some)))] + source: Option>, + message: String, + location: Location, + } + + #[test] + fn track_caller_is_applied_on_build() { + let base_line = line!(); + let inner = InnerSnafu.build(); + assert_eq!( + inner.location.line, + base_line + 1, + "Actual location: {}", + inner.location, + ); + } + + #[test] + fn track_caller_is_applied_on_fail() { + let base_line = line!(); + let inner = InnerSnafu.fail::<()>().unwrap_err(); + assert_eq!( + inner.location.line, + base_line + 1, + "Actual location: {}", + inner.location, + ); + } + + #[test] + fn track_caller_is_applied_on_ensure() { + let base_line = line!(); + fn x() -> Result<(), InnerError> { + ensure!(false, InnerSnafu); + Ok(()) + } + let inner = x().unwrap_err(); + assert_eq!( + inner.location.line, + base_line + 2, + "Actual location: {}", + inner.location, + ); + } + + #[test] + fn track_caller_is_applied_on_whatever() { + let base_line = line!(); + fn x() -> Result<(), MyWhatever> { + whatever!("bang"); + } + let inner = x().unwrap_err(); + assert_eq!( + inner.location.line, + base_line + 2, + "Actual location: {}", + inner.location, + ); + } + + #[test] + fn track_caller_is_applied_on_result_context() { + let base_line = line!(); + let wrap_no_user_fields = InnerSnafu + .fail::<()>() + .context(WrapNoUserFieldsSnafu) + .unwrap_err(); + assert_eq!( + wrap_no_user_fields.location.line, + base_line + 3, + "Actual location: {}", + wrap_no_user_fields.location, + ); + } + + #[test] + fn track_caller_is_applied_on_result_with_context() { + let base_line = line!(); + let wrap_no_user_fields = InnerSnafu + .fail::<()>() + .with_context(|| WrapNoUserFieldsSnafu) + .unwrap_err(); + assert_eq!( + wrap_no_user_fields.location.line, + base_line + 3, + "Actual location: {}", + wrap_no_user_fields.location, + ); + } + + #[test] + fn track_caller_is_applied_on_result_whatever_context() { + let base_line = line!(); + let whatever: MyWhatever = InnerSnafu + .fail::<()>() + .whatever_context("bang") + .unwrap_err(); + assert_eq!( + whatever.location.line, + base_line + 3, + "Actual location: {}", + whatever.location, + ); + } + + #[test] + fn track_caller_is_applied_on_result_with_whatever_context() { + let base_line = line!(); + let whatever: MyWhatever = InnerSnafu + .fail::<()>() + .with_whatever_context(|_| "bang") + .unwrap_err(); + assert_eq!( + whatever.location.line, + base_line + 3, + "Actual location: {}", + whatever.location, + ); + } + + #[test] + fn track_caller_is_applied_on_option_context() { + let base_line = line!(); + let option_to_error_no_user_fields = None::<()>.context(InnerSnafu).unwrap_err(); + assert_eq!( + option_to_error_no_user_fields.location.line, + base_line + 1, + "Actual location: {}", + option_to_error_no_user_fields.location, + ); + } + + #[test] + fn track_caller_is_applied_on_option_with_context() { + let base_line = line!(); + let option_to_error_no_user_fields = None::<()>.with_context(|| InnerSnafu).unwrap_err(); + assert_eq!( + option_to_error_no_user_fields.location.line, + base_line + 1, + "Actual location: {}", + option_to_error_no_user_fields.location, + ); + } + + #[test] + fn track_caller_is_applied_on_option_whatever_context() { + let base_line = line!(); + let whatever: MyWhatever = None::<()>.whatever_context("bang").unwrap_err(); + assert_eq!( + whatever.location.line, + base_line + 1, + "Actual location: {}", + whatever.location, + ); + } + + #[test] + fn track_caller_is_applied_on_option_with_whatever_context() { + let base_line = line!(); + let whatever: MyWhatever = None::<()>.with_whatever_context(|| "bang").unwrap_err(); + assert_eq!( + whatever.location.line, + base_line + 1, + "Actual location: {}", + whatever.location, + ); + } + + // `track_caller` not supported on the `Try` trait, so we have no + // useful location for `context(false)` errors. Check back in the + // future to see if there's a fix. +}