From 4152409ac5db46f45ab28455d6217f49bfc49207 Mon Sep 17 00:00:00 2001 From: davidot Date: Sun, 13 Jun 2021 15:13:43 +0200 Subject: [PATCH] LibJS: Make Array.prototype.concat generic --- .../LibJS/Runtime/ArrayPrototype.cpp | 80 ++++++++++++++++--- .../Array.prototype-generic-functions.js | 39 +++++++++ 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp index 8ebe14655b..02c2c0c1c3 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp @@ -435,27 +435,87 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::join) // 23.1.3.1 Array.prototype.concat ( ...items ), https://tc39.es/ecma262/#sec-array.prototype.concat JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::concat) { - auto* array = Array::typed_this(vm, global_object); - if (!array) + auto* this_object = vm.this_value(global_object).to_object(global_object); + if (!this_object) return {}; - auto* new_array = Array::create(global_object); - new_array->indexed_properties().append_all(array, array->indexed_properties()); + // FIXME: Use ArraySpeciesCreate. + auto new_array = Array::create(global_object); + + size_t n = 0; + + // 23.1.3.1.1 IsConcatSpreadable ( O ), https://tc39.es/ecma262/#sec-isconcatspreadable + auto is_concat_spreadable = [&vm, &global_object](Value const& val) { + if (!val.is_object()) { + return false; + } + auto* object = val.to_object(global_object); + if (vm.exception()) + return false; + + auto spreadable = object->get(vm.well_known_symbol_is_concat_spreadable()).value_or(js_undefined()); + if (vm.exception()) + return false; + + if (!spreadable.is_undefined()) + return spreadable.to_boolean(); + + return object->is_array(); + }; + + auto append_to_new_array = [&vm, &is_concat_spreadable, &new_array, &global_object, &n](Value arg) { + auto spreadable = is_concat_spreadable(arg); + if (vm.exception()) + return; + if (spreadable) { + VERIFY(arg.is_object()); + Object& obj = arg.as_object(); + size_t k = 0; + auto length = length_of_array_like(global_object, obj); + if (vm.exception()) + return; + + if (length + k > MAX_ARRAY_LIKE_INDEX) { + vm.throw_exception(global_object, ErrorType::ArrayMaxSize); + return; + } + while (k < length) { + auto k_exists = obj.has_property(k); + if (vm.exception()) + return; + if (k_exists) { + auto k_value = obj.get(k).value_or(js_undefined()); + if (vm.exception()) + return; + new_array->put(n, k_value); + } + ++n; + ++k; + } + } else { + if (n >= MAX_ARRAY_LIKE_INDEX) { + vm.throw_exception(global_object, ErrorType::ArrayMaxSize); + return; + } + new_array->put(n, arg); + ++n; + } + }; + + append_to_new_array(this_object); if (vm.exception()) return {}; for (size_t i = 0; i < vm.argument_count(); ++i) { auto argument = vm.argument(i); - if (argument.is_array(global_object)) { - auto& argument_object = argument.as_object(); - new_array->indexed_properties().append_all(&argument_object, argument_object.indexed_properties()); - continue; - } + append_to_new_array(argument); if (vm.exception()) return {}; - new_array->indexed_properties().append(argument); } + new_array->put(vm.names.length, Value(n)); + if (vm.exception()) + return {}; return Value(new_array); } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype-generic-functions.js b/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype-generic-functions.js index e7683289ee..af1b189834 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype-generic-functions.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype-generic-functions.js @@ -203,4 +203,43 @@ describe("ability to work with generic non-array objects", () => { 0: "foo", }); }); + + test("concat", () => { + expect(Array.prototype.concat.call(o)).toEqual([o]); + expect(Array.prototype.concat.call(o, true)).toEqual([o, true]); + expect(Array.prototype.concat.call({}, o)).toEqual([{}, o]); + + const spreadable = { + length: 5, + 0: "foo", + 1: "bar", + 3: "baz", + [Symbol.isConcatSpreadable]: true, + }; + expect(Array.prototype.concat.call(spreadable)).toEqual([ + "foo", + "bar", + undefined, + "baz", + undefined, + ]); + expect(Array.prototype.concat.call(spreadable, [1, 2])).toEqual([ + "foo", + "bar", + undefined, + "baz", + undefined, + 1, + 2, + ]); + expect(Array.prototype.concat.call([1], spreadable, [2])).toEqual([ + 1, + "foo", + "bar", + undefined, + "baz", + undefined, + 2, + ]); + }); });