diff --git a/Cargo.lock b/Cargo.lock index fb34bccb96d..f080117fe54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,7 @@ dependencies = [ "static_assertions", "sys-locale", "tap", + "thiserror", "unicode-normalization", ] diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 1e4ed384f22..88694009e0e 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -118,7 +118,6 @@ impl Opt { #[derive(Debug, Clone, ValueEnum)] enum DumpFormat { /// The different types of format available for dumping. - /// // NOTE: This can easily support other formats just by // adding a field to this enum and adding the necessary // implementation. Example: Toml, Html, etc. diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 35a8d5d8c56..a93bdd72960 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -53,6 +53,7 @@ once_cell = "1.15.0" tap = "1.0.1" sptr = "0.3.2" static_assertions = "1.1.0" +thiserror = "1.0.35" icu_locale_canonicalizer = { version = "0.6.0", features = ["serde"], optional = true } icu_locid = { version = "0.6.0", features = ["serde"], optional = true } icu_datetime = { version = "0.6.0", features = ["serde"], optional = true } diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index a42c2a5912a..e6ea7180641 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -16,7 +16,7 @@ use crate::{ symbol::WellKnownSymbols, value::JsValue, vm::GeneratorResumeKind, - Context, JsResult, + Context, JsError, JsResult, }; use boa_gc::{Cell, Finalize, Gc, Trace}; use boa_profiler::Profiler; @@ -399,7 +399,10 @@ impl AsyncGenerator { } // 8. Let completion be ThrowCompletion(exception). - let completion = (Err(args.get_or_undefined(0).clone().into()), false); + let completion = ( + Err(JsError::from_opaque(args.get_or_undefined(0).clone())), + false, + ); // 9. Perform AsyncGeneratorEnqueue(generator, completion, promiseCapability). generator.enqueue(completion.clone(), promise_capability.clone()); @@ -473,7 +476,7 @@ impl AsyncGenerator { // a. Perform ! Call(promiseCapability.[[Reject]], undefined, « value »). promise_capability .reject() - .call(&JsValue::undefined(), &[e.to_value(context)], context) + .call(&JsValue::undefined(), &[e.to_opaque(context)], context) .expect("cannot fail per spec"); } // 8. Else, @@ -552,7 +555,7 @@ impl AsyncGenerator { } } (Err(value), _) => { - let value = value.to_value(context); + let value = value.to_opaque(context); context.vm.push(value); context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw; } @@ -669,7 +672,7 @@ impl AsyncGenerator { gen.state = AsyncGeneratorState::Completed; // b. Let result be ThrowCompletion(reason). - let result = Err(args.get_or_undefined(0).clone().into()); + let result = Err(JsError::from_opaque(args.get_or_undefined(0).clone())); // c. Perform AsyncGeneratorCompleteStep(generator, result, true). let next = gen.queue.pop_front().expect("must have one entry"); diff --git a/boa_engine/src/builtins/generator/mod.rs b/boa_engine/src/builtins/generator/mod.rs index 0a737a7ea74..74eea06fc13 100644 --- a/boa_engine/src/builtins/generator/mod.rs +++ b/boa_engine/src/builtins/generator/mod.rs @@ -18,7 +18,7 @@ use crate::{ symbol::WellKnownSymbols, value::JsValue, vm::{CallFrame, GeneratorResumeKind, ReturnType}, - Context, JsResult, + Context, JsError, JsResult, }; use boa_gc::{Cell, Finalize, Gc, Trace}; use boa_profiler::Profiler; @@ -198,7 +198,11 @@ impl Generator { // 1. Let g be the this value. // 2. Let C be ThrowCompletion(exception). // 3. Return ? GeneratorResumeAbrupt(g, C, empty). - Self::generator_resume_abrupt(this, Err(args.get_or_undefined(0).clone().into()), context) + Self::generator_resume_abrupt( + this, + Err(JsError::from_opaque(args.get_or_undefined(0).clone())), + context, + ) } /// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )` @@ -386,7 +390,7 @@ impl Generator { context.run() } Err(value) => { - let value = value.to_value(context); + let value = value.to_opaque(context); context.vm.push(value); context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw; context.run() diff --git a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs index 642d4f90f9c..86d9e983837 100644 --- a/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs +++ b/boa_engine/src/builtins/iterable/async_from_sync_iterator.rs @@ -244,7 +244,8 @@ impl AsyncFromSyncIterator { &JsValue::Undefined, &[JsNativeError::typ() .with_message("iterator return function returned non-object") - .to_value(context)], + .to_opaque(context) + .into()], context, ) .expect("cannot fail according to spec"); @@ -343,7 +344,8 @@ impl AsyncFromSyncIterator { &JsValue::Undefined, &[JsNativeError::typ() .with_message("iterator throw function returned non-object") - .to_value(context)], + .to_opaque(context) + .into()], context, ) .expect("cannot fail according to spec"); diff --git a/boa_engine/src/builtins/number/mod.rs b/boa_engine/src/builtins/number/mod.rs index a41c0ec8e7c..375a36e3e73 100644 --- a/boa_engine/src/builtins/number/mod.rs +++ b/boa_engine/src/builtins/number/mod.rs @@ -327,7 +327,6 @@ impl Number { /// /// This function traverses a string representing a number, /// returning the floored log10 of this number. - /// fn flt_str_to_exp(flt: &str) -> i32 { let mut non_zero_encountered = false; let mut dot_encountered = false; @@ -361,7 +360,6 @@ impl Number { /// the exponent. The string is kept at an exact length of `precision`. /// /// When this procedure returns, `digits` is exactly `precision` long. - /// fn round_to_precision(digits: &mut String, precision: usize) -> bool { if digits.len() > precision { let to_round = digits.split_off(precision); diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 02c5470eb66..ff0ee49acd2 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -19,7 +19,7 @@ use crate::{ property::{Attribute, PropertyDescriptorBuilder}, symbol::WellKnownSymbols, value::JsValue, - Context, JsResult, + Context, JsError, JsResult, }; use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; use boa_profiler::Profiler; @@ -39,7 +39,7 @@ macro_rules! if_abrupt_reject_promise { let $value = match $value { // 1. If value is an abrupt completion, then Err(err) => { - let err = err.to_value($context); + let err = err.to_opaque($context); // a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »). $context.call( &$capability.reject().clone().into(), @@ -338,7 +338,7 @@ impl Promise { // 10. If completion is an abrupt completion, then if let Err(e) = completion { - let e = e.to_value(context); + let e = e.to_opaque(context); // a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »). context.call(&resolving_functions.reject, &JsValue::Undefined, &[e])?; } @@ -1064,7 +1064,7 @@ impl Promise { .expect("cannot fail per spec"); // 3. Return ThrowCompletion(error). - return Err(error.into()); + return Err(JsError::from_opaque(error.into())); } // iv. Return resultCapability.[[Promise]]. @@ -1255,14 +1255,14 @@ impl Promise { // a. Let selfResolutionError be a newly created TypeError object. let self_resolution_error = JsNativeError::typ() .with_message("SameValue(resolution, promise) is true") - .to_value(context); + .to_opaque(context); // b. Perform RejectPromise(promise, selfResolutionError). promise .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject_promise(&self_resolution_error, context); + .reject_promise(&self_resolution_error.into(), context); // c. Return undefined. return Ok(JsValue::Undefined); @@ -1292,7 +1292,7 @@ impl Promise { .borrow_mut() .as_promise_mut() .expect("Expected promise to be a Promise") - .reject_promise(&e.to_value(context), context); + .reject_promise(&e.to_opaque(context), context); // b. Return undefined. return Ok(JsValue::Undefined); @@ -1834,7 +1834,7 @@ impl Promise { context, |_this, _args, captures, _context| { // 1. Return ThrowCompletion(reason). - Err(captures.reason.clone().into()) + Err(JsError::from_opaque(captures.reason.clone())) }, ThrowReasonCaptures { reason: reason.clone(), diff --git a/boa_engine/src/builtins/promise/promise_job.rs b/boa_engine/src/builtins/promise/promise_job.rs index 8361e947aa3..af97c6887b6 100644 --- a/boa_engine/src/builtins/promise/promise_job.rs +++ b/boa_engine/src/builtins/promise/promise_job.rs @@ -56,7 +56,7 @@ impl PromiseJob { // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). Some(handler) => handler .call_job_callback(&JsValue::Undefined, &[argument.clone()], context) - .map_err(|e| e.to_value(context)), + .map_err(|e| e.to_opaque(context)), }; match promise_capability { @@ -146,7 +146,7 @@ impl PromiseJob { // c. If thenCallResult is an abrupt completion, then if let Err(value) = then_call_result { - let value = value.to_value(context); + let value = value.to_opaque(context); // i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »). return context.call( &resolving_functions.reject, diff --git a/boa_engine/src/class.rs b/boa_engine/src/class.rs index 4c789a542ad..d06c5b85306 100644 --- a/boa_engine/src/class.rs +++ b/boa_engine/src/class.rs @@ -2,14 +2,14 @@ //! //! Native classes are implemented through the [`Class`][class-trait] trait. //! ``` -//!# use boa_engine::{ -//!# property::Attribute, -//!# class::{Class, ClassBuilder}, -//!# Context, JsResult, JsValue, -//!# builtins::JsArgs, -//!# }; -//!# use boa_gc::{Finalize, Trace}; -//!# +//! # use boa_engine::{ +//! # property::Attribute, +//! # class::{Class, ClassBuilder}, +//! # Context, JsResult, JsValue, +//! # builtins::JsArgs, +//! # }; +//! # use boa_gc::{Finalize, Trace}; +//! # //! // This does not have to be an enum it can also be a struct. //! #[derive(Debug, Trace, Finalize)] //! enum Animal { diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 1cad90c053e..4c64ed42099 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -48,9 +48,9 @@ pub use icu::BoaProvider; /// /// ```rust /// use boa_engine::{ -/// Context, /// object::ObjectInitializer, -/// property::{Attribute, PropertyDescriptor} +/// property::{Attribute, PropertyDescriptor}, +/// Context, /// }; /// /// let script = r#" @@ -71,11 +71,7 @@ pub use icu::BoaProvider; /// let arg = ObjectInitializer::new(&mut context) /// .property("x", 12, Attribute::READONLY) /// .build(); -/// context.register_global_property( -/// "arg", -/// arg, -/// Attribute::all() -/// ); +/// context.register_global_property("arg", arg, Attribute::all()); /// /// let value = context.eval("test(arg)").unwrap(); /// @@ -422,36 +418,20 @@ impl Context { /// # Example /// ``` /// use boa_engine::{ - /// Context, + /// object::ObjectInitializer, /// property::{Attribute, PropertyDescriptor}, - /// object::ObjectInitializer + /// Context, /// }; /// /// let mut context = Context::default(); /// - /// context.register_global_property( - /// "myPrimitiveProperty", - /// 10, - /// Attribute::all() - /// ); + /// context.register_global_property("myPrimitiveProperty", 10, Attribute::all()); /// /// let object = ObjectInitializer::new(&mut context) - /// .property( - /// "x", - /// 0, - /// Attribute::all() - /// ) - /// .property( - /// "y", - /// 1, - /// Attribute::all() - /// ) - /// .build(); - /// context.register_global_property( - /// "myObjectProperty", - /// object, - /// Attribute::all() - /// ); + /// .property("x", 0, Attribute::all()) + /// .property("y", 1, Attribute::all()) + /// .build(); + /// context.register_global_property("myObjectProperty", object, Attribute::all()); /// ``` #[inline] pub fn register_global_property(&mut self, key: K, value: V, attribute: Attribute) @@ -474,7 +454,7 @@ impl Context { /// /// # Examples /// ``` - ///# use boa_engine::Context; + /// # use boa_engine::Context; /// let mut context = Context::default(); /// /// let value = context.eval("1 + 3").unwrap(); @@ -610,7 +590,6 @@ impl Context { /// Additionally, if the `intl` feature is enabled, [`ContextBuilder`] becomes /// the only way to create a new [`Context`], since now it requires a /// valid data provider for the `Intl` functionality. -/// #[cfg_attr( feature = "intl", doc = "The required data in a valid provider is specified in [`BoaProvider`]" diff --git a/boa_engine/src/error/mod.rs b/boa_engine/src/error/mod.rs index e1d2c74d901..d15afbd950f 100644 --- a/boa_engine/src/error/mod.rs +++ b/boa_engine/src/error/mod.rs @@ -1,5 +1,3 @@ -use boa_gc::{Finalize, Trace}; - use crate::{ builtins::{error::ErrorKind, Array}, object::JsObject, @@ -8,26 +6,156 @@ use crate::{ syntax::parser, Context, JsResult, JsString, JsValue, }; - +use boa_gc::{Finalize, Trace}; +use thiserror::Error; + +/// The error type returned by all operations related +/// to the execution of Javascript code. +/// +/// This is essentially an enum that can store either [`JsNativeError`]s (for ideal +/// native errors) or opaque [`JsValue`]s, since Javascript allows throwing any valid +/// `JsValue`. +/// +/// The implementation doesn't provide a [`From`] conversion +/// for `JsValue`. This is with the intent of encouraging the usage of proper +/// `JsNativeError`s instead of plain `JsValue`s. However, if you +/// do need a proper opaque error, you can construct one using the +/// [`JsError::from_opaque`] method. +/// +/// # Examples +/// +/// ```rust +/// # use boa_engine::{JsError, JsNativeError, JsNativeErrorKind, JsValue}; +/// let cause = JsError::from_opaque("error!".into()); +/// +/// assert!(cause.as_opaque().is_some()); +/// assert_eq!(cause.as_opaque().unwrap(), &JsValue::from("error!")); +/// +/// let native_error: JsError = JsNativeError::typ() +/// .with_message("invalid type!") +/// .with_cause(cause) +/// .into(); +/// +/// assert!(native_error.as_native().is_some()); +/// +/// let kind = &native_error.as_native().unwrap().kind; +/// assert!(matches!(kind, JsNativeErrorKind::Type)); +/// ``` #[derive(Debug, Clone, Trace, Finalize)] pub struct JsError { inner: Repr, } +// hiding the enum makes it a bit more difficult +// to treat `JsError` as a proper enum, which should +// make it a bit harder to try to match against a native error +// without calling `try_native` first. #[derive(Debug, Clone, Trace, Finalize)] enum Repr { Native(JsNativeError), Opaque(JsValue), } +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 { - pub fn to_value(&self, context: &mut Context) -> JsValue { + /// Creates a new `JsError` from a native error `err`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsError, JsNativeError}; + /// let error = JsError::from_native(JsNativeError::syntax()); + /// + /// assert!(error.as_native().is_some()); + /// ``` + pub fn from_native(err: JsNativeError) -> Self { + Self { + inner: Repr::Native(err), + } + } + + /// Creates a new `JsError` from an opaque error `value`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::JsError; + /// let error = JsError::from_opaque(5.0f64.into()); + /// + /// assert!(error.as_opaque().is_some()); + /// ``` + pub fn from_opaque(value: JsValue) -> Self { + Self { + inner: Repr::Opaque(value), + } + } + + /// Converts the error to an opaque `JsValue` error + /// + /// Unwraps the inner `JsValue` if the error is already an opaque error. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{Context, JsError, JsNativeError}; + /// let context = &mut Context::default(); + /// let error: JsError = JsNativeError::eval().with_message("invalid script").into(); + /// let error_val = error.to_opaque(context); + /// + /// assert!(error_val.as_object().unwrap().borrow().is_error()); + /// ``` + pub fn to_opaque(&self, context: &mut Context) -> JsValue { match &self.inner { - Repr::Native(e) => e.to_value(context), + Repr::Native(e) => e.to_opaque(context).into(), Repr::Opaque(v) => v.clone(), } } + /// Unwraps the inner error if this contains a native error. + /// Otherwise, it will inspect the opaque error and try to extract the + /// necessary information to construct a native error similar to the provided + /// opaque error. + /// + /// # Note 1 + /// + /// This method won't try to make any conversions between JS types. + /// In other words, for this conversion to succeed: + /// - `message` **MUST** be a `JsString` value. + /// - `errors` (in the case of `AggregateError`s) **MUST** be an `Array` object. + /// + /// # Note 2 + /// + /// This operation should be considered a lossy conversion, since it + /// won't store any additional properties of the opaque + /// error, other than `message`, `cause` and `errors` (in the case of + /// `AggregateError`s). If you cannot affort a lossy conversion, clone + /// the object before calling [`from_opaque`][JsError::from_opaque] + /// to preserve its original properties. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{Context, JsError, JsNativeError, JsNativeErrorKind}; + /// let context = &mut Context::default(); + /// + /// // create a new, opaque Error object + /// let error: JsError = JsNativeError::typ().with_message("type error!").into(); + /// let error_val = error.to_opaque(context); + /// + /// // then, try to recover the original + /// let error = JsError::from_opaque(error_val).try_native(context).unwrap(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Type)); + /// assert_eq!(error.message(), "type error!"); + /// ``` // TODO: Should probably change this to return a custom error instead pub fn try_native(&self, context: &mut Context) -> JsResult { match &self.inner { @@ -54,11 +182,10 @@ impl JsError { "".into() }; - let cause = if obj.has_property("cause", context)? { - Some(obj.get("cause", context)?) - } else { - None - }; + let cause = obj + .has_property("cause", context)? + .then(|| obj.get("cause", context)) + .transpose()?; let kind = match error { ErrorKind::Error => JsNativeErrorKind::Error, @@ -75,7 +202,9 @@ impl JsError { Some(errors) if errors.is_array() => { let length = errors.length_of_array_like(context)?; for i in 0..length { - error_list.push(errors.get(i, context)?.into()); + error_list.push(JsError::from_opaque( + errors.get(i, context)?, + )); } } _ => { @@ -94,18 +223,34 @@ impl JsError { return Ok(JsNativeError { kind, message, - cause, + cause: cause.map(|v| Box::new(JsError::from_opaque(v))), }); } } Err(JsNativeError::typ() .with_message("failed to convert value to native error") - .with_cause(val) .into()) } } } + /// Gets the inner [`JsValue`] if the error is an opaque error, + /// or `None` otherwise. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsError, JsNativeError}; + /// let error: JsError = JsNativeError::reference() + /// .with_message("variable not found!") + /// .into(); + /// + /// assert!(error.as_opaque().is_none()); + /// + /// let error = JsError::from_opaque(256u32.into()); + /// + /// assert!(error.as_opaque().is_some()); + /// ``` pub fn as_opaque(&self) -> Option<&JsValue> { match self.inner { Repr::Native(_) => None, @@ -113,6 +258,21 @@ impl JsError { } } + /// Gets the inner [`JsNativeError`] if the error is a native + /// error, or `None` otherwise. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsError, JsNativeError, JsValue}; + /// let error: JsError = JsNativeError::error().with_message("Unknown error").into(); + /// + /// assert!(error.as_native().is_some()); + /// + /// let error = JsError::from_opaque(JsValue::undefined().into()); + /// + /// assert!(error.as_native().is_none()); + /// ``` pub fn as_native(&self) -> Option<&JsNativeError> { match self.inner { Repr::Native(ref e) => Some(e), @@ -135,22 +295,6 @@ impl From for JsError { } } -impl From for JsError { - fn from(value: JsValue) -> Self { - Self { - inner: Repr::Opaque(value), - } - } -} - -impl From for JsError { - fn from(object: JsObject) -> Self { - Self { - inner: Repr::Opaque(object.into()), - } - } -} - impl std::fmt::Display for JsError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.inner { @@ -160,15 +304,41 @@ impl std::fmt::Display for JsError { } } -#[derive(Debug, Clone, Trace, Finalize)] +/// Native representation of an ideal `Error` object from Javascript. +/// +/// This representation is more space efficient than its [`JsObject`] equivalent, +/// since it doesn't need to create a whole new `JsObject` to be instantiated. +/// Prefer using this over [`JsError`] when you don't need to throw +/// plain [`JsValue`]s as errors, or when you need to inspect the error type +/// of a `JsError`. +/// +/// # Examples +/// +/// ```rust +/// # use boa_engine::{JsNativeError, JsNativeErrorKind}; +/// let native_error = JsNativeError::uri().with_message("cannot decode uri"); +/// +/// match native_error.kind { +/// JsNativeErrorKind::Uri => { /* handle URI error*/ } +/// _ => unreachable!(), +/// } +/// +/// assert_eq!(native_error.message(), "cannot decode uri"); +/// ``` +#[derive(Debug, Clone, Trace, Finalize, Error)] +#[error("{kind}: {message}")] pub struct JsNativeError { + /// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.) pub kind: JsNativeErrorKind, message: Box, - cause: Option, + #[source] + cause: Option>, } impl JsNativeError { - fn new(kind: JsNativeErrorKind, message: Box, cause: Option) -> Self { + /// Creates a new `JsNativeError` from its `kind`, `message` and (optionally) + /// its `cause`. + fn new(kind: JsNativeErrorKind, message: Box, cause: Option>) -> Self { Self { kind, message, @@ -176,31 +346,150 @@ impl JsNativeError { } } + /// Creates a new `JsNativeError` of kind `AggregateError` from + /// a list of [`JsError`]s, with empty `message` and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let inner_errors = vec![ + /// JsNativeError::typ().into(), + /// JsNativeError::syntax().into() + /// ]; + /// let error = JsNativeError::aggregate(inner_errors); + /// + /// assert!(matches!( + /// error.kind, + /// JsNativeErrorKind::Aggregate(ref errors) if errors.len() == 2 + /// )); + /// ``` pub fn aggregate(errors: Vec) -> Self { Self::new(JsNativeErrorKind::Aggregate(errors), Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `Error`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::error(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Error)); + /// ``` pub fn error() -> Self { Self::new(JsNativeErrorKind::Error, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `EvalError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::eval(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Eval)); + /// ``` pub fn eval() -> Self { Self::new(JsNativeErrorKind::Eval, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `RangeError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::range(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Range)); + /// ``` pub fn range() -> Self { Self::new(JsNativeErrorKind::Range, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `ReferenceError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::reference(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Reference)); + /// ``` pub fn reference() -> Self { Self::new(JsNativeErrorKind::Reference, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `SyntaxError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::syntax(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Syntax)); + /// ``` pub fn syntax() -> Self { Self::new(JsNativeErrorKind::Syntax, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `TypeError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::typ(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Type)); + /// ``` pub fn typ() -> Self { Self::new(JsNativeErrorKind::Type, Box::default(), None) } + + /// Creates a new `JsNativeError` of kind `UriError`, with empty `message` + /// and undefined `cause`. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{JsNativeError, JsNativeErrorKind}; + /// let error = JsNativeError::uri(); + /// + /// assert!(matches!(error.kind, JsNativeErrorKind::Uri)); + /// ``` pub fn uri() -> Self { Self::new(JsNativeErrorKind::Uri, Box::default(), None) } + /// Appends a message to this error. + /// + /// # Note + /// + /// A static [`str`] will be stored as a simple pointer, + /// while a [`String`] will be converted to a [`Rc`][std::rc::Rc] in order + /// to make clones more efficient. Prefer static `str`s if you want + /// efficiency, and [`String`] s if you prefer descriptive error messages. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::JsNativeError; + /// let error = JsNativeError::range().with_message("number too large"); + /// + /// assert_eq!(error.message(), "number too large"); + /// ``` #[must_use] pub fn with_message(mut self, message: S) -> Self where @@ -210,53 +499,122 @@ impl JsNativeError { self } + /// Appends a cause to this error. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::JsNativeError; + /// let cause = JsNativeError::syntax(); + /// let error = JsNativeError::error().with_cause(cause); + /// + /// assert!(error.cause().unwrap().as_native().is_some()); + /// ``` #[must_use] pub fn with_cause(mut self, cause: V) -> Self where - V: Into, + V: Into, { - self.cause = Some(cause.into()); + self.cause = Some(Box::new(cause.into())); self } + /// Gets the `message` of this error. + /// + /// This is equivalent to the [`NativeError.prototype.message`][spec] + /// property. + /// + /// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/message + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::JsNativeError; + /// let error = JsNativeError::range().with_message("number too large"); + /// + /// assert_eq!(error.message(), "number too large"); + /// ``` pub fn message(&self) -> &str { &self.message } - pub fn cause(&self) -> Option<&JsValue> { - self.cause.as_ref() + /// Gets the `cause` of this error. + /// + /// This is equivalent to the [`NativeError.prototype.cause`][spec] + /// property. + /// + /// [spec]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::JsNativeError; + /// let cause = JsNativeError::syntax(); + /// let error = JsNativeError::error().with_cause(cause); + /// + /// assert!(error.cause().unwrap().as_native().is_some()); + /// ``` + pub fn cause(&self) -> Option<&JsError> { + self.cause.as_deref() } - pub fn to_value(&self, context: &mut Context) -> JsValue { + /// Converts this native error to its opaque representation as a + /// [`JsObject`]. + /// + /// # Examples + /// + /// ```rust + /// # use boa_engine::{Context, JsError, JsNativeError}; + /// let context = &mut Context::default(); + /// + /// let error = JsNativeError::error().with_message("error!"); + /// let error_obj = error.to_opaque(context); + /// + /// assert!(error_obj.borrow().is_error()); + /// assert_eq!(error_obj.get("message", context).unwrap(), "error!".into()) + /// ``` + pub fn to_opaque(&self, context: &mut Context) -> JsObject { let Self { kind, message, cause, } = self; let constructors = context.intrinsics().constructors(); - let prototype = match kind { - JsNativeErrorKind::Aggregate(_) => constructors.aggregate_error().prototype(), - JsNativeErrorKind::Error => constructors.error().prototype(), - JsNativeErrorKind::Eval => constructors.eval_error().prototype(), - JsNativeErrorKind::Range => constructors.range_error().prototype(), - JsNativeErrorKind::Reference => constructors.reference_error().prototype(), - JsNativeErrorKind::Syntax => constructors.syntax_error().prototype(), - JsNativeErrorKind::Type => constructors.type_error().prototype(), - JsNativeErrorKind::Uri => constructors.uri_error().prototype(), + 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), }; - let o = JsObject::from_proto_and_data(prototype, ObjectData::error(kind.as_error_kind())); + let o = JsObject::from_proto_and_data(prototype, ObjectData::error(tag)); o.create_non_enumerable_data_property_or_throw("message", &**message, context); if let Some(cause) = cause { - o.create_non_enumerable_data_property_or_throw("cause", cause.clone(), context); + o.create_non_enumerable_data_property_or_throw( + "cause", + cause.to_opaque(context), + context, + ); } if let JsNativeErrorKind::Aggregate(errors) = kind { let errors = errors .iter() - .map(|e| e.to_value(context)) + .map(|e| e.to_opaque(context)) .collect::>(); let errors = Array::create_array_from_list(errors, context); o.define_property_or_throw( @@ -270,14 +628,7 @@ impl JsNativeError { ) .expect("The spec guarantees this succeeds for a newly created object "); } - o.into() - } -} - -impl std::fmt::Display for JsNativeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Self { kind, message, .. } = self; - write!(f, "{kind}: {message}") + o } } @@ -287,34 +638,95 @@ impl From for JsNativeError { } } +/// The list of possible error types a [`JsNativeError`] can be. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// - [MDN documentation][mdn] +/// +/// [spec]: https://tc39.es/ecma262/#sec-error-objects +/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error #[derive(Debug, Clone, Trace, Finalize)] #[non_exhaustive] pub enum JsNativeErrorKind { + /// A collection of errors wrapped in a single error. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-aggregate-error-objects + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError Aggregate(Vec), + /// A generic error. Commonly used as the base for custom exceptions. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-error-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error Error, + /// An error related to the global function [`eval()`][eval]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError + /// [eval]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval Eval, + /// An error thrown when a value is outside its valid range. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError Range, + /// An error representing an invalid de-reference of a variable. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError Reference, + /// An error representing an invalid syntax in the Javascript language. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError Syntax, + /// An error thrown when a variable or argument is not of a valid type. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError Type, + /// An error thrown when the [`encodeURI()`][e_uri] and [`decodeURI()`][d_uri] + /// functions receive invalid parameters. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError + /// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI + /// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI Uri, } -impl JsNativeErrorKind { - fn as_error_kind(&self) -> ErrorKind { - match self { - JsNativeErrorKind::Aggregate(_) => ErrorKind::Aggregate, - JsNativeErrorKind::Error => ErrorKind::Error, - JsNativeErrorKind::Eval => ErrorKind::Eval, - JsNativeErrorKind::Range => ErrorKind::Range, - JsNativeErrorKind::Reference => ErrorKind::Reference, - JsNativeErrorKind::Syntax => ErrorKind::Syntax, - JsNativeErrorKind::Type => ErrorKind::Type, - JsNativeErrorKind::Uri => ErrorKind::Uri, - } - } -} - impl std::fmt::Display for JsNativeErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/boa_engine/src/object/builtins/jsmap.rs b/boa_engine/src/object/builtins/jsmap.rs index b2a003e9d51..4a3b6867be7 100644 --- a/boa_engine/src/object/builtins/jsmap.rs +++ b/boa_engine/src/object/builtins/jsmap.rs @@ -32,7 +32,6 @@ use std::ops::Deref; /// map.set("Key-2", 10, context).unwrap(); /// /// assert_eq!(map.get_size(context).unwrap(), 2.into()); -/// /// ``` /// /// Create a `JsMap` from a `JsArray` @@ -52,15 +51,18 @@ use std::ops::Deref; /// let vec_one: Vec = vec![JsValue::new("first-key"), JsValue::new("first-value")]; /// /// // We create an push our `[key, value]` pair onto our array as a `JsArray` -/// js_array.push(JsArray::from_iter(vec_one, context), context).unwrap(); +/// js_array +/// .push(JsArray::from_iter(vec_one, context), context) +/// .unwrap(); /// /// // Create a `JsMap` from the `JsArray` using it's iterable property. /// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context).unwrap(); /// -/// assert_eq!(js_iterable_map.get("first-key", context).unwrap(), "first-value".into()); -/// +/// assert_eq!( +/// js_iterable_map.get("first-key", context).unwrap(), +/// "first-value".into() +/// ); /// ``` -/// #[derive(Debug, Clone, Trace, Finalize)] pub struct JsMap { inner: JsObject, @@ -82,7 +84,6 @@ impl JsMap { /// /// // Create a new empty `JsMap`. /// let map = JsMap::new(context); - /// /// ``` #[inline] pub fn new(context: &mut Context) -> Self { @@ -107,13 +108,13 @@ impl JsMap { /// /// // Create a `[key, value]` pair of JsValues and add it to the `JsArray` as a `JsArray` /// let vec_one: Vec = vec![JsValue::new("first-key"), JsValue::new("first-value")]; - /// js_array.push(JsArray::from_iter(vec_one, context), context).unwrap(); + /// js_array + /// .push(JsArray::from_iter(vec_one, context), context) + /// .unwrap(); /// /// // Create a `JsMap` from the `JsArray` using it's iterable property. /// let js_iterable_map = JsMap::from_js_iterable(&js_array.into(), context).unwrap(); - /// /// ``` - /// #[inline] pub fn from_js_iterable(iterable: &JsValue, context: &mut Context) -> JsResult { // Create a new map object. @@ -146,12 +147,11 @@ impl JsMap { /// // `some_object` can be any JavaScript `Map` object. /// let some_object = JsObject::from_proto_and_data( /// context.intrinsics().constructors().map().prototype(), - /// ObjectData::map(OrderedMap::new()) + /// ObjectData::map(OrderedMap::new()), /// ); /// /// // Create `JsMap` object with incoming object. /// let js_map = JsMap::from_object(some_object).unwrap(); - /// /// ``` /// /// Invalid Example - returns a `TypeError` with the message "object is not a Map" @@ -167,7 +167,6 @@ impl JsMap { /// /// // `some_object` is an Array object, not a map object /// assert!(JsMap::from_object(some_object.into()).is_err()); - /// /// ``` #[inline] pub fn from_object(object: JsObject) -> JsResult { @@ -226,7 +225,6 @@ impl JsMap { /// /// assert_eq!(js_map.get("foo", context).unwrap(), "bar".into()); /// assert_eq!(js_map.get(2, context).unwrap(), 4.into()) - /// /// ``` #[inline] pub fn set(&self, key: K, value: V, context: &mut Context) -> JsResult @@ -260,7 +258,6 @@ impl JsMap { /// let map_size = js_map.get_size(context).unwrap(); /// /// assert_eq!(map_size, 1.into()); - /// /// ``` #[inline] pub fn get_size(&self, context: &mut Context) -> JsResult { @@ -287,7 +284,6 @@ impl JsMap { /// /// assert_eq!(js_map.get_size(context).unwrap(), 1.into()); /// assert_eq!(js_map.get("foo", context).unwrap(), JsValue::undefined()); - /// /// ``` #[inline] pub fn delete(&self, key: T, context: &mut Context) -> JsResult @@ -314,7 +310,6 @@ impl JsMap { /// let retrieved_value = js_map.get("foo", context).unwrap(); /// /// assert_eq!(retrieved_value, "bar".into()); - /// /// ``` #[inline] pub fn get(&self, key: T, context: &mut Context) -> JsResult @@ -343,7 +338,6 @@ impl JsMap { /// js_map.clear(context).unwrap(); /// /// assert_eq!(js_map.get_size(context).unwrap(), 0.into()); - /// /// ``` #[inline] pub fn clear(&self, context: &mut Context) -> JsResult { @@ -368,7 +362,6 @@ impl JsMap { /// let has_key = js_map.has("foo", context).unwrap(); /// /// assert_eq!(has_key, true.into()); - /// /// ``` #[inline] pub fn has(&self, key: T, context: &mut Context) -> JsResult diff --git a/boa_engine/src/object/mod.rs b/boa_engine/src/object/mod.rs index 7752daf5c6a..2adb80352a1 100644 --- a/boa_engine/src/object/mod.rs +++ b/boa_engine/src/object/mod.rs @@ -1802,16 +1802,8 @@ impl<'context> FunctionBuilder<'context> { /// # use boa_engine::{Context, JsValue, object::ObjectInitializer, property::Attribute}; /// let mut context = Context::default(); /// let object = ObjectInitializer::new(&mut context) -/// .property( -/// "hello", -/// "world", -/// Attribute::all() -/// ) -/// .property( -/// 1, -/// 1, -/// Attribute::all() -/// ) +/// .property("hello", "world", Attribute::all()) +/// .property(1, 1, Attribute::all()) /// .function(|_, _, _| Ok(JsValue::undefined()), "func", 0) /// .build(); /// ``` diff --git a/boa_engine/src/symbol.rs b/boa_engine/src/symbol.rs index f500b86077a..f7f44488294 100644 --- a/boa_engine/src/symbol.rs +++ b/boa_engine/src/symbol.rs @@ -27,7 +27,7 @@ use std::{ /// /// # Examples /// ``` -///# use boa_engine::symbol::WellKnownSymbols; +/// # use boa_engine::symbol::WellKnownSymbols; /// /// let iterator = WellKnownSymbols::iterator(); /// assert_eq!(iterator.description().unwrap().to_std_string_escaped(), "Symbol.iterator"); diff --git a/boa_engine/src/syntax/ast/function/parameters.rs b/boa_engine/src/syntax/ast/function/parameters.rs index 32ff6b88ed6..da450b7513b 100644 --- a/boa_engine/src/syntax/ast/function/parameters.rs +++ b/boa_engine/src/syntax/ast/function/parameters.rs @@ -237,10 +237,10 @@ impl Default for FormalParameterListFlags { /// /// In the declaration of a function, the parameters must be identifiers, /// not any value like numbers, strings, or objects. -///```text -///function foo(formalParameter1, formalParameter2) { -///} -///``` +/// ```text +/// function foo(formalParameter1, formalParameter2) { +/// } +/// ``` /// /// More information: /// - [ECMAScript reference][spec] diff --git a/boa_engine/src/syntax/ast/statement/switch/mod.rs b/boa_engine/src/syntax/ast/statement/switch/mod.rs index 2a2b286ca24..9a5fd59b39a 100644 --- a/boa_engine/src/syntax/ast/statement/switch/mod.rs +++ b/boa_engine/src/syntax/ast/statement/switch/mod.rs @@ -1,10 +1,8 @@ //! Switch node. //! -use crate::syntax::ast::{expression::Expression, statement::Statement}; +use crate::syntax::ast::{expression::Expression, statement::Statement, StatementList}; use boa_interner::{Interner, ToInternedString}; -use crate::syntax::ast::StatementList; - use super::ContainsSymbol; #[cfg(test)] diff --git a/boa_engine/src/syntax/parser/expression/identifiers.rs b/boa_engine/src/syntax/parser/expression/identifiers.rs index 4de308f7127..2e284dbd3b2 100644 --- a/boa_engine/src/syntax/parser/expression/identifiers.rs +++ b/boa_engine/src/syntax/parser/expression/identifiers.rs @@ -4,7 +4,6 @@ //! - [ECMAScript specification][spec] //! //! [spec]: https://tc39.es/ecma262/#sec-identifiers -//! use crate::syntax::{ ast::{expression::Identifier, Keyword}, diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index e78b4390570..f11e1af6bba 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -18,7 +18,7 @@ use crate::{ call_frame::CatchAddresses, code_block::{initialize_instance_elements, Readable}, }, - Context, JsBigInt, JsResult, JsString, JsValue, + Context, JsBigInt, JsError, JsResult, JsString, JsValue, }; use boa_interner::ToInternedString; use boa_profiler::Profiler; @@ -1477,7 +1477,7 @@ impl Context { } Opcode::Throw => { let value = self.vm.pop(); - return Err(value.into()); + return Err(JsError::from_opaque(value)); } Opcode::TryStart => { let next = self.vm.read::(); @@ -1560,7 +1560,7 @@ impl Context { return Ok(ShouldExit::True); } FinallyReturn::Err => { - return Err(self.vm.pop().into()); + return Err(JsError::from_opaque(self.vm.pop())); } } } @@ -2380,7 +2380,7 @@ impl Context { GeneratorResumeKind::Normal => return Ok(ShouldExit::False), GeneratorResumeKind::Throw => { let received = self.vm.pop(); - return Err(received.into()); + return Err(JsError::from_opaque(received)); } GeneratorResumeKind::Return => { let mut finally_left = false; @@ -2407,7 +2407,7 @@ impl Context { let value = self.vm.pop(); if self.vm.frame().generator_resume_kind == GeneratorResumeKind::Throw { - return Err(value.into()); + return Err(JsError::from_opaque(value)); } let completion = Ok(value); @@ -2437,7 +2437,7 @@ impl Context { if *r#return { let value = match completion { Ok(value) => value.clone(), - Err(e) => e.clone().to_value(self), + Err(e) => e.clone().to_opaque(self), }; self.vm.push(value); self.vm.push(true); @@ -2842,14 +2842,14 @@ impl Context { self.vm.frame_mut().catch.pop(); self.vm.frame_mut().finally_return = FinallyReturn::Err; self.vm.frame_mut().thrown = true; - let e = e.to_value(self); + let e = e.to_opaque(self); self.vm.push(e); } else { self.vm.stack.truncate(start_stack_size); // Step 3.f in [AsyncBlockStart](https://tc39.es/ecma262/#sec-asyncblockstart). if let Some(promise_capability) = promise_capability { - let e = e.to_value(self); + let e = e.to_opaque(self); promise_capability .reject() .call(&JsValue::undefined(), &[e.clone()], self)