From 683a0696f3e37d038e33543f128f4bb61ed29fab Mon Sep 17 00:00:00 2001 From: mattco98 Date: Wed, 29 Apr 2020 18:59:23 -0700 Subject: [PATCH] LibJS: Add Object.{keys,values,entries}() --- Libraries/LibJS/Runtime/Object.cpp | 63 +++++++++++++++++++ Libraries/LibJS/Runtime/Object.h | 7 +++ Libraries/LibJS/Runtime/ObjectConstructor.cpp | 39 ++++++++++++ Libraries/LibJS/Runtime/ObjectConstructor.h | 3 + Libraries/LibJS/Tests/Object.entries.js | 54 ++++++++++++++++ Libraries/LibJS/Tests/Object.keys.js | 42 +++++++++++++ Libraries/LibJS/Tests/Object.values.js | 42 +++++++++++++ 7 files changed, 250 insertions(+) create mode 100644 Libraries/LibJS/Tests/Object.entries.js create mode 100644 Libraries/LibJS/Tests/Object.keys.js create mode 100644 Libraries/LibJS/Tests/Object.values.js diff --git a/Libraries/LibJS/Runtime/Object.cpp b/Libraries/LibJS/Runtime/Object.cpp index 82ffbc5829..ee1906f916 100644 --- a/Libraries/LibJS/Runtime/Object.cpp +++ b/Libraries/LibJS/Runtime/Object.cpp @@ -108,6 +108,69 @@ Value Object::get_own_property(const Object& this_object, const FlyString& prope return value_here; } +Value Object::get_enumerable_own_properties(const Object& this_object, GetOwnPropertyMode kind) const +{ + auto* properties_array = Array::create(interpreter().global_object()); + + // FIXME: Support generic iterables + if (this_object.is_string_object()) { + auto str = static_cast(this_object).primitive_string().string(); + + for (size_t i = 0; i < str.length(); ++i) { + if (kind == GetOwnPropertyMode::Key) { + properties_array->put_by_index(i, js_string(interpreter(), String::number(i))); + } else if (kind == GetOwnPropertyMode::Value) { + properties_array->put_by_index(i, js_string(interpreter(), String::format("%c", str[i]))); + } else { + auto* entry_array = Array::create(interpreter().global_object()); + entry_array->put_by_index(0, js_string(interpreter(), String::number(i))); + entry_array->put_by_index(1, js_string(interpreter(), String::format("%c", str[i]))); + properties_array->put_by_index(i, entry_array); + } + } + + return properties_array; + } + + size_t property_index = 0; + for (size_t i = 0; i < m_elements.size(); ++i) { + if (m_elements.at(i).is_empty()) + continue; + + if (kind == GetOwnPropertyMode::Key) { + properties_array->put_by_index(property_index, js_string(interpreter(), String::number(i))); + } else if (kind == GetOwnPropertyMode::Value) { + properties_array->put_by_index(property_index, m_elements.at(i)); + } else { + auto* entry_array = Array::create(interpreter().global_object()); + entry_array->put_by_index(0, js_string(interpreter(), String::number(i))); + entry_array->put_by_index(1, m_elements.at(i)); + properties_array->put_by_index(property_index, entry_array); + } + + ++property_index; + } + + for (auto& it : this_object.shape().property_table_ordered()) { + if (it.value.attributes & Attribute::Enumerable) { + size_t offset = it.value.offset + property_index; + + if (kind == GetOwnPropertyMode::Key) { + properties_array->put_by_index(offset, js_string(interpreter(), it.key)); + } else if (kind == GetOwnPropertyMode::Value) { + properties_array->put_by_index(offset, this_object.get(it.key)); + } else { + auto* entry_array = Array::create(interpreter().global_object()); + entry_array->put_by_index(0, js_string(interpreter(), it.key)); + entry_array->put_by_index(1, this_object.get(it.key)); + properties_array->put_by_index(offset, entry_array); + } + } + } + + return properties_array; +} + void Object::set_shape(Shape& new_shape) { m_storage.resize(new_shape.property_count()); diff --git a/Libraries/LibJS/Runtime/Object.h b/Libraries/LibJS/Runtime/Object.h index fa812c102e..5908d57e0a 100644 --- a/Libraries/LibJS/Runtime/Object.h +++ b/Libraries/LibJS/Runtime/Object.h @@ -61,6 +61,13 @@ public: Value get_own_property(const Object& this_object, const FlyString& property_name) const; + enum class GetOwnPropertyMode { + Key, + Value, + KeyAndValue, + }; + Value get_enumerable_own_properties(const Object& this_object, GetOwnPropertyMode) const; + enum class PutOwnPropertyMode { Put, DefineProperty, diff --git a/Libraries/LibJS/Runtime/ObjectConstructor.cpp b/Libraries/LibJS/Runtime/ObjectConstructor.cpp index 475bbd56f4..d566d66513 100644 --- a/Libraries/LibJS/Runtime/ObjectConstructor.cpp +++ b/Libraries/LibJS/Runtime/ObjectConstructor.cpp @@ -47,6 +47,9 @@ ObjectConstructor::ObjectConstructor() put_native_function("getOwnPropertyNames", get_own_property_names, 1, attr); put_native_function("getPrototypeOf", get_prototype_of, 1, attr); put_native_function("setPrototypeOf", set_prototype_of, 2, attr); + put_native_function("keys", keys, 1, attr); + put_native_function("values", values, 1, attr); + put_native_function("entries", entries, 1, attr); } ObjectConstructor::~ObjectConstructor() @@ -165,4 +168,40 @@ Value ObjectConstructor::is(Interpreter& interpreter) return typed_eq(interpreter, value1, value2); } +Value ObjectConstructor::keys(Interpreter& interpreter) +{ + if (!interpreter.argument_count()) + return interpreter.throw_exception("Can't convert undefined to object"); + + auto* obj_arg = interpreter.argument(0).to_object(interpreter.heap()); + if (interpreter.exception()) + return {}; + + return obj_arg->get_enumerable_own_properties(*obj_arg, GetOwnPropertyMode::Key); +} + +Value ObjectConstructor::values(Interpreter& interpreter) +{ + if (!interpreter.argument_count()) + return interpreter.throw_exception("Can't convert undefined to object"); + + auto* obj_arg = interpreter.argument(0).to_object(interpreter.heap()); + if (interpreter.exception()) + return {}; + + return obj_arg->get_enumerable_own_properties(*obj_arg, GetOwnPropertyMode::Value); +} + +Value ObjectConstructor::entries(Interpreter& interpreter) +{ + if (!interpreter.argument_count()) + return interpreter.throw_exception("Can't convert undefined to object"); + + auto* obj_arg = interpreter.argument(0).to_object(interpreter.heap()); + if (interpreter.exception()) + return {}; + + return obj_arg->get_enumerable_own_properties(*obj_arg, GetOwnPropertyMode::KeyAndValue); +} + } diff --git a/Libraries/LibJS/Runtime/ObjectConstructor.h b/Libraries/LibJS/Runtime/ObjectConstructor.h index 4fbfa6ed59..3d80eff929 100644 --- a/Libraries/LibJS/Runtime/ObjectConstructor.h +++ b/Libraries/LibJS/Runtime/ObjectConstructor.h @@ -48,6 +48,9 @@ private: static Value get_own_property_names(Interpreter&); static Value get_prototype_of(Interpreter&); static Value set_prototype_of(Interpreter&); + static Value keys(Interpreter&); + static Value values(Interpreter&); + static Value entries(Interpreter&); }; } diff --git a/Libraries/LibJS/Tests/Object.entries.js b/Libraries/LibJS/Tests/Object.entries.js new file mode 100644 index 0000000000..d99b24113e --- /dev/null +++ b/Libraries/LibJS/Tests/Object.entries.js @@ -0,0 +1,54 @@ +load("test-common.js"); + +try { + assert(Object.entries.length === 1); + assert(Object.entries(true).length === 0); + assert(Object.entries(45).length === 0); + assert(Object.entries(-998).length === 0); + assert(Object.entries("abcd").length === 4); + assert(Object.entries([1, 2, 3]).length === 3); + assert(Object.entries({ a: 1, b: 2, c: 3 }).length === 3); + + assertThrowsError(() => { + Object.entries(null); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + assertThrowsError(() => { + Object.entries(undefined); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + let entries = Object.entries({ foo: 1, bar: 2, baz: 3 }); + assert( + entries.length === 3 && entries[0].length === 2 && + entries[1].length === 2 && entries[2].length === 2 && + entries[0][0] === "foo" && entries[0][1] === 1 && + entries[1][0] === "bar" && entries[1][1] === 2 && + entries[2][0] === "baz" && entries[2][1] === 3 + ); + + entries = Object.entries(["a", "b", "c"]); + assert( + entries.length === 3 && entries[0].length === 2 && + entries[1].length === 2 && entries[2].length === 2 && + entries[0][0] === "0" && entries[0][1] === "a" && + entries[1][0] === "1" && entries[1][1] === "b" && + entries[2][0] === "2" && entries[2][1] === "c" + ); + + let obj = { foo: 1 }; + Object.defineProperty(obj, "getFoo", { + value: function() { return this.foo; }, + }); + let entries = Object.entries(obj); + assert(entries.length === 1 && entries[0][0] === "foo" && entries[0][1] === 1); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Object.keys.js b/Libraries/LibJS/Tests/Object.keys.js new file mode 100644 index 0000000000..3e0ccebc83 --- /dev/null +++ b/Libraries/LibJS/Tests/Object.keys.js @@ -0,0 +1,42 @@ +load("test-common.js"); + +try { + assert(Object.keys.length === 1); + assert(Object.keys(true).length === 0); + assert(Object.keys(45).length === 0); + assert(Object.keys(-998).length === 0); + assert(Object.keys("abcd").length === 4); + assert(Object.keys([1, 2, 3]).length === 3); + assert(Object.keys({ a: 1, b: 2, c: 3 }).length === 3); + + assertThrowsError(() => { + Object.keys(null); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + assertThrowsError(() => { + Object.keys(undefined); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + let keys = Object.keys({ foo: 1, bar: 2, baz: 3 }); + assert(keys[0] === "foo" && keys[1] === "bar" && keys[2] === "baz"); + + keys = Object.keys(["a", "b", "c"]); + assert(keys[0] === "0" && keys[1] === "1" && keys[2] === "2"); + + let obj = { foo: 1 }; + Object.defineProperty(obj, 'getFoo', { + value: function() { return this.foo; }, + }); + keys = Object.keys(obj); + assert(keys.length === 1 && keys[0] === 'foo'); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +} diff --git a/Libraries/LibJS/Tests/Object.values.js b/Libraries/LibJS/Tests/Object.values.js new file mode 100644 index 0000000000..35fb5a34aa --- /dev/null +++ b/Libraries/LibJS/Tests/Object.values.js @@ -0,0 +1,42 @@ +load("test-common.js"); + +try { + assert(Object.values.length === 1); + assert(Object.values(true).length === 0); + assert(Object.values(45).length === 0); + assert(Object.values(-998).length === 0); + assert(Object.values("abcd").length === 4); + assert(Object.values([1, 2, 3]).length === 3); + assert(Object.values({ a: 1, b: 2, c: 3 }).length === 3); + + assertThrowsError(() => { + Object.values(null); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + assertThrowsError(() => { + Object.values(undefined); + }, { + error: TypeError, + message: "ToObject on null or undefined.", + }); + + let values = Object.values({ foo: 1, bar: 2, baz: 3 }); + assert(values[0] === 1 && values[1] === 2 && values[2] === 3); + + values = Object.values(["a", "b", "c"]); + assert(values[0] === "a" && values[1] === "b" && values[2] === "c"); + + let obj = { foo: 1 }; + Object.defineProperty(obj, 'getFoo', { + value: function() { return this.foo; }, + }); + let values = Object.values(obj); + assert(values.length === 1 && values[0] === 1); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +}