use std::borrow::Cow;
use std::fmt;
use std::process::{ExitCode, Termination};
use crate::ColorMode;
use crate::Diagnostic;
use crate::color::style;
use crate::theme::{Theme, get_theme};
use crate::trace_printer::{TracePrinter, error_backtrace_frame_filter, marker_strip_filter};
pub struct Report<E> {
res: Result<(), E>,
color_config: ColorMode,
theme_override: Option<Theme>,
backtrace: Option<oopsie_core::Backtrace>,
}
impl<E: Diagnostic> Report<E> {
fn resolve_backtrace(res: &Result<(), E>) -> Option<oopsie_core::Backtrace> {
let backtrace = res.as_ref().err()?.oopsie_backtrace()?.clone();
backtrace.resolve();
(!backtrace.frames().is_empty()).then_some(backtrace)
}
#[must_use]
#[inline]
pub fn new(error: E) -> Self {
let res = Err(error);
Self {
backtrace: Self::resolve_backtrace(&res),
res,
color_config: ColorMode::Auto,
theme_override: None,
}
}
#[must_use]
#[inline]
pub const fn ok() -> Self {
Self {
res: Ok(()),
color_config: ColorMode::Auto,
theme_override: None,
backtrace: None,
}
}
#[must_use]
pub fn run<F>(func: F) -> Self
where
F: FnOnce() -> Result<(), E>,
{
crate::panic_hook::acquire_hook();
let mut prev_marker = None;
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
prev_marker = oopsie_core::__private::set_marker();
func()
}));
oopsie_core::__private::restore_marker(prev_marker);
crate::panic_hook::release_hook();
let result = match result {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
};
Self {
backtrace: Self::resolve_backtrace(&result),
res: result,
color_config: ColorMode::Auto,
theme_override: None,
}
}
#[must_use]
#[inline]
pub const fn no_colors(mut self) -> Self {
self.color_config = ColorMode::Never;
self
}
#[must_use]
#[inline]
pub const fn force_colors(mut self) -> Self {
self.color_config = ColorMode::Always;
self
}
#[must_use]
#[inline]
pub const fn with_theme(mut self, theme: Theme) -> Self {
self.theme_override = Some(theme);
self
}
#[inline]
fn resolved_theme(&self) -> Cow<'_, Theme> {
match self.theme_override.as_ref() {
Some(theme) => Cow::Borrowed(theme),
None => Cow::Owned(get_theme()),
}
}
#[must_use]
#[inline]
pub const fn error(&self) -> Option<&E> {
match &self.res {
Err(err) => Some(err),
Ok(()) => None,
}
}
#[must_use]
#[inline]
pub fn into_error(self) -> Option<E> {
self.res.err()
}
fn write_error_chain(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
const MAX_SOURCE_CHAIN_DEPTH: usize = 128;
let c = self.color_config.should_colorize();
let theme = self.resolved_theme();
let Err(err) = &self.res else { return Ok(()) };
let error_code = err.oopsie_error_code();
let help_text = err.oopsie_help_text();
write!(f, "{}", style!("Error", theme.error_title(), c))?;
if let Some(code) = error_code {
write!(
f,
"{}{}{}",
style!("[", theme.delimiter(), c),
style!(code, theme.error_code(), c),
style!("]", theme.delimiter(), c)
)?;
}
writeln!(f, ": {err}")?;
if let Some(location) = err.oopsie_location() {
let at = format_args!(
"{}:{}:{}",
location.file(),
location.line(),
location.column()
);
writeln!(
f,
" {} {}",
style!("at", theme.hint(), c),
style!(at, theme.hint(), c)
)?;
}
let mut source = err.source();
let mut depth = 0_usize;
while let Some(err) = source {
if depth == MAX_SOURCE_CHAIN_DEPTH {
writeln!(
f,
" {} (source chain truncated)",
style!("╰─▶", theme.cause(), c)
)?;
break;
}
let next_source = err.source();
let arrow = if next_source.is_some() {
"├─▶"
} else {
"╰─▶"
};
writeln!(f, " {} {err}", style!(arrow, theme.cause(), c))?;
source = next_source;
depth += 1;
}
if let Some(help) = help_text {
writeln!(f, "\n {}: {help}", style!("help", theme.help(), c))?;
}
Ok(())
}
#[cfg(feature = "tracing")]
fn write_spantrace(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(span_trace) = self.error().and_then(|e| e.oopsie_spantrace()) else {
return Ok(());
};
if !span_trace.is_captured() {
return Ok(());
}
writeln!(f)?;
let printer = TracePrinter::new();
let printer = if self.color_config.should_colorize() {
printer.with_theme(self.resolved_theme().trace())
} else {
printer.plain()
};
printer.write_spantrace(f, span_trace)?;
Ok(())
}
fn write_backtrace(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(backtrace) = &self.backtrace else {
return Ok(());
};
writeln!(f)?;
let mut printer = if oopsie_core::rust_backtrace().is_full() {
TracePrinter::unfiltered()
} else if let Some(cut) = backtrace.marker_hidden_frames() {
TracePrinter::with_filter(marker_strip_filter(cut))
.add_frame_filter(error_backtrace_frame_filter)
} else {
TracePrinter::new()
};
printer = if self.color_config.should_colorize() {
printer.with_theme(self.resolved_theme().trace())
} else {
printer.plain()
};
printer.write_backtrace(f, backtrace)?;
Ok(())
}
}
impl<E> Termination for Report<E>
where
E: Diagnostic,
{
#[expect(
clippy::print_stderr,
reason = "Termination renders the error report to stderr at process exit"
)]
fn report(self) -> ExitCode {
match &self.res {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{self}");
let declared = e
.oopsie_exit_code()
.map_or(ExitCode::FAILURE, |n| ExitCode::from(n.get()));
#[cfg(feature = "unstable-error-generic-member-access")]
{
core::error::request_ref::<ExitCode>(e)
.copied()
.unwrap_or(declared)
}
#[cfg(not(feature = "unstable-error-generic-member-access"))]
{
declared
}
}
}
}
}
#[cfg(feature = "unstable-try-trait-v2")]
impl<T, E: Diagnostic> core::ops::FromResidual<Result<T, E>> for Report<E> {
fn from_residual(residual: Result<T, E>) -> Self {
let res = residual.map(drop);
Self {
backtrace: Self::resolve_backtrace(&res),
res,
color_config: ColorMode::default(),
theme_override: None,
}
}
}
impl<E: Diagnostic> fmt::Display for Report<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.write_error_chain(f)?;
#[cfg(feature = "tracing")]
self.write_spantrace(f)?;
self.write_backtrace(f)?;
Ok(())
}
}
impl<E: Diagnostic> fmt::Debug for Report<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl<E: Diagnostic> From<E> for Report<E> {
#[inline]
fn from(error: E) -> Self {
Self::new(error)
}
}