diff --git a/src/org/mozilla/javascript/NativeArray.java b/src/org/mozilla/javascript/NativeArray.java index b34a08be44..69cb0d0090 100644 --- a/src/org/mozilla/javascript/NativeArray.java +++ b/src/org/mozilla/javascript/NativeArray.java @@ -299,6 +299,10 @@ protected void initPrototypeId(int id) { arity = 0; s = "flat"; break; + case Id_flatMap: + arity = 1; + s = "flatMap"; + break; default: throw new IllegalArgumentException(String.valueOf(id)); } @@ -441,6 +445,9 @@ public Object execIdCall( case Id_flat: return js_flat(cx, scope, thisObj, args); + case Id_flatMap: + return js_flatMap(cx, scope, thisObj, args); + case Id_every: case Id_filter: case Id_forEach: @@ -1133,6 +1140,27 @@ private static String toStringHelper( return result.toString(); } + private static Function getCallbackArg(Context cx, Object callbackArg) { + if (!(callbackArg instanceof Function)) { + throw ScriptRuntime.notFunctionError(callbackArg); + } + if (cx.getLanguageVersion() >= Context.VERSION_ES6 + && (callbackArg instanceof NativeRegExp)) { + // Previously, it was allowed to pass RegExp instance as a callback (it implements + // Function) + // But according to ES2015 21.2.6 Properties of RegExp Instances: + // > RegExp instances are ordinary objects that inherit properties from the RegExp + // prototype object. + // > RegExp instances have internal slots [[RegExpMatcher]], [[OriginalSource]], and + // [[OriginalFlags]]. + // so, no [[Call]] for RegExp-s + throw ScriptRuntime.notFunctionError(callbackArg); + } + + Function f = (Function) callbackArg; + return f; + } + /** See ECMA 15.4.4.3 */ private static String js_join(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj); @@ -2038,6 +2066,47 @@ private static Scriptable flat(Context cx, Scriptable scope, Scriptable source, return result; } + private static Object js_flatMap( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj); + Object callbackArg = args.length > 0 ? args[0] : Undefined.instance; + + Function f = getCallbackArg(cx, callbackArg); + Scriptable parent = ScriptableObject.getTopLevelScope(f); + Scriptable thisArg; + if (args.length < 2 || args[1] == null || args[1] == Undefined.instance) { + thisArg = parent; + } else { + thisArg = ScriptRuntime.toObject(cx, scope, args[1]); + } + + long length = getLengthProperty(cx, o); + + Scriptable result; + result = cx.newArray(scope, 0); + long j = 0; + for (long i = 0; i < length; i++) { + Object elem = getRawElem(o, i); + if (elem == Scriptable.NOT_FOUND) { + continue; + } + Object[] innerArgs = new Object[] {elem, Long.valueOf(i), o}; + Object mapCall = f.call(cx, parent, thisArg, innerArgs); + if (js_isArray(mapCall)) { + Scriptable arr = (Scriptable) mapCall; + long arrLength = getLengthProperty(cx, arr); + for (long k = 0; k < arrLength; k++) { + Object temp = getRawElem(arr, k); + defineElemOrThrow(cx, result, j++, temp); + } + } else { + defineElemOrThrow(cx, result, j++, mapCall); + } + } + setLengthProperty(cx, result, j); + return result; + } + /** Implements the methods "every", "filter", "forEach", "map", and "some". */ private static Object iterativeMethod( Context cx, @@ -2063,23 +2132,8 @@ private static Object iterativeMethod( } Object callbackArg = args.length > 0 ? args[0] : Undefined.instance; - if (callbackArg == null || !(callbackArg instanceof Function)) { - throw ScriptRuntime.notFunctionError(callbackArg); - } - if (cx.getLanguageVersion() >= Context.VERSION_ES6 - && (callbackArg instanceof NativeRegExp)) { - // Previously, it was allowed to pass RegExp instance as a callback (it implements - // Function) - // But according to ES2015 21.2.6 Properties of RegExp Instances: - // > RegExp instances are ordinary objects that inherit properties from the RegExp - // prototype object. - // > RegExp instances have internal slots [[RegExpMatcher]], [[OriginalSource]], and - // [[OriginalFlags]]. - // so, no [[Call]] for RegExp-s - throw ScriptRuntime.notFunctionError(callbackArg); - } - Function f = (Function) callbackArg; + Function f = getCallbackArg(cx, callbackArg); Scriptable parent = ScriptableObject.getTopLevelScope(f); Scriptable thisArg; if (args.length < 2 || args[1] == null || args[1] == Undefined.instance) { @@ -2623,6 +2677,9 @@ protected int findPrototypeId(String s) { case "flat": id = Id_flat; break; + case "flatMap": + id = Id_flatMap; + break; default: id = 0; break; @@ -2663,7 +2720,8 @@ protected int findPrototypeId(String s) { Id_copyWithin = 31, Id_at = 32, Id_flat = 33, - SymbolId_iterator = 34, + Id_flatMap = 34, + SymbolId_iterator = 35, MAX_PROTOTYPE_ID = SymbolId_iterator; private static final int ConstructorId_join = -Id_join, ConstructorId_reverse = -Id_reverse, diff --git a/testsrc/jstests/es2019/array-flat-map.js b/testsrc/jstests/es2019/array-flat-map.js new file mode 100644 index 0000000000..97ef37798b --- /dev/null +++ b/testsrc/jstests/es2019/array-flat-map.js @@ -0,0 +1,171 @@ +// Copyright 2018 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Taken from https://github.com/v8/v8/blob/main/test/mjsunit/harmony/array-flat-map.js and changed due to Rhino errors +// TypeError: redeclaration of const input + +// Flags: --allow-natives-syntax +load("testsrc/assert.js"); + +let input; +let result; + +assertEquals(Array.prototype.flatMap.length, 1); +assertEquals(Array.prototype.flatMap.name, 'flatMap'); + +assertEquals( + [1, 2, 3, 4].flatMap((element) => [element, element ** 2]), + [1, 1, 2, 4, 3, 9, 4, 16] +); +assertEquals( + [1, 2, 3, 4].flatMap((element) => [[element, element ** 2]]), + [[1, 1], [2, 4], [3, 9], [4, 16]] +); + +{ + const elements = new Set([ + -Infinity, + -1, + -0, + +0, + +1, + Infinity, + null, + undefined, + true, + false, + '', + 'foo', + /./, + [], + {}, + Object.create(null), + //new Proxy({}, {}), + Symbol(), + x => x ** 2, + String + ]); + + for (let value of elements) { + assertEquals( + [value].flatMap((element) => [element, element]), + [value, value] + ); + } +} + +{ + const array = [42]; + assertEquals( + [array].flatMap((element) => [element, element]), + [array, array] + ); +} + +{ + const nonCallables = new Set([ + -Infinity, + -1, + -0, + +0, + +1, + Infinity, + null, + undefined, + true, + false, + '', + 'foo', + /./, + [], + {}, + Object.create(null), + //new Proxy({}, {}), + Symbol(), + ]); + for (let nonCallable of nonCallables) { + assertThrows(() => { + [].flatMap(nonCallable); + }, TypeError); + } +} + +{ + const object = { + foo: 42, + get length() { + object.foo = 0; + } + }; + result = [object].flatMap((element) => [element, element]); + //%HeapObjectVerify(result); + assertEquals(result, [object, object]); + assertEquals(result[0].foo, 42); +} + +assertThrows(() => { + Array.prototype.flatMap.call(null, (element) => element); +}, TypeError); +assertThrows(() => { + Array.prototype.flatMap.call(undefined, (element) => element); +}, TypeError); + +assertEquals( + Array.prototype.flatMap.call( + { + length: 1, + 0: 'a', + 1: 'b', + }, + (element) => element + ), + ['a'] +); +assertEquals( + Array.prototype.flatMap.call( + { + length: 2, + 0: 'a', + 1: 'b', + }, + (element) => element + ), + ['a', 'b'] +); + +{ + result = [1, 2, 3].flatMap(function() { + return [this]; + }, 'abc'); + assertEquals(true, result[0] == 'abc'); + assertEquals(true, result[1] == 'abc'); + assertEquals(true, result[2] == 'abc'); +} + +{ + input = { 0: 'a', 1: 'b', 2: 'c', length: 'wat' }; + assertEquals(Array.prototype.flatMap.call(input, x => [x]), []); +} + +{ + let count = 0; + input = { + get length() { ++count; return 0; } + }; + result = Array.prototype.flatMap.call(input, x => [x]); + assertEquals(count, 1); +} + +{ + const descriptor = Object.getOwnPropertyDescriptor( + Array.prototype, + 'flatMap' + ); + assertEquals(descriptor.value, Array.prototype.flatMap); + assertEquals(descriptor.writable, true); + assertEquals(descriptor.enumerable, false); + assertEquals(descriptor.configurable, true); +} + +"success"; diff --git a/testsrc/org/mozilla/javascript/tests/Test262SuiteTest.java b/testsrc/org/mozilla/javascript/tests/Test262SuiteTest.java index 82670fedab..db31713b23 100644 --- a/testsrc/org/mozilla/javascript/tests/Test262SuiteTest.java +++ b/testsrc/org/mozilla/javascript/tests/Test262SuiteTest.java @@ -88,7 +88,6 @@ public class Test262SuiteTest { static final Set UNSUPPORTED_FEATURES = new HashSet<>( Arrays.asList( - "Array.prototype.flatMap", "Atomics", "IsHTMLDDA", "Proxy", diff --git a/testsrc/org/mozilla/javascript/tests/es2019/ArrayFlatMapTest.java b/testsrc/org/mozilla/javascript/tests/es2019/ArrayFlatMapTest.java new file mode 100644 index 0000000000..5c59fcbda0 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/es2019/ArrayFlatMapTest.java @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript.tests.es2019; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.drivers.LanguageVersion; +import org.mozilla.javascript.drivers.RhinoTest; +import org.mozilla.javascript.drivers.ScriptTestsBase; + +@RhinoTest("testsrc/jstests/es2019/array-flat-map.js") +@LanguageVersion(Context.VERSION_ES6) +public class ArrayFlatMapTest extends ScriptTestsBase {} diff --git a/testsrc/test262.properties b/testsrc/test262.properties index bafd3a036e..93673bf95f 100644 --- a/testsrc/test262.properties +++ b/testsrc/test262.properties @@ -1,6 +1,6 @@ # This is a configuration file for Test262SuiteTest.java. See ./README.md for more info about this file -built-ins/Array 188/2670 (7.04%) +built-ins/Array 179/2670 (6.7%) from/calling-from-valid-1-noStrict.js non-strict Spec pretty clearly says this should be undefined from/elements-deleted-after.js Checking to see if length changed, but spec says it should not from/iter-map-fn-this-non-strict.js non-strict Error propagation needs work in general @@ -72,7 +72,18 @@ built-ins/Array 188/2670 (7.04%) prototype/filter/target-array-with-non-writable-property.js {unsupported: [Symbol.species]} prototype/findIndex/predicate-call-this-strict.js strict prototype/find/predicate-call-this-strict.js strict - prototype/flatMap 21/21 (100.0%) + prototype/flatMap/array-like-objects.js + prototype/flatMap/array-like-objects-poisoned-length.js + prototype/flatMap/proxy-access-count.js + prototype/flatMap/target-array-non-extensible.js {unsupported: [Symbol.species]} + prototype/flatMap/target-array-with-non-configurable-property.js {unsupported: [Symbol.species]} + prototype/flatMap/target-array-with-non-writable-property.js {unsupported: [Symbol.species]} + prototype/flatMap/this-value-ctor-non-object.js + prototype/flatMap/this-value-ctor-object-species.js {unsupported: [Symbol.species]} + prototype/flatMap/this-value-ctor-object-species-bad-throws.js {unsupported: [Symbol.species]} + prototype/flatMap/this-value-ctor-object-species-custom-ctor.js {unsupported: [Symbol.species]} + prototype/flatMap/this-value-ctor-object-species-custom-ctor-poisoned-throws.js {unsupported: [Symbol.species]} + prototype/flatMap/thisArg-argument.js strict prototype/flat/non-object-ctor-throws.js prototype/flat/proxy-access-count.js prototype/flat/target-array-non-extensible.js {unsupported: [Symbol.species]} @@ -159,7 +170,7 @@ built-ins/Array 188/2670 (7.04%) prototype/toLocaleString/primitive_this_value.js strict prototype/toLocaleString/primitive_this_value_getter.js strict prototype/unshift/throws-with-string-receiver.js - prototype/methods-called-as-functions.js {unsupported: [Symbol.species, Array.prototype.flatMap]} + prototype/methods-called-as-functions.js {unsupported: [Symbol.species]} prototype/Symbol.iterator.js Expects a particular string value Symbol.species 4/4 (100.0%) proto-from-ctor-realm-one.js {unsupported: [Reflect]}