use alloc::{boxed::Box, vec, vec::Vec};
use core::{error::Error, marker::PhantomData, mem, panic::Location};
#[cfg(feature = "backtrace")]
use std::backtrace::{Backtrace, BacktraceStatus};
#[cfg(feature = "std")]
use std::process::ExitCode;
#[cfg(feature = "spantrace")]
use tracing_error::{SpanTrace, SpanTraceStatus};
#[cfg(nightly)]
use crate::iter::{RequestRef, RequestValue};
use crate::{
Attachment, Frame, OpaqueAttachment,
context::SourceContext,
iter::{Frames, FramesMut},
};
#[cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/report_display__doc.snap")))]
#[cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/report_display_alt__doc.snap")))]
#[cfg_attr(doc, doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/snapshots/doc/report_debug__doc.snap")))]
#[must_use]
#[expect(clippy::field_scoped_visibility_modifiers)]
pub struct Report<C: ?Sized> {
#[expect(clippy::box_collection)]
pub(super) frames: Box<Vec<Frame>>,
_context: PhantomData<fn() -> *const C>,
}
impl<C> Report<C> {
#[inline]
#[track_caller]
#[expect(clippy::missing_panics_doc, reason = "No panic possible")]
pub fn new(context: C) -> Self
where
C: Error + Send + Sync + 'static,
{
if let Some(mut current_source) = context.source() {
let mut sources = vec![SourceContext::from_error(current_source)];
while let Some(source) = current_source.source() {
sources.push(SourceContext::from_error(source));
current_source = source;
}
let mut report = Report::from_frame(Frame::from_context(
sources.pop().expect("At least one context is guaranteed"),
Box::new([]),
));
while let Some(source) = sources.pop() {
report = report.change_context(source);
}
return report.change_context(context);
}
Self::from_frame(Frame::from_context(context, Box::new([])))
}
#[track_caller]
pub(crate) fn from_frame(frame: Frame) -> Self {
#[cfg(nightly)]
let location = core::error::request_ref::<Location>(&frame.as_error())
.is_none()
.then_some(Location::caller());
#[cfg(not(nightly))]
let location = Some(Location::caller());
#[cfg(all(nightly, feature = "backtrace"))]
let backtrace = core::error::request_ref::<Backtrace>(&frame.as_error())
.is_none_or(|backtrace| backtrace.status() != BacktraceStatus::Captured)
.then(Backtrace::capture);
#[cfg(all(not(nightly), feature = "backtrace"))]
let backtrace = Some(Backtrace::capture());
#[cfg(all(nightly, feature = "spantrace"))]
let span_trace = core::error::request_ref::<SpanTrace>(&frame.as_error())
.is_none_or(|span_trace| span_trace.status() != SpanTraceStatus::CAPTURED)
.then(SpanTrace::capture);
#[cfg(all(not(nightly), feature = "spantrace"))]
let span_trace = Some(SpanTrace::capture());
let mut report = Self {
frames: Box::new(vec![frame]),
_context: PhantomData,
};
if let Some(location) = location {
report = report.attach_opaque(*location);
}
#[cfg(feature = "backtrace")]
if let Some(backtrace) =
backtrace.filter(|bt| matches!(bt.status(), BacktraceStatus::Captured))
{
report = report.attach_opaque(backtrace);
}
#[cfg(feature = "spantrace")]
if let Some(span_trace) = span_trace.filter(|st| st.status() == SpanTraceStatus::CAPTURED) {
report = report.attach_opaque(span_trace);
}
report
}
pub fn expand(self) -> Report<[C]> {
Report {
frames: self.frames,
_context: PhantomData,
}
}
#[must_use]
pub fn current_frame(&self) -> &Frame {
self.frames.first().unwrap_or_else(|| {
unreachable!(
"Report does not contain any frames. This should not happen as a Report must \
always contain at least one frame.\n\n
Please file an issue to https://github.com/hashintel/hash/issues/new?template=bug-report-error-stack.yml\n\n
Report:\n{self:?}",
)
})
}
#[must_use]
pub fn current_context(&self) -> &C
where
C: Send + Sync + 'static,
{
self.downcast_ref().unwrap_or_else(|| {
unreachable!(
"Report does not contain a context. This should not happen as a Report must \
always contain at least one frame.\n\n
Please file an issue to https://github.com/hashintel/hash/issues/new?template=bug-report-error-stack.yml\n\n
Report:\n{self:?}",
)
})
}
#[must_use]
pub fn into_error(self) -> impl Error + Send + Sync + 'static
where
C: 'static,
{
crate::error::ReportError::new(self)
}
#[must_use]
pub fn as_error(&self) -> &(impl Error + Send + Sync + 'static)
where
C: 'static,
{
crate::error::ReportError::from_ref(self)
}
}
impl<C: ?Sized> Report<C> {
#[must_use]
pub(crate) fn current_frames_unchecked(&self) -> &[Frame] {
&self.frames
}
#[track_caller]
pub fn attach<A>(mut self, attachment: A) -> Self
where
A: Attachment,
{
let old_frames = mem::replace(self.frames.as_mut(), Vec::with_capacity(1));
self.frames.push(Frame::from_printable_attachment(
attachment,
old_frames.into_boxed_slice(),
));
self
}
#[track_caller]
pub fn attach_opaque<A>(mut self, attachment: A) -> Self
where
A: OpaqueAttachment,
{
let old_frames = mem::replace(self.frames.as_mut(), Vec::with_capacity(1));
self.frames.push(Frame::from_attachment(
attachment,
old_frames.into_boxed_slice(),
));
self
}
#[track_caller]
pub fn change_context<T>(mut self, context: T) -> Report<T>
where
T: Error + Send + Sync + 'static,
{
let old_frames = mem::replace(self.frames.as_mut(), Vec::with_capacity(1));
let context_frame = vec![Frame::from_context(context, old_frames.into_boxed_slice())];
self.frames.push(Frame::from_attachment(
*Location::caller(),
context_frame.into_boxed_slice(),
));
Report {
frames: self.frames,
_context: PhantomData,
}
}
pub fn frames(&self) -> Frames<'_> {
Frames::new(&self.frames)
}
pub fn frames_mut(&mut self) -> FramesMut<'_> {
FramesMut::new(&mut self.frames)
}
#[cfg(nightly)]
pub fn request_ref<T: ?Sized + Send + Sync + 'static>(&self) -> RequestRef<'_, T> {
RequestRef::new(&self.frames)
}
#[cfg(nightly)]
pub fn request_value<T: Send + Sync + 'static>(&self) -> RequestValue<'_, T> {
RequestValue::new(&self.frames)
}
#[must_use]
pub fn contains<T: Send + Sync + 'static>(&self) -> bool {
self.frames().any(Frame::is::<T>)
}
#[must_use]
pub fn downcast_ref<T: Send + Sync + 'static>(&self) -> Option<&T> {
self.frames().find_map(Frame::downcast_ref::<T>)
}
#[must_use]
pub fn downcast_mut<T: Send + Sync + 'static>(&mut self) -> Option<&mut T> {
self.frames_mut().find_map(Frame::downcast_mut::<T>)
}
}
impl<C> Report<[C]> {
#[must_use]
pub fn current_frames(&self) -> &[Frame] {
&self.frames
}
pub fn push(&mut self, mut report: Report<C>) {
self.frames.append(&mut report.frames);
}
pub fn append(&mut self, mut report: Self) {
self.frames.append(&mut report.frames);
}
pub fn current_contexts(&self) -> impl Iterator<Item = &C>
where
C: Send + Sync + 'static,
{
let mut output = Vec::new();
let mut stack = vec![self.current_frames()];
while let Some(frames) = stack.pop() {
for frame in frames {
if let Some(context) = frame.downcast_ref::<C>() {
output.push(context);
continue;
}
let sources = frame.sources();
match sources {
[] => unreachable!(
"Report does not contain a context. This is considered a bug and should be \
reported to https://github.com/hashintel/hash/issues/new/choose"
),
sources => {
stack.push(sources);
}
}
}
}
output.into_iter()
}
}
impl<C: 'static> From<Report<C>> for Box<dyn Error> {
fn from(report: Report<C>) -> Self {
Box::new(report.into_error())
}
}
impl<C: 'static> From<Report<C>> for Box<dyn Error + Send> {
fn from(report: Report<C>) -> Self {
Box::new(report.into_error())
}
}
impl<C: 'static> From<Report<C>> for Box<dyn Error + Sync> {
fn from(report: Report<C>) -> Self {
Box::new(report.into_error())
}
}
impl<C: 'static> From<Report<C>> for Box<dyn Error + Send + Sync> {
fn from(report: Report<C>) -> Self {
Box::new(report.into_error())
}
}
impl<C> From<Report<C>> for Report<[C]> {
fn from(report: Report<C>) -> Self {
Self {
frames: report.frames,
_context: PhantomData,
}
}
}
#[cfg(feature = "std")]
impl<C> std::process::Termination for Report<C> {
fn report(self) -> ExitCode {
#[cfg(not(nightly))]
return ExitCode::FAILURE;
#[cfg(nightly)]
self.request_ref::<ExitCode>()
.next()
.copied()
.unwrap_or(ExitCode::FAILURE)
}
}
impl<C> FromIterator<Report<C>> for Option<Report<[C]>> {
fn from_iter<T: IntoIterator<Item = Report<C>>>(iter: T) -> Self {
let mut iter = iter.into_iter();
let mut base = iter.next()?.expand();
for rest in iter {
base.push(rest);
}
Some(base)
}
}
impl<C> FromIterator<Report<[C]>> for Option<Report<[C]>> {
fn from_iter<T: IntoIterator<Item = Report<[C]>>>(iter: T) -> Self {
let mut iter = iter.into_iter();
let mut base = iter.next()?;
for mut rest in iter {
base.frames.append(&mut rest.frames);
}
Some(base)
}
}
impl<C> Extend<Report<C>> for Report<[C]> {
fn extend<T: IntoIterator<Item = Report<C>>>(&mut self, iter: T) {
for item in iter {
self.push(item);
}
}
}
impl<C> Extend<Self> for Report<[C]> {
fn extend<T: IntoIterator<Item = Self>>(&mut self, iter: T) {
for mut item in iter {
self.frames.append(&mut item.frames);
}
}
}
pub trait IntoReport {
type Context: ?Sized;
fn into_report(self) -> Report<Self::Context>;
}
impl<C: ?Sized> IntoReport for Report<C> {
type Context = C;
#[track_caller]
fn into_report(self) -> Report<Self::Context> {
self
}
}
impl<E> IntoReport for E
where
E: Into<Report<E>>,
{
type Context = E;
#[track_caller]
fn into_report(self) -> Report<Self::Context> {
self.into()
}
}