From 4e6523b61b407f9e1a9104175952b148862d556c Mon Sep 17 00:00:00 2001 From: Cam Walter Date: Fri, 23 Aug 2024 00:51:10 -0600 Subject: [PATCH] Implement ES2024 `groupBy` --- .../AbstractEcmaObjectOperations.java | 71 +++++++++++++++++++ .../org/mozilla/javascript/NativeMap.java | 36 +++++++++- .../org/mozilla/javascript/NativeObject.java | 34 +++++++++ tests/testsrc/test262.properties | 30 +------- 4 files changed, 143 insertions(+), 28 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaObjectOperations.java b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaObjectOperations.java index 2dc3cfdb10..3e51ea04bb 100644 --- a/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaObjectOperations.java +++ b/rhino/src/main/java/org/mozilla/javascript/AbstractEcmaObjectOperations.java @@ -1,5 +1,10 @@ package org.mozilla.javascript; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + /** * Abstract Object Operations as defined by EcmaScript * @@ -24,6 +29,11 @@ enum INTEGRITY_LEVEL { SEALED } + enum KEY_COERCION { + PROPERTY, + COLLECTION, + } + /** * Implementation of Abstract Object operation HasOwnProperty as defined by EcmaScript * @@ -232,4 +242,65 @@ static void put(Context cx, Scriptable o, int p, Object v, boolean isThrow) { base.put(p, o, v); } } + + /** + * Implement the ECMAScript abstract operation "GroupBy" defined in section 7.3.35 of ECMA262. + * + * @param cx + * @param scope + * @param items + * @param callback + * @param keyCoercion + * @see + */ + static Map> groupBy( + Context cx, + Scriptable scope, + IdFunctionObject f, + Object items, + Object callback, + KEY_COERCION keyCoercion) { + if (cx.getLanguageVersion() >= Context.VERSION_ES6) { + ScriptRuntimeES6.requireObjectCoercible(cx, items, f); + } + if (!(callback instanceof Callable)) { + throw ScriptRuntime.typeErrorById( + "msg.isnt.function", callback, ScriptRuntime.typeof(callback)); + } + + // LinkedHashMap used to preserve key creation order + Map> groups = new LinkedHashMap<>(); + final Object iterator = ScriptRuntime.callIterator(items, cx, scope); + try (IteratorLikeIterable it = new IteratorLikeIterable(cx, scope, iterator)) { + double i = 0; + for (Object o : it) { + if (i > NativeNumber.MAX_SAFE_INTEGER) { + it.close(); + throw ScriptRuntime.typeError("Too many values to iterate"); + } + + Object[] args = {o, i}; + Object key = + ((Callable) callback).call(cx, scope, Undefined.SCRIPTABLE_UNDEFINED, args); + if (keyCoercion == KEY_COERCION.PROPERTY) { + if (!ScriptRuntime.isSymbol(key)) { + key = ScriptRuntime.toString(key); + } + } else { + assert keyCoercion == KEY_COERCION.COLLECTION; + if ((key instanceof Number) + && ((Number) key).doubleValue() == ScriptRuntime.negativeZero) { + key = ScriptRuntime.zeroObj; + } + } + + List group = groups.computeIfAbsent(key, (k) -> new ArrayList<>()); + group.add(o); + + i++; + } + } + + return groups; + } } diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeMap.java b/rhino/src/main/java/org/mozilla/javascript/NativeMap.java index a718a8a86c..512bf5519d 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeMap.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeMap.java @@ -6,6 +6,9 @@ package org.mozilla.javascript; +import java.util.List; +import java.util.Map; + public class NativeMap extends IdScriptableObject { private static final long serialVersionUID = 1171922614280016891L; private static final Object MAP_TAG = "Map"; @@ -37,6 +40,12 @@ public String getClassName() { return "Map"; } + @Override + public void fillConstructorProperties(IdFunctionObject ctor) { + addIdFunctionProperty(ctor, MAP_TAG, ConstructorId_groupBy, "groupBy", 2); + super.fillConstructorProperties(ctor); + } + @Override public Object execIdCall( IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { @@ -82,6 +91,30 @@ public Object execIdCall( args.length > 1 ? args[1] : Undefined.instance); case SymbolId_getSize: return realThis(thisObj, f).js_getSize(); + + case ConstructorId_groupBy: + { + Object items = args.length < 1 ? Undefined.instance : args[0]; + Object callback = args.length < 2 ? Undefined.instance : args[1]; + + Map> groups = + AbstractEcmaObjectOperations.groupBy( + cx, + scope, + f, + items, + callback, + AbstractEcmaObjectOperations.KEY_COERCION.COLLECTION); + + NativeMap map = (NativeMap) cx.newObject(scope, "Map"); + + for (Map.Entry> entry : groups.entrySet()) { + Scriptable elements = cx.newArray(scope, entry.getValue().toArray()); + map.entries.put(entry.getKey(), elements); + } + + return map; + } } throw new IllegalArgumentException("Map.prototype has no method: " + f.getFunctionName()); } @@ -315,7 +348,8 @@ protected int findPrototypeId(String s) { // Note that "SymbolId_iterator" is not present here. That's because the spec // requires that it be the same value as the "entries" prototype property. - private static final int Id_constructor = 1, + private static final int ConstructorId_groupBy = -1, + Id_constructor = 1, Id_set = 2, Id_get = 3, Id_delete = 4, diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeObject.java b/rhino/src/main/java/org/mozilla/javascript/NativeObject.java index c3aea11948..ce316bbe22 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeObject.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; @@ -85,6 +86,7 @@ protected void fillConstructorProperties(IdFunctionObject ctor) { addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_freeze, "freeze", 1); addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_assign, "assign", 2); addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_is, "is", 2); + addIdFunctionProperty(ctor, OBJECT_TAG, ConstructorId_groupBy, "groupBy", 2); super.fillConstructorProperties(ctor); } @@ -687,6 +689,37 @@ && isEnumerable(stringId, sourceObj)) { return ScriptRuntime.wrapBoolean(ScriptRuntime.same(a1, a2)); } + case ConstructorId_groupBy: + { + Object items = args.length < 1 ? Undefined.instance : args[0]; + Object callback = args.length < 2 ? Undefined.instance : args[1]; + + Map> groups = + AbstractEcmaObjectOperations.groupBy( + cx, + scope, + f, + items, + callback, + AbstractEcmaObjectOperations.KEY_COERCION.PROPERTY); + + NativeObject obj = (NativeObject) cx.newObject(scope); + obj.setPrototype(null); + + for (Map.Entry> entry : groups.entrySet()) { + Scriptable elements = cx.newArray(scope, entry.getValue().toArray()); + + ScriptableObject desc = (ScriptableObject) cx.newObject(scope); + desc.put("enumerable", desc, Boolean.TRUE); + desc.put("configurable", desc, Boolean.TRUE); + desc.put("value", desc, elements); + + obj.defineOwnProperty(cx, entry.getKey(), desc); + } + + return obj; + } + default: throw new IllegalArgumentException(String.valueOf(id)); } @@ -1021,6 +1054,7 @@ protected int findPrototypeId(String s) { ConstructorId_fromEntries = -20, ConstructorId_values = -21, ConstructorId_hasOwn = -22, + ConstructorId_groupBy = -23, Id_constructor = 1, Id_toString = 2, Id_toLocaleString = 3, diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index 9a66b55846..bda92a6188 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -991,19 +991,7 @@ built-ins/JSON 37/144 (25.69%) stringify/value-object-proxy-revoked.js {unsupported: [Proxy]} stringify/value-string-escape-unicode.js -built-ins/Map 25/171 (14.62%) - groupBy/callback-arg.js - groupBy/callback-throws.js - groupBy/emptyList.js - groupBy/evenOdd.js - groupBy/groupLength.js - groupBy/iterator-next-throws.js - groupBy/length.js - groupBy/map-instance.js - groupBy/name.js - groupBy/negativeZero.js - groupBy/string.js - groupBy/toPropertyKey.js +built-ins/Map 13/171 (7.6%) prototype/clear/not-a-constructor.js {unsupported: [Reflect.construct]} prototype/delete/not-a-constructor.js {unsupported: [Reflect.construct]} prototype/entries/not-a-constructor.js {unsupported: [Reflect.construct]} @@ -1110,7 +1098,7 @@ built-ins/Number 24/335 (7.16%) S9.3.1_A3_T1_U180E.js {unsupported: [u180e]} S9.3.1_A3_T2_U180E.js {unsupported: [u180e]} -built-ins/Object 230/3403 (6.76%) +built-ins/Object 218/3403 (6.41%) assign/assignment-to-readonly-property-of-target-must-throw-a-typeerror-exception.js assign/not-a-constructor.js {unsupported: [Reflect.construct]} assign/source-own-prop-desc-missing.js {unsupported: [Proxy]} @@ -1200,18 +1188,6 @@ built-ins/Object 230/3403 (6.76%) getOwnPropertySymbols/proxy-invariant-not-extensible-absent-string-key.js {unsupported: [Proxy]} getOwnPropertySymbols/proxy-invariant-not-extensible-extra-string-key.js {unsupported: [Proxy]} getPrototypeOf/not-a-constructor.js {unsupported: [Reflect.construct]} - groupBy/callback-arg.js - groupBy/callback-throws.js - groupBy/emptyList.js - groupBy/evenOdd.js - groupBy/groupLength.js - groupBy/invalid-property-key.js - groupBy/iterator-next-throws.js - groupBy/length.js - groupBy/name.js - groupBy/null-prototype.js - groupBy/string.js - groupBy/toPropertyKey.js hasOwn/length.js hasOwn/not-a-constructor.js {unsupported: [Reflect.construct]} hasOwn/symbol_property_toPrimitive.js @@ -5306,7 +5282,7 @@ language/expressions/new 41/59 (69.49%) ~language/expressions/new.target -language/expressions/object 867/1169 (74.17%) +language/expressions/object 866/1169 (74.08%) dstr/async-gen-meth-ary-init-iter-close.js {unsupported: [async-iteration, async]} dstr/async-gen-meth-ary-init-iter-get-err.js {unsupported: [async-iteration]} dstr/async-gen-meth-ary-init-iter-get-err-array-prototype.js {unsupported: [async-iteration]}