mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 03:43:29 +00:00
JSON: stringify unwraps Number and String objects to their primitive values
Number objects made by ToObject now carry their primitive value, as
String and Boolean objects already did (ECMA-262 5.1 sections 8.6.2
and 9.9). JSON.stringify reads it for a value, a replacer list entry
and the space argument (section 15.12.3, Str step 4, step 4.b.ii and
step 5) when the method that ToNumber or ToString calls first is not
found before the object's own prototype, so JSON.stringify(Object(3))
gives 3 and JSON.stringify(Object('abc')) gives "abc". A valueOf or
toString on the object or on its prototype still runs.
This commit is contained in:
@@ -78,7 +78,7 @@ fn try_install(interp: &mut Interp) -> JsResult<()> {
|
||||
}
|
||||
interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto));
|
||||
interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto));
|
||||
interp.intrinsics.number_proto = alloc(interp, Class::Number(0.0), Some(object_proto));
|
||||
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean(false), Some(object_proto));
|
||||
interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto));
|
||||
let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?;
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::collections::HashSet;
|
||||
|
||||
use super::{Builder, ELEMENT_BYTES, arg, data, key_of, length_of, method, new_array, new_string, to_integer};
|
||||
use crate::heap::{Class, Key, OBJECT_BYTES, OPEN, ObjId, Object, Prop, key, string_bytes};
|
||||
use crate::interp::{Desc, ErrorType, Interp, JsResult};
|
||||
use crate::interp::{Desc, ErrorType, Hint, Interp, JsResult};
|
||||
use crate::value::{Str, Value, number_to_str};
|
||||
|
||||
pub(super) fn install(interp: &mut Interp) -> JsResult<()> {
|
||||
@@ -459,14 +459,7 @@ fn settings(interp: &mut Interp, replacer: &Value, space: &Value) -> JsResult<Se
|
||||
s.properties = Some(property_list(interp, *r)?);
|
||||
}
|
||||
}
|
||||
let space = match space {
|
||||
Value::Object(o) => match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number) => Value::Number(interp.to_number(space)?),
|
||||
Some(Class::String(_)) => Value::String(interp.to_str(space)?),
|
||||
_ => space.clone(),
|
||||
},
|
||||
_ => space.clone(),
|
||||
};
|
||||
let space = unwrap_primitive(interp, space, false)?.unwrap_or_else(|| space.clone());
|
||||
match space {
|
||||
Value::Number(_) => {
|
||||
let n = to_integer(interp, &space)?.clamp(0.0, 10.0) as usize;
|
||||
@@ -489,9 +482,10 @@ fn property_list(interp: &mut Interp, r: ObjId) -> JsResult<Vec<Key>> {
|
||||
let item = match &v {
|
||||
Value::String(s) => Some(s.clone()),
|
||||
Value::Number(_) => Some(interp.primitive_to_str(&v)),
|
||||
Value::Object(o) if interp.heap.object(*o).is_some_and(|x| matches!(x.class, Class::String(_) | Class::Number)) => {
|
||||
Some(interp.to_str(&v)?)
|
||||
}
|
||||
Value::Object(_) => match unwrap_primitive(interp, &v, true)? {
|
||||
Some(Value::String(s)) => Some(s),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let Some(s) = item else { continue };
|
||||
@@ -546,12 +540,11 @@ fn resolve_rooted(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &K
|
||||
keep(interp, &v);
|
||||
}
|
||||
if let Value::Object(o) = &v {
|
||||
v = match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number) => Value::Number(interp.to_number(&v)?),
|
||||
Some(Class::String(_)) => Value::String(interp.to_str(&v)?),
|
||||
Some(Class::Boolean(b)) => Value::Bool(*b),
|
||||
_ => v,
|
||||
};
|
||||
if let Some(Class::Boolean(b)) = interp.heap.object(*o).map(|x| &x.class) {
|
||||
v = Value::Bool(*b);
|
||||
} else if let Some(p) = unwrap_primitive(interp, &v, false)? {
|
||||
v = p;
|
||||
}
|
||||
}
|
||||
Ok(match v {
|
||||
Value::Undefined => Item::Skip,
|
||||
@@ -561,6 +554,43 @@ fn resolve_rooted(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &K
|
||||
})
|
||||
}
|
||||
|
||||
/// ToNumber of a Number object, or ToString of a String object (of either when `as_string` is
|
||||
/// set), as section 15.12.3 applies them in Str step 4, step 4.b.ii and step 5. Returns `None`
|
||||
/// for any other value. When the method that [[DefaultValue]] (section 8.12.8) calls first is not
|
||||
/// found on the object or on its prototype chain before the object's standard prototype, the
|
||||
/// result is the object's [[PrimitiveValue]] (sections 8.6.2 and 9.9), the value that
|
||||
/// `Number.prototype.valueOf` (15.7.4.4), `Number.prototype.toString` (15.7.4.2) and
|
||||
/// `String.prototype.toString` (15.5.4.2) return. Otherwise ToPrimitive calls the method found.
|
||||
fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult<Option<Value>> {
|
||||
let Value::Object(o) = v else { return Ok(None) };
|
||||
let (primitive, proto, string) = match interp.heap.object(*o).map(|x| &x.class) {
|
||||
Some(Class::Number(n)) => (Value::Number(*n), interp.intrinsics.number_proto, as_string),
|
||||
Some(Class::String(s)) => (Value::String(s.clone()), interp.intrinsics.string_proto, true),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
let (hint, name) = if string { (Hint::String, "toString") } else { (Hint::Number, "valueOf") };
|
||||
let p = if reaches_prototype(interp, *o, proto, name)? { primitive } else { interp.to_primitive(v, hint)? };
|
||||
Ok(Some(if string { Value::String(interp.primitive_to_str(&p)) } else { Value::Number(interp.to_number(&p)?) }))
|
||||
}
|
||||
|
||||
/// 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<bool> {
|
||||
let k = key(name);
|
||||
let mut cur = Some(o);
|
||||
while let Some(id) = cur {
|
||||
interp.use_fuel()?;
|
||||
if interp.get_own_property(id, &k)?.is_some() {
|
||||
return Ok(false);
|
||||
}
|
||||
if id == proto {
|
||||
return Ok(true);
|
||||
}
|
||||
cur = interp.heap.object(id).and_then(|x| x.proto);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Quote (section 15.12.3): the string in double quotes with the characters JSON escapes.
|
||||
fn write_quoted(interp: &mut Interp, out: &mut Builder, s: &[u16]) -> JsResult<()> {
|
||||
out.push_units(interp, &[QUOTE])?;
|
||||
|
||||
@@ -529,6 +529,25 @@ fn json_stringify_writes_values_with_replacers_and_indentation() {
|
||||
assert_eq!(string("var x = {}; JSON.stringify([x, x])"), "[{},{}]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_stringify_unwraps_number_string_and_boolean_objects() {
|
||||
assert_eq!(string("JSON.stringify(Object(3))"), "3");
|
||||
assert_eq!(string("JSON.stringify(Object('abc'))"), "\"abc\"");
|
||||
assert_eq!(string("JSON.stringify(Object(true))"), "true");
|
||||
assert_eq!(string("JSON.stringify({n: Object(-0), m: Object(NaN), s: Object('')})"), "{\"n\":0,\"m\":null,\"s\":\"\"}");
|
||||
assert_eq!(string("JSON.stringify([Object.getPrototypeOf(Object(3)), Object.getPrototypeOf(Object('abc'))])"), "[0,\"\"]");
|
||||
// The space argument (step 5).
|
||||
assert_eq!(string("JSON.stringify([1], null, Object(2))"), "[\n 1\n]");
|
||||
assert_eq!(string("JSON.stringify([1], null, Object('--'))"), "[\n--1\n]");
|
||||
// Replacer list entries (step 4.b.ii).
|
||||
assert_eq!(string("JSON.stringify({a: 1, 3: 2, b: 3}, [Object('a'), Object(3)])"), "{\"a\":1,\"3\":2}");
|
||||
// Methods on Object.prototype do not reach the objects; methods on their own prototypes do.
|
||||
assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; JSON.stringify(Object('abc'))"), "\"abc\"");
|
||||
assert_eq!(string("Object.prototype.valueOf = function () { return 9; }; JSON.stringify(Object(3))"), "3");
|
||||
assert_eq!(string("Object.getPrototypeOf(Object(1)).valueOf = function () { return 4; }; JSON.stringify(Object(3))"), "4");
|
||||
assert_eq!(string("Object.getPrototypeOf(Object('')).toString = function () { return 'q'; }; JSON.stringify(Object('abc'))"), "\"q\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deep_json_nesting_needs_no_native_stack() {
|
||||
let handle = std::thread::Builder::new()
|
||||
|
||||
@@ -209,8 +209,8 @@ pub(crate) enum Class {
|
||||
Arguments(Box<ArgMap>),
|
||||
/// A `Boolean` object made by ToObject, with its primitive value.
|
||||
Boolean(bool),
|
||||
/// A `Number` object made by ToObject.
|
||||
Number,
|
||||
/// A `Number` object made by ToObject, with its primitive value.
|
||||
Number(f64),
|
||||
String(Str),
|
||||
RegExp,
|
||||
ForIn(Box<ForIn>),
|
||||
@@ -232,7 +232,7 @@ impl Class {
|
||||
Class::Error => "Error",
|
||||
Class::Arguments(_) => "Arguments",
|
||||
Class::Boolean(_) => "Boolean",
|
||||
Class::Number => "Number",
|
||||
Class::Number(_) => "Number",
|
||||
Class::String(_) => "String",
|
||||
Class::RegExp => "RegExp",
|
||||
}
|
||||
|
||||
@@ -2240,7 +2240,7 @@ impl Interp {
|
||||
Value::Object(o) => return Ok(*o),
|
||||
Value::Undefined | Value::Null => return Err(self.type_error("cannot convert undefined or null to an object")),
|
||||
Value::Bool(b) => (Class::Boolean(*b), self.intrinsics.boolean_proto),
|
||||
Value::Number(_) => (Class::Number, self.intrinsics.number_proto),
|
||||
Value::Number(n) => (Class::Number(*n), self.intrinsics.number_proto),
|
||||
Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto),
|
||||
};
|
||||
Ok(self.heap.alloc_object(Object::new(class, Some(proto))))
|
||||
|
||||
Reference in New Issue
Block a user