diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 885bf81d5..815a2f6ab 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -360,8 +360,28 @@ fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> J Ok(Value::str(&format!("[object {class}]"))) } +/// The string form of a Number, String or Boolean object's [[PrimitiveValue]] (sections 8.6.2 and +/// 9.9), when `toString` is not found on the object or on its prototype chain up to and including +/// its standard prototype. Returns `None` otherwise, and for any other object. +fn boxed_primitive_string(interp: &mut Interp, o: ObjId) -> JsResult> { + let (primitive, proto) = match interp.heap.object(o).map(|x| &x.class) { + Some(Class::Number(n)) => (Value::Number(*n), interp.intrinsics.number_proto), + Some(Class::String(s)) => (Value::String(s.clone()), interp.intrinsics.string_proto), + Some(Class::Boolean(b)) => (Value::Bool(*b), interp.intrinsics.boolean_proto), + _ => return Ok(None), + }; + if !json::reaches_prototype(interp, o, proto, "toString")? { + return Ok(None); + } + Ok(Some(Value::String(interp.primitive_to_str(&primitive)))) +} + +/// `Object.prototype.toLocaleString` (section 15.2.4.3). fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult { let o = interp.to_object(&this)?; + if let Some(s) = boxed_primitive_string(interp, o)? { + return Ok(s); + } let receiver = Value::Object(o); let f = interp.get_from(o, &key("toString"), &receiver)?; if !interp.is_callable(&f) { diff --git a/crates/js/src/builtins/json.rs b/crates/js/src/builtins/json.rs index b84d9dc7b..b9f4a6c6f 100644 --- a/crates/js/src/builtins/json.rs +++ b/crates/js/src/builtins/json.rs @@ -574,7 +574,7 @@ fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult /// Whether looking up `name` from `o` reaches the object `proto` on the prototype chain before /// any object on the way, `proto` included, has its own property `name`. -fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult { +pub(super) fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult { let k = key(name); let mut cur = Some(o); while let Some(id) = cur { diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index f3d2cb7f6..8d9ad318c 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -380,6 +380,21 @@ fn to_locale_string_calls_to_string_on_the_boxed_value() { ); } +#[test] +fn to_locale_string_writes_the_primitive_of_a_boxed_value() { + assert_eq!(string("[1, 'x', true, false, 1.5].toLocaleString()"), "1,x,true,false,1.5"); + assert_eq!(string("Object.prototype.toLocaleString.call(5)"), "5"); + assert_eq!(string("Object.prototype.toLocaleString.call(Object('s'))"), "s"); + // Object.prototype.toString comes after the standard prototypes on the chain. + assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; [1, 'x', true].toLocaleString()"), "1,x,true"); + // A toString on the object or on its standard prototype is called. + assert_eq!(string("var n = Object(1); n.toString = function () { return 'own'; }; [n].toLocaleString()"), "own"); + assert_eq!(string("Object.getPrototypeOf(true).toString = function () { return 'p'; }; [true].toLocaleString()"), "p"); + assert_eq!(string("Object.getPrototypeOf(1).toString = Object.prototype.toString; [1].toLocaleString()"), "[object Number]"); + // Other objects still reach Object.prototype.toString. + assert_eq!(string("Object.prototype.toLocaleString.call({})"), "[object Object]"); +} + #[test] fn array_methods_overwriting_elements_need_no_heap_for_new_ones() { // With the heap exactly at its limit, an array method's write over an existing element