diff --git a/boa/src/builtins/json/mod.rs b/boa/src/builtins/json/mod.rs index 7a5e80458c..ad7cd378fd 100644 --- a/boa/src/builtins/json/mod.rs +++ b/boa/src/builtins/json/mod.rs @@ -15,6 +15,8 @@ use crate::builtins::{ function::make_builtin_fn, + object::ObjectKind, + property::Property, value::{ResultValue, Value}, }; use crate::exec::Interpreter; @@ -65,10 +67,66 @@ pub fn parse(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue /// /// [spec]: https://tc39.es/ecma262/#sec-json.stringify /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify -pub fn stringify(_: &mut Value, args: &[Value], _: &mut Interpreter) -> ResultValue { - let obj = args.get(0).expect("cannot get argument for JSON.stringify"); - let json = obj.to_json().to_string(); - Ok(Value::from(json)) +pub fn stringify(_: &mut Value, args: &[Value], interpreter: &mut Interpreter) -> ResultValue { + let object = match args.get(0) { + Some(obj) if obj.is_symbol() || obj.is_function() => return Ok(Value::undefined()), + None => return Ok(Value::undefined()), + Some(obj) => obj, + }; + let replacer = match args.get(1) { + Some(replacer) if replacer.is_object() => replacer, + _ => return Ok(Value::from(object.to_json().to_string())), + }; + + let replacer_as_object = replacer + .as_object() + .expect("JSON.stringify replacer was an object"); + if replacer_as_object.is_callable() { + object + .as_object() + .map(|obj| { + let object_to_return = Value::new_object(None); + for (key, val) in obj + .properties + .iter() + .filter_map(|(k, v)| v.value.as_ref().map(|value| (k, value))) + { + let mut this_arg = object.clone(); + object_to_return.set_property( + key.to_owned(), + Property::default().value(interpreter.call( + replacer, + &mut this_arg, + &[Value::string(key), val.clone()], + )?), + ); + } + Ok(Value::from(object_to_return.to_json().to_string())) + }) + .ok_or_else(Value::undefined)? + } else if replacer_as_object.kind == ObjectKind::Array { + let mut obj_to_return = + serde_json::Map::with_capacity(replacer_as_object.properties.len() - 1); + let fields = replacer_as_object.properties.keys().filter_map(|key| { + if key == "length" { + None + } else { + Some(replacer.get_field(key.to_string())) + } + }); + for field in fields { + if let Some(value) = object + .get_property(&field.to_string()) + .map(|prop| prop.value.as_ref().map(|v| v.to_json())) + .flatten() + { + obj_to_return.insert(field.to_string(), value); + } + } + Ok(Value::from(JSONValue::Object(obj_to_return).to_string())) + } else { + Ok(Value::from(object.to_json().to_string())) + } } /// Create a new `JSON` object. diff --git a/boa/src/builtins/json/tests.rs b/boa/src/builtins/json/tests.rs index ba8535884e..e5e419540a 100644 --- a/boa/src/builtins/json/tests.rs +++ b/boa/src/builtins/json/tests.rs @@ -16,3 +16,176 @@ fn json_sanity() { "true" ); } + +#[test] +fn json_stringify_remove_undefined_values_from_objects() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let actual = forward( + &mut engine, + r#"JSON.stringify({ aaa: undefined, bbb: 'ccc' })"#, + ); + let expected = r#"{"bbb":"ccc"}"#; + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_remove_function_values_from_objects() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let actual = forward( + &mut engine, + r#"JSON.stringify({ aaa: () => {}, bbb: 'ccc' })"#, + ); + let expected = r#"{"bbb":"ccc"}"#; + + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +// there is a bug for setting a symbol as a field's value +fn json_stringify_remove_symbols_from_objects() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let actual = forward( + &mut engine, + r#"JSON.stringify({ aaa: Symbol(), bbb: 'ccc' })"#, + ); + let expected = r#"{"bbb":"ccc"}"#; + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_array_strings() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward( + &mut engine, + r#"JSON.stringify({aaa: 'bbb', bbb: 'ccc', ccc: 'ddd'}, ['aaa', 'bbb'])"#, + ); + let expected = forward(&mut engine, r#"'{"aaa":"bbb","bbb":"ccc"}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_array_numbers() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward( + &mut engine, + r#"JSON.stringify({ 0: 'aaa', 1: 'bbb', 2: 'ccc'}, [1, 2])"#, + ); + let expected = forward(&mut engine, r#"'{"1":"bbb","2":"ccc"}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_function() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward( + &mut engine, + r#"JSON.stringify({ aaa: 1, bbb: 2}, (key, value) => { + if (key === 'aaa') { + return undefined; + } + + return value; + })"#, + ); + let expected = forward(&mut engine, r#"'{"bbb":2}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_arrays() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward(&mut engine, r#"JSON.stringify(['a', 'b'])"#); + let expected = forward(&mut engine, r#"'["a","b"]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_object_array() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward(&mut engine, r#"JSON.stringify([{a: 'b'}, {b: 'c'}])"#); + let expected = forward(&mut engine, r#"'[{"a":"b"},{"b":"c"}]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_array_converts_undefined_to_null() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward(&mut engine, r#"JSON.stringify([undefined])"#); + let expected = forward(&mut engine, r#"'[null]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_array_converts_function_to_null() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward(&mut engine, r#"JSON.stringify([() => {}])"#); + let expected = forward(&mut engine, r#"'[null]'"#); + + assert_eq!(actual, expected); +} + +#[test] +#[ignore] +fn json_stringify_array_converts_symbol_to_null() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + let actual = forward(&mut engine, r#"JSON.stringify([Symbol()])"#); + let expected = forward(&mut engine, r#"'[null]'"#); + + assert_eq!(actual, expected); +} +#[test] +fn json_stringify_function_replacer_propogate_error() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let actual = forward( + &mut engine, + r#" + let thrown = 0; + try { + JSON.stringify({x: 1}, (key, value) => { throw 1 }) + } catch (err) { + thrown = err; + } + thrown + "#, + ); + let expected = forward(&mut engine, r#"1"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_return_undefined() { + let realm = Realm::create(); + let mut engine = Interpreter::new(realm); + + let actual_no_args = forward(&mut engine, r#"JSON.stringify()"#); + let actual_function = forward(&mut engine, r#"JSON.stringify(() => {})"#); + let actual_symbol = forward(&mut engine, r#"JSON.stringify(Symbol())"#); + let expected = forward(&mut engine, r#"undefined"#); + + assert_eq!(actual_no_args, expected); + assert_eq!(actual_function, expected); + assert_eq!(actual_symbol, expected); +} diff --git a/boa/src/builtins/value/mod.rs b/boa/src/builtins/value/mod.rs index 0d3f86672c..ec682c7b09 100644 --- a/boa/src/builtins/value/mod.rs +++ b/boa/src/builtins/value/mod.rs @@ -717,16 +717,33 @@ impl ValueData { /// Conversts the `Value` to `JSON`. pub fn to_json(&self) -> JSONValue { match *self { - Self::Null | Self::Symbol(_) | Self::Undefined => JSONValue::Null, + Self::Null => JSONValue::Null, Self::Boolean(b) => JSONValue::Bool(b), Self::Object(ref obj) => { - let new_obj = obj - .borrow() - .properties - .iter() - .map(|(k, _)| (k.clone(), self.get_field(k.as_str()).to_json())) - .collect::>(); - JSONValue::Object(new_obj) + if obj.borrow().kind == ObjectKind::Array { + let mut arr: Vec = Vec::new(); + obj.borrow().properties.keys().for_each(|k| { + if k != "length" { + let value = self.get_field(k.to_string()); + if value.is_undefined() || value.is_function() { + arr.push(JSONValue::Null); + } else { + arr.push(self.get_field(k.to_string()).to_json()); + } + } + }); + JSONValue::Array(arr) + } else { + let mut new_obj = Map::new(); + obj.borrow().properties.keys().for_each(|k| { + let key = k.clone(); + let value = self.get_field(k.to_string()); + if !value.is_undefined() && !value.is_function() { + new_obj.insert(key, value.to_json()); + } + }); + JSONValue::Object(new_obj) + } } Self::String(ref str) => JSONValue::String(str.clone()), Self::Rational(num) => JSONValue::Number( @@ -737,6 +754,9 @@ impl ValueData { // TODO: throw TypeError panic!("TypeError: \"BigInt value can't be serialized in JSON\""); } + Self::Symbol(_) | Self::Undefined => { + unreachable!("Symbols and Undefined JSON Values depend on parent type"); + } } }