mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
806 lines
26 KiB
806 lines
26 KiB
//! This module implements the `JsObject` structure. |
|
//! |
|
//! The `JsObject` is a garbage collected Object. |
|
|
|
use super::{JsPrototype, NativeObject, Object, PropertyMap}; |
|
use crate::{ |
|
object::{ObjectData, ObjectKind}, |
|
property::{PropertyDescriptor, PropertyKey}, |
|
value::PreferredType, |
|
Context, JsResult, JsValue, |
|
}; |
|
use boa_gc::{self, Finalize, Gc, Trace}; |
|
use std::{ |
|
cell::RefCell, |
|
collections::HashMap, |
|
error::Error, |
|
fmt::{self, Debug, Display}, |
|
result::Result as StdResult, |
|
}; |
|
|
|
/// A wrapper type for an immutably borrowed type T. |
|
pub type Ref<'a, T> = boa_gc::Ref<'a, T>; |
|
|
|
/// A wrapper type for a mutably borrowed type T. |
|
pub type RefMut<'a, T, U> = boa_gc::RefMut<'a, T, U>; |
|
|
|
/// Garbage collected `Object`. |
|
#[derive(Trace, Finalize, Clone, Default)] |
|
pub struct JsObject(Gc<boa_gc::Cell<Object>>); |
|
|
|
impl JsObject { |
|
/// Create a new `JsObject` from an internal `Object`. |
|
#[inline] |
|
fn from_object(object: Object) -> Self { |
|
Self(Gc::new(boa_gc::Cell::new(object))) |
|
} |
|
|
|
/// Create a new empty `JsObject`, with `prototype` set to `JsValue::Null` |
|
/// and `data` set to `ObjectData::ordinary` |
|
pub fn empty() -> Self { |
|
Self::from_object(Object::default()) |
|
} |
|
|
|
/// The more general form of `OrdinaryObjectCreate` and `MakeBasicObject`. |
|
/// |
|
/// Create a `JsObject` and automatically set its internal methods and |
|
/// internal slots from the `data` provided. |
|
#[inline] |
|
pub fn from_proto_and_data<O: Into<Option<Self>>>(prototype: O, data: ObjectData) -> Self { |
|
Self::from_object(Object { |
|
data, |
|
prototype: prototype.into(), |
|
extensible: true, |
|
properties: PropertyMap::default(), |
|
}) |
|
} |
|
|
|
/// Immutably borrows the `Object`. |
|
/// |
|
/// The borrow lasts until the returned `Ref` exits scope. |
|
/// Multiple immutable borrows can be taken out at the same time. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn borrow(&self) -> Ref<'_, Object> { |
|
self.try_borrow().expect("Object already mutably borrowed") |
|
} |
|
|
|
/// Mutably borrows the Object. |
|
/// |
|
/// The borrow lasts until the returned `RefMut` exits scope. |
|
/// The object cannot be borrowed while this borrow is active. |
|
/// |
|
///# Panics |
|
/// Panics if the object is currently borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn borrow_mut(&self) -> RefMut<'_, Object, Object> { |
|
self.try_borrow_mut().expect("Object already borrowed") |
|
} |
|
|
|
/// Immutably borrows the `Object`, returning an error if the value is currently mutably borrowed. |
|
/// |
|
/// The borrow lasts until the returned `GcCellRef` exits scope. |
|
/// Multiple immutable borrows can be taken out at the same time. |
|
/// |
|
/// This is the non-panicking variant of [`borrow`](#method.borrow). |
|
#[inline] |
|
pub fn try_borrow(&self) -> StdResult<Ref<'_, Object>, BorrowError> { |
|
self.0.try_borrow().map_err(|_| BorrowError) |
|
} |
|
|
|
/// Mutably borrows the object, returning an error if the value is currently borrowed. |
|
/// |
|
/// The borrow lasts until the returned `GcCellRefMut` exits scope. |
|
/// The object be borrowed while this borrow is active. |
|
/// |
|
/// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). |
|
#[inline] |
|
pub fn try_borrow_mut(&self) -> StdResult<RefMut<'_, Object, Object>, BorrowMutError> { |
|
self.0.try_borrow_mut().map_err(|_| BorrowMutError) |
|
} |
|
|
|
/// Checks if the garbage collected memory is the same. |
|
#[inline] |
|
pub fn equals(lhs: &Self, rhs: &Self) -> bool { |
|
std::ptr::eq(lhs.as_ref(), rhs.as_ref()) |
|
} |
|
|
|
/// Converts an object to a primitive. |
|
/// |
|
/// Diverges from the spec to prevent a stack overflow when the object is recursive. |
|
/// For example, |
|
/// ```javascript |
|
/// let a = [1]; |
|
/// a[1] = a; |
|
/// console.log(a.toString()); // We print "1," |
|
/// ``` |
|
/// The spec doesn't mention what to do in this situation, but a naive implementation |
|
/// would overflow the stack recursively calling `toString()`. We follow v8 and SpiderMonkey |
|
/// instead by returning a default value for the given `hint` -- either `0.` or `""`. |
|
/// Example in v8: <https://repl.it/repls/IvoryCircularCertification#index.js> |
|
/// |
|
/// More information: |
|
/// - [ECMAScript][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-ordinarytoprimitive |
|
pub(crate) fn ordinary_to_primitive( |
|
&self, |
|
context: &mut Context, |
|
hint: PreferredType, |
|
) -> JsResult<JsValue> { |
|
// 1. Assert: Type(O) is Object. |
|
// Already is JsObject by type. |
|
// 2. Assert: Type(hint) is String and its value is either "string" or "number". |
|
debug_assert!(hint == PreferredType::String || hint == PreferredType::Number); |
|
|
|
// Diverge from the spec here to make sure we aren't going to overflow the stack by converting |
|
// a recursive structure |
|
// We can follow v8 & SpiderMonkey's lead and return a default value for the hint in this situation |
|
// (see https://repl.it/repls/IvoryCircularCertification#index.js) |
|
let recursion_limiter = RecursionLimiter::new(self); |
|
if recursion_limiter.live { |
|
// we're in a recursive object, bail |
|
return Ok(match hint { |
|
PreferredType::Number => JsValue::new(0), |
|
PreferredType::String => JsValue::new(""), |
|
PreferredType::Default => unreachable!("checked type hint in step 2"), |
|
}); |
|
} |
|
|
|
// 3. If hint is "string", then |
|
// a. Let methodNames be « "toString", "valueOf" ». |
|
// 4. Else, |
|
// a. Let methodNames be « "valueOf", "toString" ». |
|
let method_names = if hint == PreferredType::String { |
|
["toString", "valueOf"] |
|
} else { |
|
["valueOf", "toString"] |
|
}; |
|
|
|
// 5. For each name in methodNames in List order, do |
|
for name in &method_names { |
|
// a. Let method be ? Get(O, name). |
|
let method = self.get(*name, context)?; |
|
|
|
// b. If IsCallable(method) is true, then |
|
if let Some(method) = method.as_callable() { |
|
// i. Let result be ? Call(method, O). |
|
let result = method.call(&self.clone().into(), &[], context)?; |
|
|
|
// ii. If Type(result) is not Object, return result. |
|
if !result.is_object() { |
|
return Ok(result); |
|
} |
|
} |
|
} |
|
|
|
// 6. Throw a TypeError exception. |
|
context.throw_type_error("cannot convert object to primitive value") |
|
} |
|
|
|
/// Return `true` if it is a native object and the native type is `T`. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is<T>(&self) -> bool |
|
where |
|
T: NativeObject, |
|
{ |
|
self.borrow().is::<T>() |
|
} |
|
|
|
/// Downcast a reference to the object, |
|
/// if the object is type native object type `T`. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn downcast_ref<T>(&self) -> Option<Ref<'_, T>> |
|
where |
|
T: NativeObject, |
|
{ |
|
let object = self.borrow(); |
|
if object.is::<T>() { |
|
Some(Ref::map(object, |x| { |
|
x.downcast_ref::<T>().expect("downcasting reference failed") |
|
})) |
|
} else { |
|
None |
|
} |
|
} |
|
|
|
/// Downcast a mutable reference to the object, |
|
/// if the object is type native object type `T`. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn downcast_mut<T>(&mut self) -> Option<RefMut<'_, Object, T>> |
|
where |
|
T: NativeObject, |
|
{ |
|
let object = self.borrow_mut(); |
|
if object.is::<T>() { |
|
Some(RefMut::map(object, |x| { |
|
x.downcast_mut::<T>() |
|
.expect("downcasting mutable reference failed") |
|
})) |
|
} else { |
|
None |
|
} |
|
} |
|
|
|
/// Get the prototype of the object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn prototype(&self) -> Ref<'_, JsPrototype> { |
|
Ref::map(self.borrow(), Object::prototype) |
|
} |
|
|
|
/// Get the extensibility of the object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
pub(crate) fn extensible(&self) -> bool { |
|
self.borrow().extensible |
|
} |
|
|
|
/// Set the prototype of the object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed |
|
#[inline] |
|
#[track_caller] |
|
pub fn set_prototype(&self, prototype: JsPrototype) -> bool { |
|
self.borrow_mut().set_prototype(prototype) |
|
} |
|
|
|
/// Checks if it's an `Array` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_array(&self) -> bool { |
|
self.borrow().is_array() |
|
} |
|
|
|
/// Checks if it is an `ArrayIterator` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_array_iterator(&self) -> bool { |
|
self.borrow().is_array_iterator() |
|
} |
|
|
|
/// Checks if it's an `ArrayBuffer` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_array_buffer(&self) -> bool { |
|
self.borrow().is_array_buffer() |
|
} |
|
|
|
/// Checks if it is a `Map` object.pub |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_map(&self) -> bool { |
|
self.borrow().is_map() |
|
} |
|
|
|
/// Checks if it's a `String` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_string(&self) -> bool { |
|
self.borrow().is_string() |
|
} |
|
|
|
/// Checks if it's a `Function` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_function(&self) -> bool { |
|
self.borrow().is_function() |
|
} |
|
|
|
/// Checks if it's a `Generator` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_generator(&self) -> bool { |
|
self.borrow().is_generator() |
|
} |
|
|
|
/// Checks if it's a `Symbol` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_symbol(&self) -> bool { |
|
self.borrow().is_symbol() |
|
} |
|
|
|
/// Checks if it's an `Error` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_error(&self) -> bool { |
|
self.borrow().is_error() |
|
} |
|
|
|
/// Checks if it's a `Boolean` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_boolean(&self) -> bool { |
|
self.borrow().is_boolean() |
|
} |
|
|
|
/// Checks if it's a `Number` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_number(&self) -> bool { |
|
self.borrow().is_number() |
|
} |
|
|
|
/// Checks if it's a `BigInt` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_bigint(&self) -> bool { |
|
self.borrow().is_bigint() |
|
} |
|
|
|
/// Checks if it's a `RegExp` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_regexp(&self) -> bool { |
|
self.borrow().is_regexp() |
|
} |
|
|
|
/// Checks if it's a `TypedArray` object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_typed_array(&self) -> bool { |
|
self.borrow().is_typed_array() |
|
} |
|
|
|
/// Checks if it's an ordinary object. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_ordinary(&self) -> bool { |
|
self.borrow().is_ordinary() |
|
} |
|
|
|
/// Returns `true` if it holds an Rust type that implements `NativeObject`. |
|
/// |
|
/// # Panics |
|
/// |
|
/// Panics if the object is currently mutably borrowed. |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_native_object(&self) -> bool { |
|
self.borrow().is_native_object() |
|
} |
|
|
|
pub fn to_property_descriptor(&self, context: &mut Context) -> JsResult<PropertyDescriptor> { |
|
// 1 is implemented on the method `to_property_descriptor` of value |
|
|
|
// 2. Let desc be a new Property Descriptor that initially has no fields. |
|
let mut desc = PropertyDescriptor::builder(); |
|
|
|
// 3. Let hasEnumerable be ? HasProperty(Obj, "enumerable"). |
|
// 4. If hasEnumerable is true, then ... |
|
if self.has_property("enumerable", context)? { |
|
// a. Let enumerable be ! ToBoolean(? Get(Obj, "enumerable")). |
|
// b. Set desc.[[Enumerable]] to enumerable. |
|
desc = desc.enumerable(self.get("enumerable", context)?.to_boolean()); |
|
} |
|
|
|
// 5. Let hasConfigurable be ? HasProperty(Obj, "configurable"). |
|
// 6. If hasConfigurable is true, then ... |
|
if self.has_property("configurable", context)? { |
|
// a. Let configurable be ! ToBoolean(? Get(Obj, "configurable")). |
|
// b. Set desc.[[Configurable]] to configurable. |
|
desc = desc.configurable(self.get("configurable", context)?.to_boolean()); |
|
} |
|
|
|
// 7. Let hasValue be ? HasProperty(Obj, "value"). |
|
// 8. If hasValue is true, then ... |
|
if self.has_property("value", context)? { |
|
// a. Let value be ? Get(Obj, "value"). |
|
// b. Set desc.[[Value]] to value. |
|
desc = desc.value(self.get("value", context)?); |
|
} |
|
|
|
// 9. Let hasWritable be ? HasProperty(Obj, ). |
|
// 10. If hasWritable is true, then ... |
|
if self.has_property("writable", context)? { |
|
// a. Let writable be ! ToBoolean(? Get(Obj, "writable")). |
|
// b. Set desc.[[Writable]] to writable. |
|
desc = desc.writable(self.get("writable", context)?.to_boolean()); |
|
} |
|
|
|
// 11. Let hasGet be ? HasProperty(Obj, "get"). |
|
// 12. If hasGet is true, then |
|
let get = if self.has_property("get", context)? { |
|
// a. Let getter be ? Get(Obj, "get"). |
|
let getter = self.get("get", context)?; |
|
// b. If IsCallable(getter) is false and getter is not undefined, throw a TypeError exception. |
|
// todo: extract IsCallable to be callable from Value |
|
if !getter.is_undefined() && getter.as_object().map_or(true, |o| !o.is_callable()) { |
|
return context.throw_type_error("Property descriptor getter must be callable"); |
|
} |
|
// c. Set desc.[[Get]] to getter. |
|
Some(getter) |
|
} else { |
|
None |
|
}; |
|
|
|
// 13. Let hasSet be ? HasProperty(Obj, "set"). |
|
// 14. If hasSet is true, then |
|
let set = if self.has_property("set", context)? { |
|
// 14.a. Let setter be ? Get(Obj, "set"). |
|
let setter = self.get("set", context)?; |
|
// 14.b. If IsCallable(setter) is false and setter is not undefined, throw a TypeError exception. |
|
// todo: extract IsCallable to be callable from Value |
|
if !setter.is_undefined() && setter.as_object().map_or(true, |o| !o.is_callable()) { |
|
return context.throw_type_error("Property descriptor setter must be callable"); |
|
} |
|
// 14.c. Set desc.[[Set]] to setter. |
|
Some(setter) |
|
} else { |
|
None |
|
}; |
|
|
|
// 15. If desc.[[Get]] is present or desc.[[Set]] is present, then ... |
|
// a. If desc.[[Value]] is present or desc.[[Writable]] is present, throw a TypeError exception. |
|
if get.as_ref().or(set.as_ref()).is_some() && desc.inner().is_data_descriptor() { |
|
return context.throw_type_error( |
|
"Invalid property descriptor.\ |
|
Cannot both specify accessors and a value or writable attribute", |
|
); |
|
} |
|
|
|
desc = desc.maybe_get(get).maybe_set(set); |
|
|
|
// 16. Return desc. |
|
Ok(desc.build()) |
|
} |
|
|
|
/// `7.3.25 CopyDataProperties ( target, source, excludedItems )` |
|
/// |
|
/// More information: |
|
/// - [ECMAScript][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-copydataproperties |
|
#[inline] |
|
pub fn copy_data_properties<K>( |
|
&self, |
|
source: &JsValue, |
|
excluded_keys: Vec<K>, |
|
context: &mut Context, |
|
) -> JsResult<()> |
|
where |
|
K: Into<PropertyKey>, |
|
{ |
|
// 1. Assert: Type(target) is Object. |
|
// 2. Assert: excludedItems is a List of property keys. |
|
// 3. If source is undefined or null, return target. |
|
if source.is_null_or_undefined() { |
|
return Ok(()); |
|
} |
|
|
|
// 4. Let from be ! ToObject(source). |
|
let from = source |
|
.to_object(context) |
|
.expect("function ToObject should never complete abruptly here"); |
|
|
|
// 5. Let keys be ? from.[[OwnPropertyKeys]](). |
|
// 6. For each element nextKey of keys, do |
|
let excluded_keys: Vec<PropertyKey> = excluded_keys.into_iter().map(Into::into).collect(); |
|
for key in from.__own_property_keys__(context)? { |
|
// a. Let excluded be false. |
|
let mut excluded = false; |
|
|
|
// b. For each element e of excludedItems, do |
|
for e in &excluded_keys { |
|
// i. If SameValue(e, nextKey) is true, then |
|
if *e == key { |
|
// 1. Set excluded to true. |
|
excluded = true; |
|
break; |
|
} |
|
} |
|
// c. If excluded is false, then |
|
if !excluded { |
|
// i. Let desc be ? from.[[GetOwnProperty]](nextKey). |
|
let desc = from.__get_own_property__(&key, context)?; |
|
|
|
// ii. If desc is not undefined and desc.[[Enumerable]] is true, then |
|
if let Some(desc) = desc { |
|
if let Some(enumerable) = desc.enumerable() { |
|
if enumerable { |
|
// 1. Let propValue be ? Get(from, nextKey). |
|
let prop_value = from.__get__(&key, from.clone().into(), context)?; |
|
|
|
// 2. Perform ! CreateDataPropertyOrThrow(target, nextKey, propValue). |
|
self.create_data_property_or_throw(key, prop_value, context) |
|
.expect( |
|
"CreateDataPropertyOrThrow should never complete abruptly here", |
|
); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
// 7. Return target. |
|
Ok(()) |
|
} |
|
|
|
/// Helper function for property insertion. |
|
#[inline] |
|
#[track_caller] |
|
pub(crate) fn insert<K, P>(&self, key: K, property: P) -> Option<PropertyDescriptor> |
|
where |
|
K: Into<PropertyKey>, |
|
P: Into<PropertyDescriptor>, |
|
{ |
|
self.borrow_mut().insert(key, property) |
|
} |
|
|
|
/// Inserts a field in the object `properties` without checking if it's writable. |
|
/// |
|
/// If a field was already in the object with the same name that a `Some` is returned |
|
/// with that field, otherwise None is returned. |
|
#[inline] |
|
pub fn insert_property<K, P>(&self, key: K, property: P) -> Option<PropertyDescriptor> |
|
where |
|
K: Into<PropertyKey>, |
|
P: Into<PropertyDescriptor>, |
|
{ |
|
self.insert(key.into(), property) |
|
} |
|
|
|
/// It determines if Object is a callable function with a `[[Call]]` internal method. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-iscallable |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_callable(&self) -> bool { |
|
self.borrow().data.internal_methods.__call__.is_some() |
|
} |
|
|
|
/// It determines if Object is a function object with a `[[Construct]]` internal method. |
|
/// |
|
/// More information: |
|
/// - [ECMAScript reference][spec] |
|
/// |
|
/// [spec]: https://tc39.es/ecma262/#sec-isconstructor |
|
#[inline] |
|
#[track_caller] |
|
pub fn is_constructor(&self) -> bool { |
|
self.borrow().data.internal_methods.__construct__.is_some() |
|
} |
|
|
|
/// Returns true if the `JsObject` is the global for a Realm |
|
pub fn is_global(&self) -> bool { |
|
matches!( |
|
self.borrow().data, |
|
ObjectData { |
|
kind: ObjectKind::Global, |
|
.. |
|
} |
|
) |
|
} |
|
} |
|
|
|
impl AsRef<boa_gc::Cell<Object>> for JsObject { |
|
#[inline] |
|
fn as_ref(&self) -> &boa_gc::Cell<Object> { |
|
&*self.0 |
|
} |
|
} |
|
|
|
impl PartialEq for JsObject { |
|
fn eq(&self, other: &Self) -> bool { |
|
Self::equals(self, other) |
|
} |
|
} |
|
|
|
/// An error returned by [`JsObject::try_borrow`](struct.JsObject.html#method.try_borrow). |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
|
pub struct BorrowError; |
|
|
|
impl Display for BorrowError { |
|
#[inline] |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
Display::fmt("Object already mutably borrowed", f) |
|
} |
|
} |
|
|
|
impl Error for BorrowError {} |
|
|
|
/// An error returned by [`JsObject::try_borrow_mut`](struct.JsObject.html#method.try_borrow_mut). |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
|
pub struct BorrowMutError; |
|
|
|
impl Display for BorrowMutError { |
|
#[inline] |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
|
Display::fmt("Object already borrowed", f) |
|
} |
|
} |
|
|
|
impl Error for BorrowMutError {} |
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
|
enum RecursionValueState { |
|
/// This value is "live": there's an active RecursionLimiter that hasn't been dropped. |
|
Live, |
|
/// This value has been seen before, but the recursion limiter has been dropped. |
|
/// For example: |
|
/// ```javascript |
|
/// let b = []; |
|
/// JSON.stringify([ // Create a recursion limiter for the root here |
|
/// b, // state for b's &JsObject here is None |
|
/// b, // state for b's &JsObject here is Visited |
|
/// ]); |
|
/// ``` |
|
Visited, |
|
} |
|
|
|
/// Prevents infinite recursion during `Debug::fmt`, `JSON.stringify`, and other conversions. |
|
/// This uses a thread local, so is not safe to use where the object graph will be traversed by |
|
/// multiple threads! |
|
#[derive(Debug)] |
|
pub struct RecursionLimiter { |
|
/// If this was the first `JsObject` in the tree. |
|
top_level: bool, |
|
/// The ptr being kept in the HashSet, so we can delete it when we drop. |
|
ptr: usize, |
|
/// If this JsObject has been visited before in the graph, but not in the current branch. |
|
pub visited: bool, |
|
/// If this JsObject has been visited in the current branch of the graph. |
|
pub live: bool, |
|
} |
|
|
|
impl Drop for RecursionLimiter { |
|
fn drop(&mut self) { |
|
if self.top_level { |
|
// When the top level of the graph is dropped, we can free the entire map for the next traversal. |
|
Self::SEEN.with(|hm| hm.borrow_mut().clear()); |
|
} else if !self.live { |
|
// This was the first RL for this object to become live, so it's no longer live now that it's dropped. |
|
Self::SEEN.with(|hm| { |
|
hm.borrow_mut() |
|
.insert(self.ptr, RecursionValueState::Visited) |
|
}); |
|
} |
|
} |
|
} |
|
|
|
impl RecursionLimiter { |
|
thread_local! { |
|
/// The map of pointers to `JsObject` that have been visited during the current `Debug::fmt` graph, |
|
/// and the current state of their RecursionLimiter (dropped or live -- see `RecursionValueState`) |
|
static SEEN: RefCell<HashMap<usize, RecursionValueState>> = RefCell::new(HashMap::new()); |
|
} |
|
|
|
/// Determines if the specified `JsObject` has been visited, and returns a struct that will free it when dropped. |
|
/// |
|
/// This is done by maintaining a thread-local hashset containing the pointers of `JsObject` values that have been |
|
/// visited. The first `JsObject` visited will clear the hashset, while any others will check if they are contained |
|
/// by the hashset. |
|
pub fn new(o: &JsObject) -> Self { |
|
// We shouldn't have to worry too much about this being moved during Debug::fmt. |
|
let ptr = (o.as_ref() as *const _) as usize; |
|
let (top_level, visited, live) = Self::SEEN.with(|hm| { |
|
let mut hm = hm.borrow_mut(); |
|
let top_level = hm.is_empty(); |
|
let old_state = hm.insert(ptr, RecursionValueState::Live); |
|
|
|
( |
|
top_level, |
|
old_state == Some(RecursionValueState::Visited), |
|
old_state == Some(RecursionValueState::Live), |
|
) |
|
}); |
|
|
|
Self { |
|
top_level, |
|
ptr, |
|
visited, |
|
live, |
|
} |
|
} |
|
} |
|
|
|
impl Debug for JsObject { |
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { |
|
let limiter = RecursionLimiter::new(self); |
|
|
|
// Typically, using `!limiter.live` would be good enough here. |
|
// However, the JS object hierarchy involves quite a bit of repitition, and the sheer amount of data makes |
|
// understanding the Debug output impossible; limiting the usefulness of it. |
|
// |
|
// Instead, we check if the object has appeared before in the entire graph. This means that objects will appear |
|
// at most once, hopefully making things a bit clearer. |
|
if !limiter.visited && !limiter.live { |
|
f.debug_tuple("JsObject").field(&self.0).finish() |
|
} else { |
|
f.write_str("{ ... }") |
|
} |
|
} |
|
}
|
|
|