use std::{error, fmt};
use crate::{
builtins::{error::ErrorObject, Array},
js_string,
object::JsObject,
property::PropertyDescriptor,
realm::Realm,
Context, JsString, JsValue,
};
use boa_gc::{custom_trace, Finalize, Trace};
use boa_macros::js_str;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)]
#[boa_gc(unsafe_no_drop)]
pub struct JsError {
inner: Repr,
}
#[derive(Debug, Clone, PartialEq, Eq, Trace, Finalize)]
#[boa_gc(unsafe_no_drop)]
enum Repr {
Native(JsNativeError),
Opaque(JsValue),
}
#[derive(Debug, Clone, Error)]
pub enum TryNativeError {
#[error("invalid type of property `{0}`")]
InvalidPropertyType(&'static str),
#[error("property `message` cannot contain unpaired surrogates")]
InvalidMessageEncoding,
#[error("invalid `constructor` property of Error object")]
InvalidConstructor,
#[error("could not access property `{property}`")]
InaccessibleProperty {
property: &'static str,
source: JsError,
},
#[error("could not get element `{index}` of property `errors`")]
InvalidErrorsIndex {
index: u64,
source: JsError,
},
#[error("opaque error of type `{:?}` is not an Error object", .0.get_type())]
NotAnErrorObject(JsValue),
#[error("could not access realm of Error object")]
InaccessibleRealm {
source: JsError,
},
}
impl error::Error for JsError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.inner {
Repr::Native(err) => err.source(),
Repr::Opaque(_) => None,
}
}
}
impl JsError {
#[must_use]
pub const fn from_native(err: JsNativeError) -> Self {
Self {
inner: Repr::Native(err),
}
}
#[must_use]
pub const fn from_opaque(value: JsValue) -> Self {
Self {
inner: Repr::Opaque(value),
}
}
pub fn to_opaque(&self, context: &mut Context) -> JsValue {
match &self.inner {
Repr::Native(e) => e.to_opaque(context).into(),
Repr::Opaque(v) => v.clone(),
}
}
pub fn try_native(&self, context: &mut Context) -> Result<JsNativeError, TryNativeError> {
match &self.inner {
Repr::Native(e) => Ok(e.clone()),
Repr::Opaque(val) => {
let obj = val
.as_object()
.ok_or_else(|| TryNativeError::NotAnErrorObject(val.clone()))?;
let error = *obj
.downcast_ref::<ErrorObject>()
.ok_or_else(|| TryNativeError::NotAnErrorObject(val.clone()))?;
let try_get_property = |key: JsString, name, context: &mut Context| {
obj.try_get(key, context)
.map_err(|e| TryNativeError::InaccessibleProperty {
property: name,
source: e,
})
};
let message = if let Some(msg) =
try_get_property(js_string!("message"), "message", context)?
{
msg.as_string()
.map(JsString::to_std_string)
.transpose()
.map_err(|_| TryNativeError::InvalidMessageEncoding)?
.ok_or(TryNativeError::InvalidPropertyType("message"))?
.into()
} else {
Box::default()
};
let cause = try_get_property(js_string!("cause"), "cause", context)?;
let kind = match error {
ErrorObject::Error => JsNativeErrorKind::Error,
ErrorObject::Eval => JsNativeErrorKind::Eval,
ErrorObject::Type => JsNativeErrorKind::Type,
ErrorObject::Range => JsNativeErrorKind::Range,
ErrorObject::Reference => JsNativeErrorKind::Reference,
ErrorObject::Syntax => JsNativeErrorKind::Syntax,
ErrorObject::Uri => JsNativeErrorKind::Uri,
ErrorObject::Aggregate => {
let errors = obj.get(js_str!("errors"), context).map_err(|e| {
TryNativeError::InaccessibleProperty {
property: "errors",
source: e,
}
})?;
let mut error_list = Vec::new();
match errors.as_object() {
Some(errors) if errors.is_array() => {
let length = errors.length_of_array_like(context).map_err(|e| {
TryNativeError::InaccessibleProperty {
property: "errors.length",
source: e,
}
})?;
for i in 0..length {
error_list.push(Self::from_opaque(
errors.get(i, context).map_err(|e| {
TryNativeError::InvalidErrorsIndex {
index: i,
source: e,
}
})?,
));
}
}
_ => return Err(TryNativeError::InvalidPropertyType("errors")),
}
JsNativeErrorKind::Aggregate(error_list)
}
};
let realm = try_get_property(js_string!("constructor"), "constructor", context)?
.as_ref()
.and_then(JsValue::as_constructor)
.ok_or(TryNativeError::InvalidConstructor)?
.get_function_realm(context)
.map_err(|err| TryNativeError::InaccessibleRealm { source: err })?;
Ok(JsNativeError {
kind,
message,
cause: cause.map(|v| Box::new(Self::from_opaque(v))),
realm: Some(realm),
})
}
}
}
#[must_use]
pub const fn as_opaque(&self) -> Option<&JsValue> {
match self.inner {
Repr::Native(_) => None,
Repr::Opaque(ref v) => Some(v),
}
}
#[must_use]
pub const fn as_native(&self) -> Option<&JsNativeError> {
match &self.inner {
Repr::Native(e) => Some(e),
Repr::Opaque(_) => None,
}
}
pub fn into_erased(self, context: &mut Context) -> JsErasedError {
let Ok(native) = self.try_native(context) else {
return JsErasedError {
inner: ErasedRepr::Opaque(self.to_string().into_boxed_str()),
};
};
let JsNativeError {
kind,
message,
cause,
..
} = native;
let cause = cause.map(|err| Box::new(err.into_erased(context)));
let kind = match kind {
JsNativeErrorKind::Aggregate(errors) => JsErasedNativeErrorKind::Aggregate(
errors
.into_iter()
.map(|err| err.into_erased(context))
.collect(),
),
JsNativeErrorKind::Error => JsErasedNativeErrorKind::Error,
JsNativeErrorKind::Eval => JsErasedNativeErrorKind::Eval,
JsNativeErrorKind::Range => JsErasedNativeErrorKind::Range,
JsNativeErrorKind::Reference => JsErasedNativeErrorKind::Reference,
JsNativeErrorKind::Syntax => JsErasedNativeErrorKind::Syntax,
JsNativeErrorKind::Type => JsErasedNativeErrorKind::Type,
JsNativeErrorKind::Uri => JsErasedNativeErrorKind::Uri,
JsNativeErrorKind::RuntimeLimit => JsErasedNativeErrorKind::RuntimeLimit,
#[cfg(feature = "fuzz")]
JsNativeErrorKind::NoInstructionsRemain => unreachable!(
"The NoInstructionsRemain native error cannot be converted to an erased kind."
),
};
JsErasedError {
inner: ErasedRepr::Native(JsErasedNativeError {
kind,
message,
cause,
}),
}
}
pub(crate) fn inject_realm(mut self, realm: Realm) -> Self {
match &mut self.inner {
Repr::Native(err) if err.realm.is_none() => {
err.realm = Some(realm);
}
_ => {}
}
self
}
#[inline]
pub(crate) fn is_catchable(&self) -> bool {
self.as_native().map_or(true, JsNativeError::is_catchable)
}
}
impl From<boa_parser::Error> for JsError {
fn from(err: boa_parser::Error) -> Self {
Self::from(JsNativeError::from(err))
}
}
impl From<JsNativeError> for JsError {
fn from(error: JsNativeError) -> Self {
Self {
inner: Repr::Native(error),
}
}
}
impl fmt::Display for JsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.inner {
Repr::Native(e) => e.fmt(f),
Repr::Opaque(v) => v.display().fmt(f),
}
}
}
#[derive(Clone, Finalize, Error, PartialEq, Eq)]
pub struct JsNativeError {
pub kind: JsNativeErrorKind,
message: Box<str>,
#[source]
cause: Option<Box<JsError>>,
realm: Option<Realm>,
}
impl fmt::Display for JsNativeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
let message = self.message.trim();
if !message.is_empty() {
write!(f, ": {message}")?;
}
Ok(())
}
}
unsafe impl Trace for JsNativeError {
custom_trace!(this, mark, {
mark(&this.kind);
mark(&this.cause);
mark(&this.realm);
});
}
impl fmt::Debug for JsNativeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JsNativeError")
.field("kind", &self.kind)
.field("message", &self.message)
.field("cause", &self.cause)
.finish_non_exhaustive()
}
}
impl JsNativeError {
fn new(kind: JsNativeErrorKind, message: Box<str>, cause: Option<Box<JsError>>) -> Self {
Self {
kind,
message,
cause,
realm: None,
}
}
#[must_use]
#[inline]
pub fn aggregate(errors: Vec<JsError>) -> Self {
Self::new(JsNativeErrorKind::Aggregate(errors), Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_aggregate(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Aggregate(_))
}
#[must_use]
#[inline]
pub fn error() -> Self {
Self::new(JsNativeErrorKind::Error, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_error(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Error)
}
#[must_use]
#[inline]
pub fn eval() -> Self {
Self::new(JsNativeErrorKind::Eval, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_eval(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Eval)
}
#[must_use]
#[inline]
pub fn range() -> Self {
Self::new(JsNativeErrorKind::Range, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_range(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Range)
}
#[must_use]
#[inline]
pub fn reference() -> Self {
Self::new(JsNativeErrorKind::Reference, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_reference(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Reference)
}
#[must_use]
#[inline]
pub fn syntax() -> Self {
Self::new(JsNativeErrorKind::Syntax, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_syntax(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Syntax)
}
#[must_use]
#[inline]
pub fn typ() -> Self {
Self::new(JsNativeErrorKind::Type, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_type(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Type)
}
#[must_use]
#[inline]
pub fn uri() -> Self {
Self::new(JsNativeErrorKind::Uri, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_uri(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Uri)
}
#[cfg(feature = "fuzz")]
#[must_use]
pub fn no_instructions_remain() -> Self {
Self::new(
JsNativeErrorKind::NoInstructionsRemain,
Box::default(),
None,
)
}
#[must_use]
#[inline]
#[cfg(feature = "fuzz")]
pub const fn is_no_instructions_remain(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::NoInstructionsRemain)
}
#[must_use]
#[inline]
pub fn runtime_limit() -> Self {
Self::new(JsNativeErrorKind::RuntimeLimit, Box::default(), None)
}
#[must_use]
#[inline]
pub const fn is_runtime_limit(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::RuntimeLimit)
}
#[must_use]
#[inline]
pub fn with_message<S>(mut self, message: S) -> Self
where
S: Into<Box<str>>,
{
self.message = message.into();
self
}
#[must_use]
#[inline]
pub fn with_cause<V>(mut self, cause: V) -> Self
where
V: Into<JsError>,
{
self.cause = Some(Box::new(cause.into()));
self
}
#[must_use]
#[inline]
pub const fn message(&self) -> &str {
&self.message
}
#[must_use]
#[inline]
pub fn cause(&self) -> Option<&JsError> {
self.cause.as_deref()
}
#[inline]
pub fn to_opaque(&self, context: &mut Context) -> JsObject {
let Self {
kind,
message,
cause,
realm,
} = self;
let constructors = realm.as_ref().map_or_else(
|| context.intrinsics().constructors(),
|realm| realm.intrinsics().constructors(),
);
let (prototype, tag) = match kind {
JsNativeErrorKind::Aggregate(_) => (
constructors.aggregate_error().prototype(),
ErrorObject::Aggregate,
),
JsNativeErrorKind::Error => (constructors.error().prototype(), ErrorObject::Error),
JsNativeErrorKind::Eval => (constructors.eval_error().prototype(), ErrorObject::Eval),
JsNativeErrorKind::Range => {
(constructors.range_error().prototype(), ErrorObject::Range)
}
JsNativeErrorKind::Reference => (
constructors.reference_error().prototype(),
ErrorObject::Reference,
),
JsNativeErrorKind::Syntax => {
(constructors.syntax_error().prototype(), ErrorObject::Syntax)
}
JsNativeErrorKind::Type => (constructors.type_error().prototype(), ErrorObject::Type),
JsNativeErrorKind::Uri => (constructors.uri_error().prototype(), ErrorObject::Uri),
#[cfg(feature = "fuzz")]
JsNativeErrorKind::NoInstructionsRemain => {
unreachable!(
"The NoInstructionsRemain native error cannot be converted to an opaque type."
)
}
JsNativeErrorKind::RuntimeLimit => {
panic!("The RuntimeLimit native error cannot be converted to an opaque type.")
}
};
let o =
JsObject::from_proto_and_data_with_shared_shape(context.root_shape(), prototype, tag);
o.create_non_enumerable_data_property_or_throw(
js_str!("message"),
js_string!(&**message),
context,
);
if let Some(cause) = cause {
o.create_non_enumerable_data_property_or_throw(
js_str!("cause"),
cause.to_opaque(context),
context,
);
}
if let JsNativeErrorKind::Aggregate(errors) = kind {
let errors = errors
.iter()
.map(|e| e.to_opaque(context))
.collect::<Vec<_>>();
let errors = Array::create_array_from_list(errors, context);
o.define_property_or_throw(
js_str!("errors"),
PropertyDescriptor::builder()
.configurable(true)
.enumerable(false)
.writable(true)
.value(errors),
context,
)
.expect("The spec guarantees this succeeds for a newly created object ");
}
o
}
pub(crate) fn with_realm(mut self, realm: Realm) -> Self {
self.realm = Some(realm);
self
}
#[inline]
pub(crate) fn is_catchable(&self) -> bool {
self.kind.is_catchable()
}
}
impl From<boa_parser::Error> for JsNativeError {
fn from(err: boa_parser::Error) -> Self {
Self::syntax().with_message(err.to_string())
}
}
#[derive(Debug, Clone, Finalize, PartialEq, Eq)]
#[non_exhaustive]
pub enum JsNativeErrorKind {
Aggregate(Vec<JsError>),
Error,
Eval,
Range,
Reference,
Syntax,
Type,
Uri,
#[cfg(feature = "fuzz")]
NoInstructionsRemain,
RuntimeLimit,
}
unsafe impl Trace for JsNativeErrorKind {
custom_trace!(
this,
mark,
match &this {
Self::Aggregate(errors) => mark(errors),
Self::Error
| Self::Eval
| Self::Range
| Self::Reference
| Self::Syntax
| Self::Type
| Self::Uri
| Self::RuntimeLimit => {}
#[cfg(feature = "fuzz")]
Self::NoInstructionsRemain => {}
}
);
}
impl JsNativeErrorKind {
#[inline]
pub(crate) fn is_catchable(&self) -> bool {
match self {
Self::Aggregate(_)
| Self::Error
| Self::Eval
| Self::Range
| Self::Reference
| Self::Syntax
| Self::Type
| Self::Uri => true,
Self::RuntimeLimit => false,
#[cfg(feature = "fuzz")]
Self::NoInstructionsRemain => false,
}
}
}
impl PartialEq<ErrorObject> for JsNativeErrorKind {
fn eq(&self, other: &ErrorObject) -> bool {
matches!(
(self, other),
(Self::Aggregate(_), ErrorObject::Aggregate)
| (Self::Error, ErrorObject::Error)
| (Self::Eval, ErrorObject::Eval)
| (Self::Range, ErrorObject::Range)
| (Self::Reference, ErrorObject::Reference)
| (Self::Syntax, ErrorObject::Syntax)
| (Self::Type, ErrorObject::Type)
| (Self::Uri, ErrorObject::Uri)
)
}
}
impl fmt::Display for JsNativeErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Aggregate(_) => "AggregateError",
Self::Error => "Error",
Self::Eval => "EvalError",
Self::Range => "RangeError",
Self::Reference => "ReferenceError",
Self::Syntax => "SyntaxError",
Self::Type => "TypeError",
Self::Uri => "UriError",
Self::RuntimeLimit => "RuntimeLimit",
#[cfg(feature = "fuzz")]
Self::NoInstructionsRemain => "NoInstructionsRemain",
}
.fmt(f)
}
}
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
pub struct JsErasedError {
inner: ErasedRepr,
}
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
enum ErasedRepr {
Native(JsErasedNativeError),
Opaque(Box<str>),
}
impl fmt::Display for JsErasedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.inner {
ErasedRepr::Native(e) => e.fmt(f),
ErasedRepr::Opaque(v) => fmt::Display::fmt(v, f),
}
}
}
impl error::Error for JsErasedError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match &self.inner {
ErasedRepr::Native(err) => err.source(),
ErasedRepr::Opaque(_) => None,
}
}
}
impl JsErasedError {
#[must_use]
pub const fn as_opaque(&self) -> Option<&str> {
match self.inner {
ErasedRepr::Native(_) => None,
ErasedRepr::Opaque(ref v) => Some(v),
}
}
#[must_use]
pub const fn as_native(&self) -> Option<&JsErasedNativeError> {
match &self.inner {
ErasedRepr::Native(e) => Some(e),
ErasedRepr::Opaque(_) => None,
}
}
}
#[derive(Debug, Clone, Trace, Finalize, Error, PartialEq, Eq)]
pub struct JsErasedNativeError {
pub kind: JsErasedNativeErrorKind,
message: Box<str>,
#[source]
cause: Option<Box<JsErasedError>>,
}
impl fmt::Display for JsErasedNativeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.kind)?;
let message = self.message.trim();
if !message.is_empty() {
write!(f, ": {message}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
#[non_exhaustive]
pub enum JsErasedNativeErrorKind {
Aggregate(Vec<JsErasedError>),
Error,
Eval,
Range,
Reference,
Syntax,
Type,
Uri,
RuntimeLimit,
}
impl fmt::Display for JsErasedNativeErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Self::Aggregate(errors) => {
return write!(f, "AggregateError(error count: {})", errors.len());
}
Self::Error => "Error",
Self::Eval => "EvalError",
Self::Range => "RangeError",
Self::Reference => "ReferenceError",
Self::Syntax => "SyntaxError",
Self::Type => "TypeError",
Self::Uri => "UriError",
Self::RuntimeLimit => "RuntimeLimit",
}
.fmt(f)
}
}