use crate::{
builtins::{error::ErrorKind, Array},
object::JsObject,
object::ObjectData,
property::PropertyDescriptor,
realm::Realm,
string::utf16,
Context, JsString, JsValue,
};
use boa_gc::{Finalize, Trace};
use thiserror::Error;
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
pub struct JsError {
inner: Repr,
}
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
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 std::error::Error for JsError {
fn source(&self) -> Option<&(dyn std::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),
}
}
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
.borrow()
.as_error()
.ok_or_else(|| TryNativeError::NotAnErrorObject(val.clone()))?;
let try_get_property = |key, context: &mut Context<'_>| {
obj.has_property(key, context)
.map_err(|e| TryNativeError::InaccessibleProperty {
property: key,
source: e,
})?
.then(|| obj.get(key, context))
.transpose()
.map_err(|e| TryNativeError::InaccessibleProperty {
property: key,
source: e,
})
};
let message = if let Some(msg) = try_get_property("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("cause", context)?;
let kind = match error {
ErrorKind::Error => JsNativeErrorKind::Error,
ErrorKind::Eval => JsNativeErrorKind::Eval,
ErrorKind::Type => JsNativeErrorKind::Type,
ErrorKind::Range => JsNativeErrorKind::Range,
ErrorKind::Reference => JsNativeErrorKind::Reference,
ErrorKind::Syntax => JsNativeErrorKind::Syntax,
ErrorKind::Uri => JsNativeErrorKind::Uri,
ErrorKind::Aggregate => {
let errors = obj.get(utf16!("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("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),
})
}
}
}
pub const fn as_opaque(&self) -> Option<&JsValue> {
match self.inner {
Repr::Native(_) => None,
Repr::Opaque(ref v) => Some(v),
}
}
pub const fn as_native(&self) -> Option<&JsNativeError> {
match &self.inner {
Repr::Native(e) => Some(e),
Repr::Opaque(_) => None,
}
}
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
}
}
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 std::fmt::Display for JsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.inner {
Repr::Native(e) => e.fmt(f),
Repr::Opaque(v) => v.display().fmt(f),
}
}
}
#[derive(Clone, Trace, Finalize, Error, PartialEq, Eq)]
#[error("{kind}: {message}")]
pub struct JsNativeError {
pub kind: JsNativeErrorKind,
message: Box<str>,
#[source]
cause: Option<Box<JsError>>,
realm: Option<Realm>,
}
impl std::fmt::Debug for JsNativeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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)
}
#[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)
}
#[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)
}
#[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)
}
#[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)
}
#[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)
}
#[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)
}
#[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)
}
#[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,
)
}
#[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)
}
#[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(),
ErrorKind::Aggregate,
),
JsNativeErrorKind::Error => (constructors.error().prototype(), ErrorKind::Error),
JsNativeErrorKind::Eval => (constructors.eval_error().prototype(), ErrorKind::Eval),
JsNativeErrorKind::Range => (constructors.range_error().prototype(), ErrorKind::Range),
JsNativeErrorKind::Reference => (
constructors.reference_error().prototype(),
ErrorKind::Reference,
),
JsNativeErrorKind::Syntax => {
(constructors.syntax_error().prototype(), ErrorKind::Syntax)
}
JsNativeErrorKind::Type => (constructors.type_error().prototype(), ErrorKind::Type),
JsNativeErrorKind::Uri => (constructors.uri_error().prototype(), ErrorKind::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,
ObjectData::error(tag),
);
o.create_non_enumerable_data_property_or_throw(utf16!("message"), &**message, context);
if let Some(cause) = cause {
o.create_non_enumerable_data_property_or_throw(
utf16!("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(
utf16!("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
}
}
impl From<boa_parser::Error> for JsNativeError {
fn from(err: boa_parser::Error) -> Self {
Self::syntax().with_message(err.to_string())
}
}
#[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
#[non_exhaustive]
pub enum JsNativeErrorKind {
Aggregate(Vec<JsError>),
Error,
Eval,
Range,
Reference,
Syntax,
Type,
Uri,
#[cfg(feature = "fuzz")]
NoInstructionsRemain,
RuntimeLimit,
}
impl PartialEq<ErrorKind> for JsNativeErrorKind {
fn eq(&self, other: &ErrorKind) -> bool {
matches!(
(self, other),
(Self::Aggregate(_), ErrorKind::Aggregate)
| (Self::Error, ErrorKind::Error)
| (Self::Eval, ErrorKind::Eval)
| (Self::Range, ErrorKind::Range)
| (Self::Reference, ErrorKind::Reference)
| (Self::Syntax, ErrorKind::Syntax)
| (Self::Type, ErrorKind::Type)
| (Self::Uri, ErrorKind::Uri)
)
}
}
impl std::fmt::Display for JsNativeErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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)
}
}