From 39576b22385a2e6b6fc4fbf5e90e6b72157e9ee2 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Wed, 10 Jun 2020 11:01:00 -0700 Subject: [PATCH] LibJS: Add JSON.stringify --- Libraries/LibJS/CMakeLists.txt | 1 + Libraries/LibJS/Runtime/ArrayConstructor.cpp | 5 +- Libraries/LibJS/Runtime/BooleanObject.h | 2 +- Libraries/LibJS/Runtime/BooleanPrototype.cpp | 4 +- Libraries/LibJS/Runtime/ErrorTypes.h | 2 + Libraries/LibJS/Runtime/GlobalObject.cpp | 2 + Libraries/LibJS/Runtime/JSONObject.cpp | 373 ++++++++++++++++++ Libraries/LibJS/Runtime/JSONObject.h | 59 +++ Libraries/LibJS/Runtime/NumberObject.h | 1 + Libraries/LibJS/Runtime/Object.h | 3 +- Libraries/LibJS/Runtime/ProxyObject.h | 1 + Libraries/LibJS/Runtime/Uint8ClampedArray.h | 1 - Libraries/LibJS/Runtime/Value.cpp | 15 + Libraries/LibJS/Runtime/Value.h | 4 +- Libraries/LibJS/Tests/JSON.stringify-order.js | 36 ++ Libraries/LibJS/Tests/JSON.stringify-proxy.js | 18 + .../LibJS/Tests/JSON.stringify-replacer.js | 39 ++ Libraries/LibJS/Tests/JSON.stringify-space.js | 51 +++ Libraries/LibJS/Tests/JSON.stringify.js | 71 ++++ 19 files changed, 678 insertions(+), 10 deletions(-) create mode 100644 Libraries/LibJS/Runtime/JSONObject.cpp create mode 100644 Libraries/LibJS/Runtime/JSONObject.h create mode 100644 Libraries/LibJS/Tests/JSON.stringify-order.js create mode 100644 Libraries/LibJS/Tests/JSON.stringify-proxy.js create mode 100644 Libraries/LibJS/Tests/JSON.stringify-replacer.js create mode 100644 Libraries/LibJS/Tests/JSON.stringify-space.js create mode 100644 Libraries/LibJS/Tests/JSON.stringify.js diff --git a/Libraries/LibJS/CMakeLists.txt b/Libraries/LibJS/CMakeLists.txt index b033360350f..af35aa7a848 100644 --- a/Libraries/LibJS/CMakeLists.txt +++ b/Libraries/LibJS/CMakeLists.txt @@ -34,6 +34,7 @@ set(SOURCES Runtime/FunctionPrototype.cpp Runtime/GlobalObject.cpp Runtime/IndexedProperties.cpp + Runtime/JSONObject.cpp Runtime/LexicalEnvironment.cpp Runtime/MarkedValueList.cpp Runtime/MathObject.cpp diff --git a/Libraries/LibJS/Runtime/ArrayConstructor.cpp b/Libraries/LibJS/Runtime/ArrayConstructor.cpp index 1f9a788ede5..23b155ea817 100644 --- a/Libraries/LibJS/Runtime/ArrayConstructor.cpp +++ b/Libraries/LibJS/Runtime/ArrayConstructor.cpp @@ -81,10 +81,7 @@ Value ArrayConstructor::construct(Interpreter& interpreter) Value ArrayConstructor::is_array(Interpreter& interpreter) { auto value = interpreter.argument(0); - if (!value.is_array()) - return Value(false); - // Exclude TypedArray and similar - return Value(StringView(value.as_object().class_name()) == "Array"); + return Value(value.is_array()); } Value ArrayConstructor::of(Interpreter& interpreter) diff --git a/Libraries/LibJS/Runtime/BooleanObject.h b/Libraries/LibJS/Runtime/BooleanObject.h index c09a6b0f9dd..4ecaaec3a54 100644 --- a/Libraries/LibJS/Runtime/BooleanObject.h +++ b/Libraries/LibJS/Runtime/BooleanObject.h @@ -43,7 +43,7 @@ public: private: virtual const char* class_name() const override { return "BooleanObject"; } - virtual bool is_boolean() const override { return true; } + virtual bool is_boolean_object() const override { return true; } bool m_value { false }; }; } diff --git a/Libraries/LibJS/Runtime/BooleanPrototype.cpp b/Libraries/LibJS/Runtime/BooleanPrototype.cpp index 3129bd579b6..ce6795c8d66 100644 --- a/Libraries/LibJS/Runtime/BooleanPrototype.cpp +++ b/Libraries/LibJS/Runtime/BooleanPrototype.cpp @@ -47,7 +47,7 @@ Value BooleanPrototype::to_string(Interpreter& interpreter) if (this_object.is_boolean()) { return js_string(interpreter.heap(), this_object.as_bool() ? "true" : "false"); } - if (!this_object.is_object() || !this_object.as_object().is_boolean()) { + if (!this_object.is_object() || !this_object.as_object().is_boolean_object()) { interpreter.throw_exception(ErrorType::NotA, "Boolean"); return {}; } @@ -62,7 +62,7 @@ Value BooleanPrototype::value_of(Interpreter& interpreter) if (this_object.is_boolean()) { return this_object; } - if (!this_object.is_object() || !this_object.as_object().is_boolean()) { + if (!this_object.is_object() || !this_object.as_object().is_boolean_object()) { interpreter.throw_exception(ErrorType::NotA, "Boolean"); return {}; } diff --git a/Libraries/LibJS/Runtime/ErrorTypes.h b/Libraries/LibJS/Runtime/ErrorTypes.h index 8d263b01c61..75835195a9d 100644 --- a/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Libraries/LibJS/Runtime/ErrorTypes.h @@ -47,6 +47,8 @@ M(InvalidLeftHandAssignment, "Invalid left-hand side in assignment") \ M(IsNotA, "%s is not a %s") \ M(IsNotAEvaluatedFrom, "%s is not a %s (evaluated from '%s')") \ + M(JsonBigInt, "Cannot serialize BigInt value to JSON") \ + M(JsonCircular, "Cannot stringify circular object") \ M(NotA, "Not a %s object") \ M(NotACtor, "%s is not a constructor") \ M(NotAFunction, "%s is not a function") \ diff --git a/Libraries/LibJS/Runtime/GlobalObject.cpp b/Libraries/LibJS/Runtime/GlobalObject.cpp index 5da7a1dbd65..3d9d7fdfb89 100644 --- a/Libraries/LibJS/Runtime/GlobalObject.cpp +++ b/Libraries/LibJS/Runtime/GlobalObject.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -96,6 +97,7 @@ void GlobalObject::initialize() define_property("globalThis", this, attr); define_property("console", heap().allocate(), attr); define_property("Math", heap().allocate(), attr); + define_property("JSON", heap().allocate(), attr); define_property("Reflect", heap().allocate(), attr); add_constructor("Array", m_array_constructor, *m_array_prototype); diff --git a/Libraries/LibJS/Runtime/JSONObject.cpp b/Libraries/LibJS/Runtime/JSONObject.cpp new file mode 100644 index 00000000000..dae2b5af5f1 --- /dev/null +++ b/Libraries/LibJS/Runtime/JSONObject.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2020, Matthew Olsson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace JS { + +JSONObject::JSONObject() + : Object(interpreter().global_object().object_prototype()) +{ + u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function("stringify", stringify, 3, attr); + define_native_function("parse", parse, 1, attr); +} + +JSONObject::~JSONObject() +{ +} + +Value JSONObject::stringify(Interpreter& interpreter) +{ + if (!interpreter.argument_count()) + return js_undefined(); + auto value = interpreter.argument(0); + auto replacer = interpreter.argument(1); + auto space = interpreter.argument(2); + + StringifyState state; + + if (replacer.is_object()) { + if (replacer.as_object().is_function()) { + state.replacer_function = &replacer.as_function(); + } else if (replacer.is_array()) { + auto& replacer_object = replacer.as_object(); + auto replacer_length = length_of_array_like(interpreter, replacer); + if (interpreter.exception()) + return {}; + Vector list; + for (size_t i = 0; i < replacer_length; ++i) { + auto replacer_value = replacer_object.get(i); + if (interpreter.exception()) + return {}; + String item; + if (replacer_value.is_string() || replacer_value.is_number()) { + item = replacer_value.to_string(interpreter); + if (interpreter.exception()) + return {}; + } else if (replacer_value.is_object()) { + auto& value_object = replacer_value.as_object(); + if (value_object.is_string_object() || value_object.is_number_object()) { + item = value_object.value_of().to_string(interpreter); + if (interpreter.exception()) + return { }; + } + } + if (!item.is_null() && !list.contains_slow(item)) { + list.append(item); + } + } + state.property_list = list; + } + } + + if (space.is_object()) { + auto& space_obj = space.as_object(); + if (space_obj.is_string_object() || space_obj.is_number_object()) + space = space_obj.value_of(); + } + + if (space.is_number()) { + StringBuilder gap_builder; + auto gap_size = min(10, space.as_i32()); + for (auto i = 0; i < gap_size; ++i) + gap_builder.append(' '); + state.gap = gap_builder.to_string(); + } else if (space.is_string()) { + auto string = space.as_string().string(); + if (string.length() <= 10) { + state.gap = string; + } else { + state.gap = string.substring(0, 10); + } + } else { + state.gap = String::empty(); + } + + auto* wrapper = Object::create_empty(interpreter, interpreter.global_object()); + wrapper->define_property(String::empty(), value); + if (interpreter.exception()) + return {}; + auto result = serialize_json_property(interpreter, state, String::empty(), wrapper); + if (interpreter.exception()) + return {}; + if (result.is_null()) + return js_undefined(); + return js_string(interpreter, result); +} + +String JSONObject::serialize_json_property(Interpreter& interpreter, StringifyState& state, const PropertyName& key, Object* holder) +{ + auto value = holder->get(key); + if (value.is_object()) { + auto to_json = value.as_object().get("toJSON"); + if (interpreter.exception()) + return {}; + if (to_json.is_function()) { + MarkedValueList arguments(interpreter.heap()); + arguments.append(js_string(interpreter, key.to_string())); + value = interpreter.call(to_json.as_function(), value, move(arguments)); + if (interpreter.exception()) + return {}; + } + } + + if (state.replacer_function) { + MarkedValueList arguments(interpreter.heap()); + arguments.values().append(js_string(interpreter, key.to_string())); + arguments.values().append(value); + value = interpreter.call(*state.replacer_function, holder, move(arguments)); + if (interpreter.exception()) + return {}; + } + + if (value.is_object()) { + auto& value_object = value.as_object(); + if (value_object.is_number_object() || value_object.is_boolean_object() || value_object.is_string_object() || value_object.is_bigint_object()) + value = value_object.value_of(); + } + + if (value.is_null()) + return "null"; + if (value.is_boolean()) + return value.as_bool() ? "true" : "false"; + if (value.is_string()) + return quote_json_string(value.as_string().string()); + if (value.is_number()) { + if (value.is_finite_number()) + return value.to_string(interpreter); + return "null"; + } + if (value.is_object() && !value.is_function()) { + if (value.is_array()) + return serialize_json_array(interpreter, state, static_cast(value.as_object())); + return serialize_json_object(interpreter, state, value.as_object()); + } + if (value.is_bigint()) + interpreter.throw_exception(ErrorType::JsonBigInt); + return {}; +} + +String JSONObject::serialize_json_object(Interpreter& interpreter, StringifyState& state, Object& object) +{ + if (state.seen_objects.contains(&object)) { + interpreter.throw_exception(ErrorType::JsonCircular); + return {}; + } + + state.seen_objects.set(&object); + String previous_indent = state.indent; + state.indent = String::format("%s%s", state.indent.characters(), state.gap.characters()); + Vector property_strings; + + auto process_property = [&](const PropertyName& key) { + auto serialized_property_string = serialize_json_property(interpreter, state, key, &object); + if (interpreter.exception()) + return; + if (!serialized_property_string.is_null()) { + property_strings.append(String::format( + "%s:%s%s", + quote_json_string(key.to_string()).characters(), + state.gap.is_empty() ? "" : " ", + serialized_property_string.characters() + )); + } + }; + + if (state.property_list.has_value()) { + auto property_list = state.property_list.value(); + for (auto& property : property_list) { + process_property(property); + if (interpreter.exception()) + return {}; + } + } else { + for (auto& entry : object.indexed_properties()) { + auto value_and_attributes = entry.value_and_attributes(&object); + if (!value_and_attributes.attributes.is_enumerable()) + continue; + process_property(entry.index()); + if (interpreter.exception()) + return {}; + } + for (auto&[key, metadata] : object.shape().property_table_ordered()) { + if (!metadata.attributes.is_enumerable()) + continue; + process_property(key); + if (interpreter.exception()) + return {}; + } + } + StringBuilder builder; + if (property_strings.is_empty()) { + builder.append("{}"); + } else { + bool first = true; + builder.append('{'); + if (state.gap.is_empty()) { + for (auto& property_string : property_strings) { + if (!first) + builder.append(','); + first = false; + builder.append(property_string); + } + } else { + builder.append('\n'); + builder.append(state.indent); + auto separator = String::format(",\n%s", state.indent.characters()); + for (auto& property_string : property_strings) { + if (!first) + builder.append(separator); + first = false; + builder.append(property_string); + } + builder.append('\n'); + builder.append(previous_indent); + } + builder.append('}'); + } + + state.seen_objects.remove(&object); + state.indent = previous_indent; + return builder.to_string(); +} + +String JSONObject::serialize_json_array(Interpreter& interpreter, StringifyState& state, Object& object) +{ + if (state.seen_objects.contains(&object)) { + interpreter.throw_exception(ErrorType::JsonCircular); + return {}; + } + + state.seen_objects.set(&object); + String previous_indent = state.indent; + state.indent = String::format("%s%s", state.indent.characters(), state.gap.characters()); + Vector property_strings; + + auto length = length_of_array_like(interpreter, Value(&object)); + if (interpreter.exception()) + return {}; + for (size_t i = 0; i < length; ++i) { + if (interpreter.exception()) + return {}; + auto serialized_property_string = serialize_json_property(interpreter, state, i, &object); + if (interpreter.exception()) + return {}; + if (serialized_property_string.is_null()) { + property_strings.append("null"); + } else { + property_strings.append(serialized_property_string); + } + } + + StringBuilder builder; + if (property_strings.is_empty()) { + builder.append("[]"); + } else { + if (state.gap.is_empty()) { + builder.append('['); + bool first = true; + for (auto& property_string : property_strings) { + if (!first) + builder.append(','); + first = false; + builder.append(property_string); + } + builder.append(']'); + } else { + builder.append("[\n"); + builder.append(state.indent); + auto separator = String::format(",\n%s", state.indent.characters()); + bool first = true; + for (auto& property_string : property_strings) { + if (!first) + builder.append(separator); + first = false; + builder.append(property_string); + } + builder.append('\n'); + builder.append(previous_indent); + builder.append(']'); + } + } + + state.seen_objects.remove(&object); + state.indent = previous_indent; + return builder.to_string(); +} + +String JSONObject::quote_json_string(String string) +{ + // FIXME: Handle UTF16 + StringBuilder builder; + builder.append('"'); + for (auto& ch : string) { + switch (ch) { + case '\b': + builder.append("\\b"); + break; + case '\t': + builder.append("\\t"); + break; + case '\n': + builder.append("\\n"); + break; + case '\f': + builder.append("\\f"); + break; + case '\r': + builder.append("\\r"); + break; + case '"': + builder.append("\\\""); + break; + case '\\': + builder.append("\\\\"); + break; + default: + if (ch < 0x20) { + builder.append("\\u%#08x", ch); + } else { + builder.append(ch); + } + } + } + builder.append('"'); + return builder.to_string(); +} + +Value JSONObject::parse(Interpreter&) +{ + return js_undefined(); +} + +} diff --git a/Libraries/LibJS/Runtime/JSONObject.h b/Libraries/LibJS/Runtime/JSONObject.h new file mode 100644 index 00000000000..fa5491b2103 --- /dev/null +++ b/Libraries/LibJS/Runtime/JSONObject.h @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020, Matthew Olsson + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include + +namespace JS { + +class JSONObject final : public Object { +public: + JSONObject(); + virtual ~JSONObject() override; + +private: + struct StringifyState { + Function* replacer_function { nullptr }; + HashTable seen_objects; + String indent { String::empty() }; + String gap; + Optional> property_list; + }; + + virtual const char* class_name() const override { return "JSONObject"; } + + // Stringify helpers + static String serialize_json_property(Interpreter&, StringifyState&, const PropertyName& key, Object* holder); + static String serialize_json_object(Interpreter&, StringifyState&, Object&); + static String serialize_json_array(Interpreter&, StringifyState&, Object&); + static String quote_json_string(String); + + static Value stringify(Interpreter&); + static Value parse(Interpreter&); +}; + +} diff --git a/Libraries/LibJS/Runtime/NumberObject.h b/Libraries/LibJS/Runtime/NumberObject.h index a5a06c297db..c316505d66b 100644 --- a/Libraries/LibJS/Runtime/NumberObject.h +++ b/Libraries/LibJS/Runtime/NumberObject.h @@ -37,6 +37,7 @@ public: NumberObject(double, Object& prototype); virtual ~NumberObject() override; + virtual bool is_number_object() const override { return true; } virtual Value value_of() const override { return Value(m_value); } private: diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index 9e7dff89593..768ad03dd2c 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -96,7 +96,6 @@ public: virtual Value delete_property(PropertyName); virtual bool is_array() const { return false; } - virtual bool is_boolean() const { return false; } virtual bool is_date() const { return false; } virtual bool is_error() const { return false; } virtual bool is_function() const { return false; } @@ -105,7 +104,9 @@ public: virtual bool is_native_property() const { return false; } virtual bool is_proxy_object() const { return false; } virtual bool is_regexp_object() const { return false; } + virtual bool is_boolean_object() const { return false; } virtual bool is_string_object() const { return false; } + virtual bool is_number_object() const { return false; } virtual bool is_symbol_object() const { return false; } virtual bool is_bigint_object() const { return false; } diff --git a/Libraries/LibJS/Runtime/ProxyObject.h b/Libraries/LibJS/Runtime/ProxyObject.h index 3909e677b3e..9d57f885d67 100644 --- a/Libraries/LibJS/Runtime/ProxyObject.h +++ b/Libraries/LibJS/Runtime/ProxyObject.h @@ -58,6 +58,7 @@ private: virtual void visit_children(Visitor&) override; virtual const char* class_name() const override { return "ProxyObject"; } virtual bool is_proxy_object() const override { return true; } + virtual bool is_array() const override { return m_target.is_array(); }; Object& m_target; Object& m_handler; diff --git a/Libraries/LibJS/Runtime/Uint8ClampedArray.h b/Libraries/LibJS/Runtime/Uint8ClampedArray.h index 8b2459c0092..a58c6c5dd4b 100644 --- a/Libraries/LibJS/Runtime/Uint8ClampedArray.h +++ b/Libraries/LibJS/Runtime/Uint8ClampedArray.h @@ -47,7 +47,6 @@ public: private: virtual const char* class_name() const override { return "Uint8ClampedArray"; } - virtual bool is_array() const override { return true; } static Value length_getter(Interpreter&); diff --git a/Libraries/LibJS/Runtime/Value.cpp b/Libraries/LibJS/Runtime/Value.cpp index 561bc7d2ca5..674769f2f58 100644 --- a/Libraries/LibJS/Runtime/Value.cpp +++ b/Libraries/LibJS/Runtime/Value.cpp @@ -81,6 +81,12 @@ bool Value::is_array() const return is_object() && as_object().is_array(); } +Array& Value::as_array() +{ + ASSERT(is_array()); + return static_cast(*m_value.as_object); +} + bool Value::is_function() const { return is_object() && as_object().is_function(); @@ -940,4 +946,13 @@ TriState abstract_relation(Interpreter& interpreter, bool left_first, Value lhs, return TriState::False; } +size_t length_of_array_like(Interpreter& interpreter, Value value) +{ + ASSERT(value.is_object()); + auto result = value.as_object().get("length"); + if (interpreter.exception()) + return 0; + return result.to_size_t(interpreter); +} + } diff --git a/Libraries/LibJS/Runtime/Value.h b/Libraries/LibJS/Runtime/Value.h index 2d72505c189..e328488aeca 100644 --- a/Libraries/LibJS/Runtime/Value.h +++ b/Libraries/LibJS/Runtime/Value.h @@ -220,6 +220,7 @@ public: return *m_value.as_bigint; } + Array& as_array(); Function& as_function(); i32 as_i32() const; @@ -313,7 +314,8 @@ bool strict_eq(Interpreter&, Value lhs, Value rhs); bool same_value(Interpreter&, Value lhs, Value rhs); bool same_value_zero(Interpreter&, Value lhs, Value rhs); bool same_value_non_numeric(Interpreter&, Value lhs, Value rhs); -TriState abstract_relation(Interpreter& interpreter, bool left_first, Value lhs, Value rhs); +TriState abstract_relation(Interpreter&, bool left_first, Value lhs, Value rhs); +size_t length_of_array_like(Interpreter&, Value); const LogStream& operator<<(const LogStream&, const Value&); diff --git a/Libraries/LibJS/Tests/JSON.stringify-order.js b/Libraries/LibJS/Tests/JSON.stringify-order.js new file mode 100644 index 00000000000..6dfab9e3df0 --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.stringify-order.js @@ -0,0 +1,36 @@ +load("test-common.js"); + +try { + assert(JSON.stringify.length === 3); + + let o = { + key1: "key1", + key2: "key2", + key3: "key3", + }; + + Object.defineProperty(o, "defined", { + enumerable: true, + get() { + o.prop = "prop"; + return "defined"; + }, + }); + + o.key4 = "key4"; + + o[2] = 2; + o[0] = 0; + o[1] = 1; + + delete o.key1; + delete o.key3; + + o.key1 = "key1"; + + assert(JSON.stringify(o) === '{"0":0,"1":1,"2":2,"key2":"key2","defined":"defined","key4":"key4","key1":"key1"}'); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/JSON.stringify-proxy.js b/Libraries/LibJS/Tests/JSON.stringify-proxy.js new file mode 100644 index 00000000000..4db7b22b410 --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.stringify-proxy.js @@ -0,0 +1,18 @@ +load("test-common.js"); + +try { + let p = new Proxy([], { + get(_, key) { + if (key === "length") + return 3; + return Number(key); + }, + }); + + assert(JSON.stringify(p) === "[0,1,2]"); + assert(JSON.stringify([[new Proxy(p, {})]]) === "[[[0,1,2]]]"); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/JSON.stringify-replacer.js b/Libraries/LibJS/Tests/JSON.stringify-replacer.js new file mode 100644 index 00000000000..e994d25ae4c --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.stringify-replacer.js @@ -0,0 +1,39 @@ +load("test-common.js"); + +try { + let o = { + var1: "foo", + var2: 42, + arr: [1, 2, { + nested: { + hello: "world", + }, + get x() { return 10; } + }], + obj: { + subarr: [3], + }, + }; + + let string = JSON.stringify(o, (key, value) => { + if (key === "hello") + return undefined; + if (value === 10) + return 20; + if (key === "subarr") + return [3, 4, 5]; + return value; + }); + + assert(string === '{"var1":"foo","var2":42,"arr":[1,2,{"nested":{},"x":20}],"obj":{"subarr":[3,4,5]}}'); + + string = JSON.stringify(o, ["var1", "var1", "var2", "obj"]); + assert(string == '{"var1":"foo","var2":42,"obj":{}}'); + + string = JSON.stringify(o, ["var1", "var1", "var2", "obj", "subarr"]); + assert(string == '{"var1":"foo","var2":42,"obj":{"subarr":[3]}}'); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/JSON.stringify-space.js b/Libraries/LibJS/Tests/JSON.stringify-space.js new file mode 100644 index 00000000000..33d49545a8b --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.stringify-space.js @@ -0,0 +1,51 @@ +load("test-common.js"); + +try { + let o = { + foo: 1, + bar: "baz", + qux: { + get x() { return 10; }, + y() { return 20; }, + arr: [1, 2, 3], + } + }; + + let string = JSON.stringify(o, null, 4); + let expected = +`{ + "foo": 1, + "bar": "baz", + "qux": { + "x": 10, + "arr": [ + 1, + 2, + 3 + ] + } +}`; + + assert(string === expected); + + string = JSON.stringify(o, null, "abcd"); + expected = +`{ +abcd"foo": 1, +abcd"bar": "baz", +abcd"qux": { +abcdabcd"x": 10, +abcdabcd"arr": [ +abcdabcdabcd1, +abcdabcdabcd2, +abcdabcdabcd3 +abcdabcd] +abcd} +}`; + + assert(string === expected); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/JSON.stringify.js b/Libraries/LibJS/Tests/JSON.stringify.js new file mode 100644 index 00000000000..d071ea961b7 --- /dev/null +++ b/Libraries/LibJS/Tests/JSON.stringify.js @@ -0,0 +1,71 @@ +load("test-common.js"); + +try { + assert(JSON.stringify.length === 3); + + assertThrowsError(() => { + JSON.stringify(5n); + }, { + error: TypeError, + message: "Cannot serialize BigInt value to JSON", + }); + + const properties = [ + [5, "5"], + [undefined, undefined], + [null, "null"], + [NaN, "null"], + [-NaN, "null"], + [Infinity, "null"], + [-Infinity, "null"], + [true, "true"], + [false, "false"], + ["test", '"test"'], + [new Number(5), "5"], + [new Boolean(false), "false"], + [new String("test"), '"test"'], + [() => {}, undefined], + [[1, 2, "foo"], '[1,2,"foo"]'], + [{ foo: 1, bar: "baz", qux() {} }, '{"foo":1,"bar":"baz"}'], + [ + { + var1: 1, + var2: 2, + toJSON(key) { + let o = this; + o.var2 = 10; + return o; + } + }, + '{"var1":1,"var2":10}', + ], + ]; + + properties.forEach(testCase => { + assert(JSON.stringify(testCase[0]) === testCase[1]); + }); + + let bad1 = {}; + bad1.foo = bad1; + let bad2 = []; + bad2[5] = [[[bad2]]]; + + let bad3a = { foo: "bar" }; + let bad3b = [1, 2, bad3a]; + bad3a.bad = bad3b; + + [bad1, bad2, bad3a].forEach(bad => assertThrowsError(() => { + JSON.stringify(bad); + }, { + error: TypeError, + message: "Cannot stringify circular object", + })); + + let o = { foo: "bar" }; + Object.defineProperty(o, "baz", { value: "qux", enumerable: false }); + assert(JSON.stringify(o) === '{"foo":"bar"}'); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +}