From 9633f60380780920c9f154d881c4d1285296c293 Mon Sep 17 00:00:00 2001 From: Tim Bates Date: Sun, 22 Mar 2026 13:41:55 +1030 Subject: [PATCH 1/3] Split `Deref for Report` into SendSync and Local variants For some reason this causes an ambiguity in type inference that wasn't there before, although the docs already warn about the potential for it. This could be a breaking change for downstream crates. --- src/compat/anyhow1.rs | 2 +- src/compat/boxed_error.rs | 4 ++-- src/compat/eyre06.rs | 2 +- src/report/owned.rs | 12 ++++++++++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/compat/anyhow1.rs b/src/compat/anyhow1.rs index 0b632ff..db09414 100644 --- a/src/compat/anyhow1.rs +++ b/src/compat/anyhow1.rs @@ -199,7 +199,7 @@ impl IntoRootcause for anyhow::Error { #[inline(always)] fn into_rootcause(self) -> Self::Output { - Report::new_custom::(self).into_dynamic() + Report::new_sendsync_custom::(self).into_dynamic() } } diff --git a/src/compat/boxed_error.rs b/src/compat/boxed_error.rs index 00206ca..a319fd9 100644 --- a/src/compat/boxed_error.rs +++ b/src/compat/boxed_error.rs @@ -360,7 +360,7 @@ impl IntoRootcause for Box { #[inline(always)] fn into_rootcause(self) -> Self::Output { - Report::new_custom::(self).into_dynamic() + Report::new_sendsync_custom::(self).into_dynamic() } } @@ -369,7 +369,7 @@ impl IntoRootcause for Box { #[inline(always)] fn into_rootcause(self) -> Self::Output { - Report::new_custom::(self).into_dynamic() + Report::new_local_custom::(self).into_dynamic() } } diff --git a/src/compat/eyre06.rs b/src/compat/eyre06.rs index f9862dd..e4107da 100644 --- a/src/compat/eyre06.rs +++ b/src/compat/eyre06.rs @@ -199,7 +199,7 @@ impl IntoRootcause for eyre::Report { #[inline(always)] fn into_rootcause(self) -> Self::Output { - Report::new_custom::(self).into_dynamic() + Report::new_sendsync_custom::(self).into_dynamic() } } diff --git a/src/report/owned.rs b/src/report/owned.rs index 7ee3264..87a186b 100644 --- a/src/report/owned.rs +++ b/src/report/owned.rs @@ -1803,8 +1803,16 @@ impl core::fmt::Debug for Report { } } -impl core::ops::Deref for Report { - type Target = dyn core::error::Error; +impl core::ops::Deref for Report { + type Target = dyn core::error::Error + Send + Sync + 'static; + + fn deref(&self) -> &Self::Target { + ErrorNoSourceWrapper::new(self) + } +} + +impl core::ops::Deref for Report { + type Target = dyn core::error::Error + 'static; fn deref(&self) -> &Self::Target { ErrorNoSourceWrapper::new(self) From 7e794e7c244ef571cc40aada5d6dba9dd7be7954 Mon Sep 17 00:00:00 2001 From: Tim Bates Date: Mon, 23 Mar 2026 10:55:58 +1030 Subject: [PATCH 2/3] Add tests for `Deref` impls --- src/report/mut_.rs | 24 ++++++++- src/report/owned.rs | 120 +++++++++++++++++++++++++++++++++++++++++++- src/report/ref_.rs | 24 ++++++++- 3 files changed, 165 insertions(+), 3 deletions(-) diff --git a/src/report/mut_.rs b/src/report/mut_.rs index f54ff5d..4b57c39 100644 --- a/src/report/mut_.rs +++ b/src/report/mut_.rs @@ -1191,7 +1191,10 @@ impl<'a, C: Sized> From> for ReportMut<'a, Dynamic, Loca #[cfg(test)] mod tests { - use alloc::string::String; + use alloc::string::{String, ToString}; + use core::error::Error as StdError; + use core::ops::Deref; + use thiserror::Error; use super::*; @@ -1236,4 +1239,23 @@ mod tests { static_assertions::assert_not_impl_any!(ReportMut<'static, Dynamic, SendSync>: Copy, Clone); static_assertions::assert_not_impl_any!(ReportMut<'static, Dynamic, Local>: Copy, Clone); } + + #[derive(Debug, Error)] + #[error("boom")] + struct Boom; + + fn make_report() -> Report { + Report::new(Boom) + } + + #[test] + fn report_mut_derefs_to_dyn_error() { + let mut report = make_report(); + let report_mut = report.as_mut(); + + let err: &dyn StdError = report_mut.deref(); + + assert!(err.to_string().contains("boom")); + assert!(report.source().is_none()); + } } diff --git a/src/report/owned.rs b/src/report/owned.rs index 87a186b..d7322d3 100644 --- a/src/report/owned.rs +++ b/src/report/owned.rs @@ -1868,7 +1868,10 @@ from_impls!( #[cfg(test)] mod tests { - use alloc::string::String; + use alloc::string::{String, ToString}; + use core::error::Error as StdError; + use core::ops::Deref; + use thiserror::Error; use super::*; @@ -1947,4 +1950,119 @@ mod tests { static_assertions::assert_not_impl_any!(Report: Copy); static_assertions::assert_not_impl_any!(Report: Copy); } + + #[derive(Debug, Error)] + #[error("boom")] + struct Boom; + + #[derive(Debug, Error)] + enum Outer { + #[error(transparent)] + Inner(#[from] Report), + } + + fn make_report() -> Report { + Report::new(Boom) + } + + fn make_dynamic_report() -> Report { + make_report().into_dynamic() + } + + fn make_cloneable_report() -> Report { + make_report().into_cloneable() + } + + fn make_local_report() -> Report { + make_report().into_local() + } + + #[test] + fn report_derefs_to_dyn_error() { + let report = make_report(); + + let err: &(dyn StdError + Send + Sync) = report.deref(); + + assert!(err.to_string().contains("boom")); + } + + #[test] + fn dynamic_report_derefs_to_dyn_error() { + let report = make_dynamic_report(); + + let err: &dyn StdError = report.deref(); + + assert!(err.to_string().contains("boom")); + } + + #[test] + fn cloneable_report_derefs_to_dyn_error() { + let report = make_cloneable_report(); + + let err: &dyn StdError = report.deref(); + + assert!(err.to_string().contains("boom")); + } + + #[test] + fn local_report_derefs_to_dyn_error() { + let report = make_local_report(); + + let err: &dyn StdError = report.deref(); + + assert!(err.to_string().contains("boom")); + } + + #[test] + fn report_deref_supports_error_methods() { + let report = make_report(); + + assert!(report.source().is_none()); + + #[expect(deprecated)] + { + assert_eq!( + report.description(), + "description() is deprecated; use Display" + ); + } + } + + #[test] + fn thiserror_from_works_for_report() { + let report = make_report(); + + let outer: Outer = report.into(); + + match outer { + Outer::Inner(inner) => { + assert!(inner.to_string().contains("boom")); + } + } + } + + #[test] + fn thiserror_question_mark_works_for_report() { + fn inner() -> Result<(), Report> { + Err(make_report()) + } + + fn outer() -> Result<(), Outer> { + inner()?; + Ok(()) + } + + let err = outer().unwrap_err(); + assert!(err.to_string().contains("boom")); + } + + #[test] + fn report_is_usable_where_dyn_error_is_expected() { + fn takes_error(err: &dyn StdError) -> String { + err.to_string() + } + + let report = make_report(); + assert!(takes_error(report.deref()).contains("boom")); + } } diff --git a/src/report/ref_.rs b/src/report/ref_.rs index 9bface1..413e8ba 100644 --- a/src/report/ref_.rs +++ b/src/report/ref_.rs @@ -1086,7 +1086,10 @@ from_impls!( #[cfg(test)] mod tests { - use alloc::string::String; + use alloc::string::{String, ToString}; + use core::error::Error as StdError; + use core::ops::Deref; + use thiserror::Error; use super::*; use crate::markers::{Mutable, Uncloneable}; @@ -1194,4 +1197,23 @@ mod tests { let preformatted: Report = report_ref.preformat(); assert_eq!(alloc::format!("{report}"), alloc::format!("{preformatted}")); } + + #[derive(Debug, Error)] + #[error("boom")] + struct Boom; + + fn make_report() -> Report { + Report::new(Boom) + } + + #[test] + fn report_ref_derefs_to_dyn_error() { + let report = make_report(); + let report_ref = report.as_ref(); + + let err: &dyn StdError = report_ref.deref(); + + assert!(err.to_string().contains("boom")); + assert!(report.source().is_none()); + } } From 407687ced5de7d1a2de5a1aba265d6911935688a Mon Sep 17 00:00:00 2001 From: Tim Bates Date: Mon, 23 Mar 2026 10:57:42 +1030 Subject: [PATCH 3/3] Implement `AsRef` for Report, ReportRef and ReportMut Allows Report to be used with methods that take `impl AsRef` parameters. --- src/report/mut_.rs | 19 +++++++++++++++ src/report/owned.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++ src/report/ref_.rs | 19 +++++++++++++++ 3 files changed, 96 insertions(+) diff --git a/src/report/mut_.rs b/src/report/mut_.rs index 4b57c39..54647dc 100644 --- a/src/report/mut_.rs +++ b/src/report/mut_.rs @@ -1175,6 +1175,13 @@ impl<'a, C: ?Sized, T> core::ops::Deref for ReportMut<'a, C, T> { } } +impl<'a, C: ?Sized, T> AsRef for ReportMut<'a, C, T> { + #[inline(always)] + fn as_ref(&self) -> &(dyn core::error::Error + 'a) { + ErrorNoSourceWrapper::new(self) + } +} + impl<'a, C: ?Sized, T> Unpin for ReportMut<'a, C, T> {} impl<'a, C: Sized> From> for ReportMut<'a, Dynamic, SendSync> { @@ -1258,4 +1265,16 @@ mod tests { assert!(err.to_string().contains("boom")); assert!(report.source().is_none()); } + + #[test] + fn report_mut_asrefs_to_dyn_error() { + let mut report = make_report(); + let report_mut = report.as_mut(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(report_mut); + } } diff --git a/src/report/owned.rs b/src/report/owned.rs index d7322d3..5a0dc7c 100644 --- a/src/report/owned.rs +++ b/src/report/owned.rs @@ -1819,6 +1819,20 @@ impl core::ops::Deref for Report { } } +impl AsRef for Report { + #[inline(always)] + fn as_ref(&self) -> &(dyn core::error::Error + Send + Sync + 'static) { + ErrorNoSourceWrapper::new(self) + } +} + +impl AsRef for Report { + #[inline(always)] + fn as_ref(&self) -> &(dyn core::error::Error + 'static) { + ErrorNoSourceWrapper::new(self) + } +} + impl Unpin for Report {} macro_rules! from_impls { @@ -1986,6 +2000,17 @@ mod tests { assert!(err.to_string().contains("boom")); } + #[test] + fn report_asrefs_to_dyn_error() { + let report = make_report(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(&report); + } + #[test] fn dynamic_report_derefs_to_dyn_error() { let report = make_dynamic_report(); @@ -1995,6 +2020,17 @@ mod tests { assert!(err.to_string().contains("boom")); } + #[test] + fn dynamic_report_asrefs_to_dyn_error() { + let report = make_dynamic_report(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(&report); + } + #[test] fn cloneable_report_derefs_to_dyn_error() { let report = make_cloneable_report(); @@ -2004,6 +2040,17 @@ mod tests { assert!(err.to_string().contains("boom")); } + #[test] + fn cloneable_report_asrefs_to_dyn_error() { + let report = make_cloneable_report(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(&report); + } + #[test] fn local_report_derefs_to_dyn_error() { let report = make_local_report(); @@ -2013,6 +2060,17 @@ mod tests { assert!(err.to_string().contains("boom")); } + #[test] + fn local_report_asrefs_to_dyn_error() { + let report = make_local_report(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(&report); + } + #[test] fn report_deref_supports_error_methods() { let report = make_report(); diff --git a/src/report/ref_.rs b/src/report/ref_.rs index 413e8ba..afadc96 100644 --- a/src/report/ref_.rs +++ b/src/report/ref_.rs @@ -1036,6 +1036,13 @@ impl<'a, C: ?Sized, O, T> core::ops::Deref for ReportRef<'a, C, O, T> { } } +impl<'a, C: ?Sized, O, T> AsRef for ReportRef<'a, C, O, T> { + #[inline(always)] + fn as_ref(&self) -> &(dyn core::error::Error + 'a) { + ErrorNoSourceWrapper::new(self) + } +} + impl<'a, C: ?Sized, O, T> Unpin for ReportRef<'a, C, O, T> {} macro_rules! from_impls { @@ -1216,4 +1223,16 @@ mod tests { assert!(err.to_string().contains("boom")); assert!(report.source().is_none()); } + + #[test] + fn report_ref_asrefs_to_dyn_error() { + let report = make_report(); + let report_ref = report.as_ref(); + + fn takes_asref<'a>(err: impl AsRef) { + assert!(err.as_ref().to_string().contains("boom")); + } + + takes_asref(report_ref); + } }