diff --git a/core/runtime/src/clone/mod.rs b/core/runtime/src/clone/mod.rs index c98654183e1..7902860d8c6 100644 --- a/core/runtime/src/clone/mod.rs +++ b/core/runtime/src/clone/mod.rs @@ -7,6 +7,9 @@ use boa_engine::realm::Realm; use boa_engine::value::TryFromJs; use boa_engine::{Context, JsResult, JsValue, boa_module}; +#[cfg(test)] +mod tests; + /// Options used by `structuredClone`. This is currently unused. #[derive(Debug, Clone, TryFromJs)] pub struct StructuredCloneOptions { diff --git a/core/runtime/src/clone/tests.rs b/core/runtime/src/clone/tests.rs new file mode 100644 index 00000000000..6b9f2cb4975 --- /dev/null +++ b/core/runtime/src/clone/tests.rs @@ -0,0 +1,74 @@ +//! Tests for the `structuredClone` extension. + +use crate::test::{TestAction, run_test_actions}; + +#[test] +fn clones_error_objects() { + run_test_actions([ + TestAction::harness(), + TestAction::run( + r#" + const original = new Error("boom"); + const cloned = structuredClone(original); + + assert(cloned instanceof Error); + assert(cloned !== original); + assertEq(cloned.name, "Error"); + assertEq(cloned.message, "boom"); + "#, + ), + ]); +} + +#[test] +fn clones_error_object_cause() { + run_test_actions([ + TestAction::harness(), + TestAction::run( + r#" + const original = new Error("boom", { cause: { code: 7 } }); + const cloned = structuredClone(original); + + assert(cloned instanceof Error); + assert(cloned.cause !== original.cause); + assertEq(cloned.cause.code, 7); + "#, + ), + ]); +} + +#[test] +fn clones_aggregate_error_entries() { + run_test_actions([ + TestAction::harness(), + TestAction::run( + r#" + const original = new AggregateError([new Error("inner")], "agg"); + const cloned = structuredClone(original); + + assert(cloned instanceof AggregateError); + assertEq(cloned.message, "agg"); + assertEq(cloned.errors.length, 1); + assert(cloned.errors[0] instanceof Error); + assertEq(cloned.errors[0].message, "inner"); + "#, + ), + ]); +} + +#[test] +fn clones_error_with_undefined_cause_property() { + run_test_actions([ + TestAction::harness(), + TestAction::run( + r#" + const original = new Error("boom", { cause: undefined }); + const cloned = structuredClone(original); + + assert(Object.hasOwn(original, "cause")); + assert(Object.hasOwn(cloned, "cause")); + assertEq(cloned.cause, undefined); + "#, + ), + ]); +} diff --git a/core/runtime/src/message/tests.rs b/core/runtime/src/message/tests.rs index 66d598f87d4..228105e14ef 100644 --- a/core/runtime/src/message/tests.rs +++ b/core/runtime/src/message/tests.rs @@ -53,6 +53,135 @@ fn basic() { ); } +#[test] +fn basic_error_message() { + let context = &mut Context::default(); + + let sender = OnMessageQueueSender::create(context); + message::register(sender, None, context).unwrap(); + + run_test_actions_with( + [ + TestAction::harness(), + TestAction::run( + r#" + let latestMessage = null; + function onMessageQueue(message) { + latestMessage = message; + } + + const message = new Error("boom"); + postMessage(message); + assert(latestMessage === null); + "#, + ), + TestAction::inspect_context(move |context| { + drop(future::block_on(future::poll_once( + context + .downcast_job_executor::() + .expect("") + .run_jobs_async(&RefCell::new(context)), + ))); + }), + TestAction::run( + r#" + assert(latestMessage instanceof Error); + assert(latestMessage !== message); + assertEq(latestMessage.name, "Error"); + assertEq(latestMessage.message, "boom"); + "#, + ), + ], + context, + ); +} + +#[test] +fn basic_error_with_object_cause() { + let context = &mut Context::default(); + + let sender = OnMessageQueueSender::create(context); + message::register(sender, None, context).unwrap(); + + run_test_actions_with( + [ + TestAction::harness(), + TestAction::run( + r#" + let latestMessage = null; + function onMessageQueue(message) { + latestMessage = message; + } + + const message = new Error("boom", { cause: { code: 7 } }); + postMessage(message); + assert(latestMessage === null); + "#, + ), + TestAction::inspect_context(move |context| { + drop(future::block_on(future::poll_once( + context + .downcast_job_executor::() + .expect("") + .run_jobs_async(&RefCell::new(context)), + ))); + }), + TestAction::run( + r#" + assert(latestMessage instanceof Error); + assert(latestMessage !== message); + assertEq(latestMessage.message, "boom"); + assertEq(latestMessage.cause.code, 7); + "#, + ), + ], + context, + ); +} + +#[test] +fn basic_error_with_undefined_cause_property() { + let context = &mut Context::default(); + + let sender = OnMessageQueueSender::create(context); + message::register(sender, None, context).unwrap(); + + run_test_actions_with( + [ + TestAction::harness(), + TestAction::run( + r#" + let latestMessage = null; + function onMessageQueue(message) { + latestMessage = message; + } + + const message = new Error("boom", { cause: undefined }); + postMessage(message); + assert(latestMessage === null); + "#, + ), + TestAction::inspect_context(move |context| { + drop(future::block_on(future::poll_once( + context + .downcast_job_executor::() + .expect("") + .run_jobs_async(&RefCell::new(context)), + ))); + }), + TestAction::run( + r#" + assert(latestMessage instanceof Error); + assert(latestMessage !== message); + assert(Object.hasOwn(latestMessage, "cause")); + assertEq(latestMessage.cause, undefined); + "#, + ), + ], + context, + ); +} + #[test] fn shared_multi_thread() { let (sender, receiver) = std::sync::mpsc::channel::(); diff --git a/core/runtime/src/store/from.rs b/core/runtime/src/store/from.rs index 1ee7682d420..4054903e1f6 100644 --- a/core/runtime/src/store/from.rs +++ b/core/runtime/src/store/from.rs @@ -2,13 +2,16 @@ use crate::store::{JsValueStore, StringStore, ValueStoreInner, unsupported_type}; use boa_engine::builtins::array_buffer::{AlignedVec, ArrayBuffer}; -use boa_engine::builtins::error::Error; +use boa_engine::builtins::error::{Error, ErrorKind}; use boa_engine::object::builtins::{ JsArray, JsArrayBuffer, JsDataView, JsDate, JsMap, JsRegExp, JsSet, JsSharedArrayBuffer, JsTypedArray, }; use boa_engine::property::PropertyKey; -use boa_engine::{Context, JsError, JsObject, JsResult, JsString, JsValue, JsVariant, js_error}; +use boa_engine::{ + Context, JsError, JsNativeErrorKind, JsObject, JsResult, JsString, JsValue, JsVariant, + js_error, js_string, +}; use std::collections::{HashMap, HashSet}; /// A Map of seen objects when walking through the value. We use the address @@ -181,6 +184,88 @@ fn clone_regexp( Ok(stored) } +fn clone_error( + original: &JsObject, + transfer: &HashSet, + seen: &mut SeenMap, + context: &mut Context, +) -> JsResult { + let mut store = JsValueStore::empty(); + seen.insert(original, store.clone()); + + let native = JsError::from_opaque(JsValue::from(original.clone())) + .try_native(context) + .map_err(|_| unsupported_type())?; + + let kind = match native.kind() { + JsNativeErrorKind::Aggregate(_) => ErrorKind::Aggregate, + JsNativeErrorKind::Eval => ErrorKind::Eval, + JsNativeErrorKind::Type => ErrorKind::Type, + JsNativeErrorKind::Range => ErrorKind::Range, + JsNativeErrorKind::Reference => ErrorKind::Reference, + JsNativeErrorKind::Syntax => ErrorKind::Syntax, + JsNativeErrorKind::Uri => ErrorKind::Uri, + _ => ErrorKind::Error, + }; + + let to_optional_string = |key: &str, context: &mut Context| -> JsResult { + let value = original.get(js_string!(key), context)?; + if value.is_undefined() { + Ok(js_string!()) + } else { + value.to_string(context) + } + }; + + let name = to_optional_string("name", context)?; + let stack = to_optional_string("stack", context)?; + + let cause = if original.has_own_property(js_string!("cause"), context)? { + let cause = original.get(js_string!("cause"), context)?; + Some(try_from_js_value(&cause, transfer, seen, context)?) + } else { + None + }; + + let errors = if matches!(kind, ErrorKind::Aggregate) { + let errors = original.get(js_string!("errors"), context)?; + if errors.is_undefined() { + Vec::new() + } else { + let Some(errors) = errors.as_object() else { + return Err(unsupported_type()); + }; + + let errors = JsArray::from_object(errors).map_err(|_| unsupported_type())?; + + let length = errors.length(context)?; + let length = usize::try_from(length).map_err(JsError::from_rust)?; + let mut values = Vec::with_capacity(length); + for i in 0..length { + let value = errors.get(i, context)?; + values.push(try_from_js_value(&value, transfer, seen, context)?); + } + values + } + } else { + Vec::new() + }; + + // SAFETY: This is safe as this function is the sole owner of the store. + unsafe { + store.replace(ValueStoreInner::Error { + kind, + name: name.into(), + message: JsString::from(native.message()).into(), + stack: stack.into(), + cause, + errors, + }); + } + + Ok(store) +} + fn try_from_map( original: &JsObject, map: &JsMap, @@ -258,8 +343,8 @@ fn try_from_js_object_clone( return clone_typed_array(object, typed_array, transfer, seen, context); } else if let Ok(ref date) = JsDate::from_object(object.clone()) { return clone_date(object, date, seen, context); - } else if let Ok(_error) = object.clone().downcast::() { - return Err(js_error!(TypeError: "Errors are not supported yet.")); + } else if object.downcast_ref::().is_some() { + return clone_error(object, transfer, seen, context); } else if let Ok(ref regexp) = JsRegExp::from_object(object.clone()) { return clone_regexp(object, regexp, seen, context); } else if let Ok(_dataview) = JsDataView::from_object(object.clone()) { diff --git a/core/runtime/src/store/mod.rs b/core/runtime/src/store/mod.rs index 432a92bbaea..f6b56613963 100644 --- a/core/runtime/src/store/mod.rs +++ b/core/runtime/src/store/mod.rs @@ -96,13 +96,13 @@ enum ValueStoreInner { Date(f64), /// Allowed error types (see the structured clone algorithm page). - #[expect(unused)] Error { kind: ErrorKind, name: StringStore, message: StringStore, stack: StringStore, - cause: StringStore, + cause: Option, + errors: Vec, }, /// Regular expression. We store the expression and its flags. Everything else diff --git a/core/runtime/src/store/to.rs b/core/runtime/src/store/to.rs index 071093590ae..e769a787cfe 100644 --- a/core/runtime/src/store/to.rs +++ b/core/runtime/src/store/to.rs @@ -1,17 +1,29 @@ //! All methods for deserializing a [`JsValueStore`] into a [`JsValue`]. use crate::store::{JsValueStore, StringStore, ValueStoreInner, unsupported_type}; use boa_engine::builtins::array_buffer::{AlignedVec, SharedArrayBuffer}; +use boa_engine::builtins::error::ErrorKind; use boa_engine::builtins::typed_array::TypedArrayKind; use boa_engine::object::builtins::{ JsArray, JsArrayBuffer, JsDataView, JsDate, JsMap, JsRegExp, JsSet, JsSharedArrayBuffer, js_typed_array_from_kind, }; -use boa_engine::{Context, JsBigInt, JsObject, JsResult, JsString, JsValue, js_error}; +use boa_engine::{ + Context, JsBigInt, JsNativeError, JsObject, JsResult, JsString, JsValue, js_string, +}; use std::collections::HashMap; #[derive(Default)] pub(super) struct ReverseSeenMap(HashMap); +type ErrorStoreFields<'a> = ( + &'a ErrorKind, + &'a StringStore, + &'a StringStore, + &'a StringStore, + &'a Option, + &'a [JsValueStore], +); + impl ReverseSeenMap { fn get(&self, object: &JsValueStore) -> Option { let addr = std::ptr::from_ref(object.0.as_ref()).addr(); @@ -150,6 +162,62 @@ fn try_into_js_date( Ok(JsValue::from(date)) } +fn try_into_js_error( + store: &JsValueStore, + error_data: ErrorStoreFields<'_>, + seen: &mut ReverseSeenMap, + context: &mut Context, +) -> JsResult { + let (kind, name, message, stack, cause, errors) = error_data; + let message = message.to_js_string().to_std_string_escaped(); + let native = match kind { + ErrorKind::Aggregate => JsNativeError::aggregate(Vec::new()), + ErrorKind::Eval => JsNativeError::eval(), + ErrorKind::Type => JsNativeError::typ(), + ErrorKind::Range => JsNativeError::range(), + ErrorKind::Reference => JsNativeError::reference(), + ErrorKind::Syntax => JsNativeError::syntax(), + ErrorKind::Uri => JsNativeError::uri(), + _ => JsNativeError::error(), + } + .with_message(message); + + let error = native.into_opaque(context); + seen.insert(store, error.clone()); + + let name = name.to_js_string(); + if !name.is_empty() { + error.set(js_string!("name"), name, true, context)?; + } + + let stack = stack.to_js_string(); + if !stack.is_empty() { + error.set(js_string!("stack"), stack, true, context)?; + } + + if let Some(cause) = cause { + let cause = try_value_into_js(cause, seen, context)?; + error.set(js_string!("cause"), cause, true, context)?; + } + + if !errors.is_empty() { + let errors_array = JsArray::new(context)?; + for (index, value) in errors.iter().enumerate() { + let value = try_value_into_js(value, seen, context)?; + errors_array.set(index, value, true, context)?; + } + + error.set( + js_string!("errors"), + JsValue::from(errors_array), + true, + context, + )?; + } + + Ok(JsValue::from(error)) +} + fn try_into_regexp( store: &JsValueStore, source: &str, @@ -207,7 +275,19 @@ pub(super) fn try_value_into_js( ValueStoreInner::Set(values) => try_into_js_set(store, values, seen, context), ValueStoreInner::Array(items) => try_items_into_js_array(store, items, seen, context), ValueStoreInner::Date(msec) => try_into_js_date(store, *msec, seen, context), - ValueStoreInner::Error { .. } => Err(js_error!("Not yet implemented.")), + ValueStoreInner::Error { + kind, + name, + message, + stack, + cause, + errors, + } => try_into_js_error( + store, + (kind, name, message, stack, cause, errors), + seen, + context, + ), ValueStoreInner::RegExp { source, flags } => { try_into_regexp(store, source, flags, seen, context) }