diff --git a/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java b/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java index aca8cb710b..b21484dda3 100644 --- a/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java +++ b/rhino/src/main/java/org/mozilla/javascript/IdScriptableObject.java @@ -820,18 +820,22 @@ protected void addIdFunctionProperty( * @return obj casted to the target type * @throws EcmaError if the cast failed. */ - @SuppressWarnings("unchecked") protected static T ensureType(Object obj, Class clazz, IdFunctionObject f) { + return ensureType(obj, clazz, f.getFunctionName()); + } + + @SuppressWarnings("unchecked") + protected static T ensureType(Object obj, Class clazz, String functionName) { if (clazz.isInstance(obj)) { return (T) obj; } if (obj == null) { throw ScriptRuntime.typeErrorById( - "msg.incompat.call.details", f.getFunctionName(), "null", clazz.getName()); + "msg.incompat.call.details", functionName, "null", clazz.getName()); } throw ScriptRuntime.typeErrorById( "msg.incompat.call.details", - f.getFunctionName(), + functionName, obj.getClass().getName(), clazz.getName()); } diff --git a/rhino/src/main/java/org/mozilla/javascript/LazilyLoadedCtor.java b/rhino/src/main/java/org/mozilla/javascript/LazilyLoadedCtor.java index b069bcfafc..602e953b32 100644 --- a/rhino/src/main/java/org/mozilla/javascript/LazilyLoadedCtor.java +++ b/rhino/src/main/java/org/mozilla/javascript/LazilyLoadedCtor.java @@ -35,7 +35,7 @@ public LazilyLoadedCtor( this(scope, propertyName, className, sealed, false); } - LazilyLoadedCtor( + public LazilyLoadedCtor( ScriptableObject scope, String propertyName, String className, diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeString.java b/rhino/src/main/java/org/mozilla/javascript/NativeString.java index 03efcc0d9d..f2b128f07f 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeString.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeString.java @@ -240,6 +240,10 @@ protected void initPrototypeId(int id) { arity = 1; s = "match"; break; + case Id_matchAll: + arity = 1; + s = "matchAll"; + break; case Id_search: arity = 1; s = "search"; @@ -859,6 +863,60 @@ else if (Normalizer.Form.NFC.name().equals(formStr)) case SymbolId_iterator: return new NativeStringIterator(scope, requireObjectCoercible(cx, thisObj, f)); + + case Id_matchAll: + { + // See ECMAScript spec 22.1.3.14 + Object o = requireObjectCoercible(cx, thisObj, f); + Object regexp = args.length > 0 ? args[0] : Undefined.instance; + RegExpProxy regExpProxy = ScriptRuntime.checkRegExpProxy(cx); + if (regexp != null && !Undefined.isUndefined(regexp)) { + boolean isRegExp = + regexp instanceof Scriptable + && regExpProxy.isRegExp((Scriptable) regexp); + if (isRegExp) { + Object flags = + ScriptRuntime.getObjectProp(regexp, "flags", cx, scope); + requireObjectCoercible(cx, flags, f); + String flagsStr = ScriptRuntime.toString(flags); + if (!flagsStr.contains("g")) { + throw ScriptRuntime.typeErrorById( + "msg.str.match.all.no.global.flag"); + } + } + + Object matcher = + ScriptRuntime.getObjectElem( + regexp, SymbolKey.MATCH_ALL, cx, scope); + // If method is not undefined, it should be a Callable + if (matcher != null && !Undefined.isUndefined(matcher)) { + if (!(matcher instanceof Callable)) { + throw ScriptRuntime.notFunctionError( + regexp, matcher, SymbolKey.MATCH_ALL.getName()); + } + return ((Callable) matcher) + .call( + cx, + scope, + ScriptRuntime.toObject(scope, regexp), + new Object[] {o}); + } + } + + String s = ScriptRuntime.toString(o); + String regexpToString = + Undefined.isUndefined(regexp) ? "" : ScriptRuntime.toString(regexp); + Object compiledRegExp = regExpProxy.compileRegExp(cx, regexpToString, "g"); + Scriptable rx = regExpProxy.wrapRegExp(cx, scope, compiledRegExp); + + Object method = + ScriptRuntime.getObjectElem(rx, SymbolKey.MATCH_ALL, cx, scope); + if (!(method instanceof Callable)) { + throw ScriptRuntime.notFunctionError( + rx, method, SymbolKey.MATCH_ALL.getName()); + } + return ((Callable) method).call(cx, scope, rx, new Object[] {s}); + } } throw new IllegalArgumentException( "String.prototype has no method: " + f.getFunctionName()); @@ -1382,6 +1440,9 @@ protected int findPrototypeId(String s) { case "match": id = Id_match; break; + case "matchAll": + id = Id_matchAll; + break; case "search": id = Id_search; break; @@ -1512,7 +1573,8 @@ protected int findPrototypeId(String s) { Id_at = 52, Id_isWellFormed = 53, Id_toWellFormed = 54, - MAX_PROTOTYPE_ID = Id_toWellFormed; + Id_matchAll = 55, + MAX_PROTOTYPE_ID = Id_matchAll; private static final int ConstructorId_charAt = -Id_charAt, ConstructorId_charCodeAt = -Id_charCodeAt, ConstructorId_indexOf = -Id_indexOf, diff --git a/rhino/src/main/java/org/mozilla/javascript/NativeSymbol.java b/rhino/src/main/java/org/mozilla/javascript/NativeSymbol.java index b47b814f23..cb23c5898b 100644 --- a/rhino/src/main/java/org/mozilla/javascript/NativeSymbol.java +++ b/rhino/src/main/java/org/mozilla/javascript/NativeSymbol.java @@ -72,6 +72,7 @@ public static void init(Context cx, Scriptable scope, boolean sealed) { createStandardSymbol(cx, scope, ctor, "isRegExp", SymbolKey.IS_REGEXP); createStandardSymbol(cx, scope, ctor, "toPrimitive", SymbolKey.TO_PRIMITIVE); createStandardSymbol(cx, scope, ctor, "match", SymbolKey.MATCH); + createStandardSymbol(cx, scope, ctor, "matchAll", SymbolKey.MATCH_ALL); createStandardSymbol(cx, scope, ctor, "replace", SymbolKey.REPLACE); createStandardSymbol(cx, scope, ctor, "search", SymbolKey.SEARCH); createStandardSymbol(cx, scope, ctor, "split", SymbolKey.SPLIT); diff --git a/rhino/src/main/java/org/mozilla/javascript/RegExpProxy.java b/rhino/src/main/java/org/mozilla/javascript/RegExpProxy.java index 092a93f136..c9abe50779 100644 --- a/rhino/src/main/java/org/mozilla/javascript/RegExpProxy.java +++ b/rhino/src/main/java/org/mozilla/javascript/RegExpProxy.java @@ -18,6 +18,8 @@ public interface RegExpProxy { public static final int RA_REPLACE_ALL = 3; public static final int RA_SEARCH = 4; + public void register(ScriptableObject scope, boolean sealed); + public boolean isRegExp(Scriptable obj); public Object compileRegExp(Context cx, String source, String flags); diff --git a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java index 1c3ac25ab7..9edefaf67d 100644 --- a/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java +++ b/rhino/src/main/java/org/mozilla/javascript/ScriptRuntime.java @@ -188,6 +188,7 @@ public static ScriptableObject initSafeStandardObjects( NativeArrayIterator.init(scope, sealed); NativeStringIterator.init(scope, sealed); + registerRegExp(cx, scope, sealed); NativeJavaObject.init(scope, sealed); NativeJavaMap.init(scope, sealed); @@ -196,8 +197,6 @@ public static ScriptableObject initSafeStandardObjects( cx.hasFeature(Context.FEATURE_E4X) && cx.getE4xImplementationFactory() != null; // define lazy-loaded properties using their class name - new LazilyLoadedCtor( - scope, "RegExp", "org.mozilla.javascript.regexp.NativeRegExp", sealed, true); new LazilyLoadedCtor( scope, "Continuation", "org.mozilla.javascript.NativeContinuation", sealed, true); @@ -299,6 +298,13 @@ public static ScriptableObject initSafeStandardObjects( return scope; } + private static void registerRegExp(Context cx, ScriptableObject scope, boolean sealed) { + RegExpProxy regExpProxy = getRegExpProxy(cx); + if (regExpProxy != null) { + regExpProxy.register(scope, sealed); + } + } + public static ScriptableObject initStandardObjects( Context cx, ScriptableObject scope, boolean sealed) { ScriptableObject s = initSafeStandardObjects(cx, scope, sealed); @@ -1388,6 +1394,14 @@ public static long toLength(Object[] args, int index) { return (long) Math.min(len, NativeNumber.MAX_SAFE_INTEGER); } + public static long toLength(Object value) { + double len = toInteger(value); + if (len <= 0.0) { + return 0; + } + return (long) Math.min(len, NativeNumber.MAX_SAFE_INTEGER); + } + /** See ECMA 9.5. */ public static int toInt32(Object val) { // short circuit for common integer values @@ -1444,6 +1458,20 @@ public static Optional canonicalNumericIndexString(String arg) { return Optional.empty(); } + /** Implements the abstract operation AdvanceStringIndex. See ECMAScript spec 22.2.7.3 */ + public static long advanceStringIndex(String string, long index, boolean unicode) { + if (index >= NativeNumber.MAX_SAFE_INTEGER) Kit.codeBug(); + if (!unicode) { + return index + 1; + } + int length = string.length(); + if (index + 1 > length) { + return index + 1; + } + int cp = string.codePointAt((int) index); + return index + Character.charCount(cp); + } + // XXX: this is until setDefaultNamespace will learn how to store NS // properly and separates namespace form Scriptable.get etc. private static final String DEFAULT_NS_TAG = "__default_namespace__"; diff --git a/rhino/src/main/java/org/mozilla/javascript/SymbolKey.java b/rhino/src/main/java/org/mozilla/javascript/SymbolKey.java index a922b760cf..5ea84ded01 100644 --- a/rhino/src/main/java/org/mozilla/javascript/SymbolKey.java +++ b/rhino/src/main/java/org/mozilla/javascript/SymbolKey.java @@ -20,6 +20,7 @@ public class SymbolKey implements Symbol, Serializable { public static final SymbolKey IS_REGEXP = new SymbolKey("Symbol.isRegExp"); public static final SymbolKey TO_PRIMITIVE = new SymbolKey("Symbol.toPrimitive"); public static final SymbolKey MATCH = new SymbolKey("Symbol.match"); + public static final SymbolKey MATCH_ALL = new SymbolKey("Symbol.matchAll"); public static final SymbolKey REPLACE = new SymbolKey("Symbol.replace"); public static final SymbolKey SEARCH = new SymbolKey("Symbol.search"); public static final SymbolKey SPLIT = new SymbolKey("Symbol.split"); diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index 40d17f282f..8d87607173 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -7,7 +7,10 @@ package org.mozilla.javascript.regexp; import java.io.Serializable; +import org.mozilla.javascript.AbstractEcmaObjectOperations; +import org.mozilla.javascript.Constructable; import org.mozilla.javascript.Context; +import org.mozilla.javascript.Function; import org.mozilla.javascript.IdFunctionObject; import org.mozilla.javascript.IdScriptableObject; import org.mozilla.javascript.Kit; @@ -2695,6 +2698,10 @@ protected void initPrototypeId(int id) { initPrototypeMethod(REGEXP_TAG, id, SymbolKey.MATCH, "[Symbol.match]", 1); return; } + if (id == SymbolId_matchAll) { + initPrototypeMethod(REGEXP_TAG, id, SymbolKey.MATCH_ALL, "[Symbol.matchAll]", 1); + return; + } if (id == SymbolId_search) { initPrototypeMethod(REGEXP_TAG, id, SymbolKey.SEARCH, "[Symbol.search]", 1); return; @@ -2761,7 +2768,7 @@ public Object execIdCall( return realThis(thisObj, f).toString(); case Id_exec: - return realThis(thisObj, f).execSub(cx, scope, args, MATCH); + return js_exec(cx, scope, thisObj, args); case Id_test: { @@ -2775,6 +2782,9 @@ public Object execIdCall( case SymbolId_match: return realThis(thisObj, f).execSub(cx, scope, args, MATCH); + case SymbolId_matchAll: + return js_SymbolMatchAll(cx, scope, thisObj, args); + case SymbolId_search: Scriptable scriptable = (Scriptable) realThis(thisObj, f).execSub(cx, scope, args, MATCH); @@ -2783,8 +2793,44 @@ public Object execIdCall( throw new IllegalArgumentException(String.valueOf(id)); } + static Object js_exec(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return realThis(thisObj, "exec").execSub(cx, scope, args, MATCH); + } + + private Object js_SymbolMatchAll( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + // See ECMAScript spec 22.2.6.9 + if (!ScriptRuntime.isObject(thisObj)) { + throw ScriptRuntime.typeErrorById("msg.arg.not.object", ScriptRuntime.typeof(thisObj)); + } + + String s = ScriptRuntime.toString(args.length > 0 ? args[0] : Undefined.instance); + + Scriptable topLevelScope = ScriptableObject.getTopLevelScope(scope); + Function defaultConstructor = + ScriptRuntime.getExistingCtor(cx, topLevelScope, getClassName()); + Constructable c = + AbstractEcmaObjectOperations.speciesConstructor(cx, thisObj, defaultConstructor); + + String flags = ScriptRuntime.toString(ScriptRuntime.getObjectProp(thisObj, "flags", cx)); + + Scriptable matcher = c.construct(cx, scope, new Object[] {thisObj, flags}); + + long lastIndex = + ScriptRuntime.toLength(ScriptRuntime.getObjectProp(thisObj, "lastIndex", cx)); + ScriptRuntime.setObjectProp(matcher, "lastIndex", lastIndex, cx); + boolean global = flags.indexOf('g') != -1; + boolean fullUnicode = flags.indexOf('u') != -1 || flags.indexOf('v') != -1; + + return new NativeRegExpStringIterator(scope, matcher, s, global, fullUnicode); + } + private static NativeRegExp realThis(Scriptable thisObj, IdFunctionObject f) { - return ensureType(thisObj, NativeRegExp.class, f); + return realThis(thisObj, f.getFunctionName()); + } + + private static NativeRegExp realThis(Scriptable thisObj, String functionName) { + return ensureType(thisObj, NativeRegExp.class, functionName); } @Override @@ -2792,6 +2838,9 @@ protected int findPrototypeId(Symbol k) { if (SymbolKey.MATCH.equals(k)) { return SymbolId_match; } + if (SymbolKey.MATCH_ALL.equals(k)) { + return SymbolId_matchAll; + } if (SymbolKey.SEARCH.equals(k)) { return SymbolId_search; } @@ -2834,7 +2883,8 @@ protected int findPrototypeId(String s) { Id_test = 5, Id_prefix = 6, SymbolId_match = 7, - SymbolId_search = 8, + SymbolId_matchAll = 8, + SymbolId_search = 9, MAX_PROTOTYPE_ID = SymbolId_search; private RECompiled re; diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExpStringIterator.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExpStringIterator.java new file mode 100644 index 0000000000..1974040952 --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExpStringIterator.java @@ -0,0 +1,111 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * 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.regexp; + +import org.mozilla.javascript.Callable; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ES6Iterator; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Undefined; + +// See ECMAScript spec 22.2.9.1 +public final class NativeRegExpStringIterator extends ES6Iterator { + private static final long serialVersionUID = 1L; + private static final String ITERATOR_TAG = "RegExpStringIterator"; + + private Scriptable regexp; + private String string; + private boolean global; + private boolean fullUnicode; + private boolean nextDone; + private Object next = null; + + public static void init(ScriptableObject scope, boolean sealed) { + ES6Iterator.init(scope, sealed, new NativeRegExpStringIterator(), ITERATOR_TAG); + } + + /** Only for constructing the prototype object. */ + private NativeRegExpStringIterator() { + super(); + } + + public NativeRegExpStringIterator( + Scriptable scope, + Scriptable regexp, + String string, + boolean global, + boolean fullUnicode) { + super(scope, ITERATOR_TAG); + + this.regexp = regexp; + this.string = string; + this.global = global; + this.fullUnicode = fullUnicode; + this.nextDone = false; + } + + @Override + public String getClassName() { + return "RegExp String Iterator"; + } + + @Override + protected boolean isDone(Context cx, Scriptable scope) { + // The base class calls _first_ isDone and _then_ nextValue, so we'll just compute the next + // value here and return it form "nextValue". + // Also, for non-global regexp, we need to return the first match and then "done" on the + // next iteration. + + if (nextDone) { + return true; + } + + next = regExpExec(cx, scope); + if (next == null) { + // Done! Point ii of the spec + next = Undefined.instance; + nextDone = true; + return true; + } else if (!global) { + // Return false at this iteration, but true at the next. Point iii of the spec + nextDone = true; + return false; + } + + // Increment index if matched empty string, as per the spec, point v. + String matchStr = ScriptRuntime.toString(ScriptRuntime.getObjectIndex(next, 0, cx, scope)); + if (matchStr.isEmpty()) { + long thisIndex = + ScriptRuntime.toLength(ScriptRuntime.getObjectProp(regexp, "lastIndex", cx)); + long nextIndex = ScriptRuntime.advanceStringIndex(string, thisIndex, fullUnicode); + ScriptRuntime.setObjectProp(regexp, "lastIndex", nextIndex, cx); + } + + return false; + } + + @Override + protected Object nextValue(Context cx, Scriptable scope) { + return next; + } + + private Object regExpExec(Context cx, Scriptable scope) { + // See ECMAScript spec 22.2.7.1 + Object execMethod = ScriptRuntime.getObjectProp(regexp, "exec", cx); + if (execMethod instanceof Callable) { + return ((Callable) execMethod).call(cx, scope, regexp, new Object[] {string}); + } + return NativeRegExp.js_exec(cx, scope, regexp, new Object[] {string}); + } + + @Override + protected String getTag() { + return ITERATOR_TAG; + } +} diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/RegExpImpl.java b/rhino/src/main/java/org/mozilla/javascript/regexp/RegExpImpl.java index f0910fd9ff..ef6ab3560f 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/RegExpImpl.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/RegExpImpl.java @@ -9,6 +9,7 @@ import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.Kit; +import org.mozilla.javascript.LazilyLoadedCtor; import org.mozilla.javascript.RegExpProxy; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; @@ -18,6 +19,13 @@ /** */ public class RegExpImpl implements RegExpProxy { + @Override + public void register(ScriptableObject scope, boolean sealed) { + NativeRegExpStringIterator.init(scope, sealed); + new LazilyLoadedCtor( + scope, "RegExp", "org.mozilla.javascript.regexp.NativeRegExp", sealed, true); + } + @Override public boolean isRegExp(Scriptable obj) { return obj instanceof NativeRegExp; diff --git a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties index b121f7e670..07d3d50c9c 100644 --- a/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties +++ b/rhino/src/main/resources/org/mozilla/javascript/resources/Messages.properties @@ -253,6 +253,9 @@ msg.bad.regexp.compile =\ msg.arg.not.object =\ Expected argument of type object, but instead had type {0} +msg.str.match.all.no.global.flag =\ + String.prototype.matchAll called with a non-global RegExp argument + # NativeDate msg.invalid.date =\ Date is invalid. diff --git a/rhino/src/test/java/org/mozilla/javascript/ScriptRuntimeAdvanceStringIndexTest.java b/rhino/src/test/java/org/mozilla/javascript/ScriptRuntimeAdvanceStringIndexTest.java new file mode 100644 index 0000000000..1773af6c90 --- /dev/null +++ b/rhino/src/test/java/org/mozilla/javascript/ScriptRuntimeAdvanceStringIndexTest.java @@ -0,0 +1,27 @@ +package org.mozilla.javascript; + +import static org.junit.Assert.*; + +import org.junit.jupiter.api.Test; + +public class ScriptRuntimeAdvanceStringIndexTest { + @Test + void nonUnicode() { + assertEquals(2, ScriptRuntime.advanceStringIndex("abc", 1, false)); + } + + @Test + void unicodeNormalCodePoint() { + assertEquals(1, ScriptRuntime.advanceStringIndex("abc", 0, true)); + } + + @Test + void unicodeCodePointSurrogatePair() { + assertEquals(2, ScriptRuntime.advanceStringIndex("\uD81B\uDF777a", 0, true)); + } + + @Test + void unicodeAfterEnd() { + assertEquals(4, ScriptRuntime.advanceStringIndex("abc", 3, true)); + } +} diff --git a/tests/src/test/java/org/mozilla/javascript/tests/Test262SuiteTest.java b/tests/src/test/java/org/mozilla/javascript/tests/Test262SuiteTest.java index 9f7466ef8c..8273bc1926 100644 --- a/tests/src/test/java/org/mozilla/javascript/tests/Test262SuiteTest.java +++ b/tests/src/test/java/org/mozilla/javascript/tests/Test262SuiteTest.java @@ -112,8 +112,6 @@ public class Test262SuiteTest { "regexp-unicode-property-escapes", "resizable-arraybuffer", "super", - "String.prototype.matchAll", - "Symbol.matchAll", "tail-call-optimization", "u180e")); diff --git a/tests/src/test/java/org/mozilla/javascript/tests/es2020/StringMatchAllTest.java b/tests/src/test/java/org/mozilla/javascript/tests/es2020/StringMatchAllTest.java new file mode 100644 index 0000000000..9231c3fa21 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/es2020/StringMatchAllTest.java @@ -0,0 +1,10 @@ +package org.mozilla.javascript.tests.es2020; + +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/es2020/string-match-all.js") +@LanguageVersion(Context.VERSION_ES6) +public class StringMatchAllTest extends ScriptTestsBase {} diff --git a/tests/testsrc/jstests/es2020/string-match-all.js b/tests/testsrc/jstests/es2020/string-match-all.js new file mode 100644 index 0000000000..3f3246f1bb --- /dev/null +++ b/tests/testsrc/jstests/es2020/string-match-all.js @@ -0,0 +1,145 @@ +// 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/. + +load("testsrc/assert.js"); + +function assertIteratorIsDone(iterator) { + const next = iterator.next(); + assertTrue(next.done); + assertEquals(undefined, next.value); +} + +(function happyPath() { + const s = "aabbc"; + const re = /([a-z])\1/g; + const matches = s.matchAll(re); + + var match = matches.next(); + assertFalse(match.done); + assertEquals("aa", match.value[0]); + assertEquals(0, match.value.index); + assertEquals(s, match.value.input); + + match = matches.next(); + assertFalse(match.done); + assertEquals("bb", match.value[0]); + assertEquals(2, match.value.index); + assertEquals(s, match.value.input); + + assertIteratorIsDone(matches); +})(); + +(function matchingEmptyStringDoesNotResultInInfiniteLoop() { + const s = "abc"; + const re = /(?=[a-b])/g; + const matches = s.matchAll(re); + + var match = matches.next(); + assertFalse(match.done); + assertEquals("", match.value[0]); + assertEquals(0, match.value.index); + assertEquals(s, match.value.input); + + match = matches.next(); + assertFalse(match.done); + assertEquals("", match.value[0]); + assertEquals(1, match.value.index); + assertEquals(s, match.value.input); + + assertIteratorIsDone(matches); +})(); + +(function stringMatchAllCalledWithNonGlobalRegExp() { + const s = "aabbc"; + const re = /([a-z])\1/; + try { + s.matchAll(re); + throw new Error('Expected a TypeError because regex is not global'); + } catch (err) { + assertEquals('TypeError', err.name); + assertEquals('String.prototype.matchAll called with a non-global RegExp argument', err.message); + } +})(); + +(function regexpSymbolMatchAllGlobal() { + const s = "aabbc"; + const re = /([a-z])\1/g; + const matches = re[Symbol.matchAll](s); + + var match = matches.next(); + assertFalse(match.done); + assertEquals("aa", match.value[0]); + assertEquals(0, match.value.index); + assertEquals(s, match.value.input); + + match = matches.next(); + assertFalse(match.done); + assertEquals("bb", match.value[0]); + assertEquals(2, match.value.index); + assertEquals(s, match.value.input); + + assertIteratorIsDone(matches); +})(); + +(function regexpSymbolMatchAllNonGlobal() { + const s = "aabbc"; + const re = /([a-z])\1/; + const matches = re[Symbol.matchAll](s); + + var match = matches.next(); + assertFalse(match.done); + assertEquals("aa", match.value[0]); + assertEquals(0, match.value.index); + assertEquals(s, match.value.input); + + // No second match, already done + assertIteratorIsDone(matches); +})(); + +(function symbolMatchAllIsNotCallable() { + const s = "aabbc"; + const re = { + [Symbol.matchAll]: 42 + }; + try { + s.matchAll(re); + throw new Error('Expected a TypeError because the value of Symbol.matchAll is not a function'); + } catch (err) { + assertEquals('TypeError', err.name); + assertEquals('Cannot call property Symbol.matchAll in object [object Object]. It is not a function, it is "number".', err.message); + } +})(); + +(function customSymbolMatchAllImplementation() { + const s = "2016-01-02|2019-03-07"; + + const numbers = { + [Symbol.matchAll]: function*(str) { + for (var n of str.matchAll(/[0-9]+/g)) { + yield n[0]; + } + }, + }; + + const results = Array.from(s.matchAll(numbers)); + assertEquals(6, results.length); + assertEquals("2016", results[0]); + assertEquals("01", results[1]); + assertEquals("02", results[2]); + assertEquals("2019", results[3]); + assertEquals("03", results[4]); + assertEquals("07", results[5]); +})(); + +(function nullRegExpShouldMatchLiteralNullString() { + const s = "to null or not to null"; + const results = Array.from(s.matchAll(null)); + assertEquals(2, results.length); + assertEquals("null", results[0][0]); + assertEquals(3, results[0].index); + assertEquals("null", results[1][0]); + assertEquals(18, results[1].index); +})(); + +'success'; diff --git a/tests/testsrc/jstests/es6/symbols.js b/tests/testsrc/jstests/es6/symbols.js index 60aa407526..18b82b7434 100644 --- a/tests/testsrc/jstests/es6/symbols.js +++ b/tests/testsrc/jstests/es6/symbols.js @@ -443,10 +443,19 @@ TestGetOwnPropertySymbolsWithProto() function TestWellKnown() { var symbols = [ + "iterator", + "species", + "toStringTag", "hasInstance", - // TODO(rossberg): reactivate once implemented. - // "isConcatSpreadable", "isRegExp", - "iterator", /* "toStringTag", */ "unscopables" + "isConcatSpreadable", + "isRegExp", + "toPrimitive", + "match", + "matchAll", + "replace", + "search", + "split", + "unscopables", ] for (var i in symbols) { diff --git a/tests/testsrc/test262.properties b/tests/testsrc/test262.properties index 4180f53bbd..a5b400211e 100644 --- a/tests/testsrc/test262.properties +++ b/tests/testsrc/test262.properties @@ -1679,7 +1679,7 @@ built-ins/Promise 406/631 (64.34%) ~built-ins/Reflect -built-ins/RegExp 1169/1854 (63.05%) +built-ins/RegExp 1152/1854 (62.14%) CharacterClassEscapes 24/24 (100.0%) dotall 4/4 (100.0%) escape 20/20 (100.0%) @@ -1758,7 +1758,15 @@ built-ins/RegExp 1169/1854 (63.05%) prototype/sticky/name.js prototype/sticky/prop-desc.js prototype/sticky/this-val-regexp-prototype.js - prototype/Symbol.matchAll 26/26 (100.0%) + prototype/Symbol.matchAll/isregexp-called-once.js + prototype/Symbol.matchAll/isregexp-this-throws.js + prototype/Symbol.matchAll/not-a-constructor.js {unsupported: [Reflect.construct]} + prototype/Symbol.matchAll/species-constructor.js + prototype/Symbol.matchAll/species-constructor-get-species-throws.js + prototype/Symbol.matchAll/this-get-flags.js + prototype/Symbol.matchAll/this-not-object-throws.js + prototype/Symbol.matchAll/this-tostring-flags.js + prototype/Symbol.matchAll/this-tostring-flags-throws.js prototype/Symbol.match/builtin-infer-unicode.js prototype/Symbol.match/builtin-success-g-set-lastindex.js prototype/Symbol.match/builtin-success-g-set-lastindex-err.js @@ -1953,7 +1961,7 @@ built-ins/RegExp 1169/1854 (63.05%) unicode_identity_escape.js valid-flags-y.js -built-ins/RegExpStringIteratorPrototype 17/17 (100.0%) +built-ins/RegExpStringIteratorPrototype 0/17 (0.0%) built-ins/Set 167/381 (43.83%) prototype/add/not-a-constructor.js {unsupported: [Reflect.construct]} @@ -2128,7 +2136,7 @@ built-ins/SetIteratorPrototype 0/11 (0.0%) ~built-ins/SharedArrayBuffer -built-ins/String 116/1182 (9.81%) +built-ins/String 101/1182 (8.54%) fromCharCode/not-a-constructor.js {unsupported: [Reflect.construct]} fromCodePoint/not-a-constructor.js {unsupported: [Reflect.construct]} prototype/charAt/not-a-constructor.js {unsupported: [Reflect.construct]} @@ -2147,7 +2155,11 @@ built-ins/String 116/1182 (9.81%) prototype/isWellFormed/to-string-primitive.js prototype/lastIndexOf/not-a-constructor.js {unsupported: [Reflect.construct]} prototype/localeCompare/not-a-constructor.js {unsupported: [Reflect.construct]} - prototype/matchAll 20/20 (100.0%) + prototype/matchAll/flags-nonglobal-throws.js + prototype/matchAll/flags-undefined-throws.js + prototype/matchAll/not-a-constructor.js {unsupported: [Reflect.construct]} + prototype/matchAll/regexp-matchAll-invocation.js + prototype/matchAll/regexp-prototype-matchAll-invocation.js prototype/match/cstm-matcher-get-err.js prototype/match/cstm-matcher-invocation.js prototype/match/duplicate-named-groups-properties.js @@ -2229,7 +2241,7 @@ built-ins/String 116/1182 (9.81%) built-ins/StringIteratorPrototype 0/7 (0.0%) -built-ins/Symbol 26/94 (27.66%) +built-ins/Symbol 25/94 (26.6%) asyncDispose/prop-desc.js asyncIterator/prop-desc.js dispose/prop-desc.js @@ -2239,7 +2251,7 @@ built-ins/Symbol 26/94 (27.66%) iterator/cross-realm.js keyFor/arg-non-symbol.js keyFor/not-a-constructor.js {unsupported: [Reflect.construct]} - matchAll 2/2 (100.0%) + matchAll/cross-realm.js match/cross-realm.js prototype/description/this-val-non-symbol.js prototype/Symbol.toPrimitive/redefined-symbol-wrapper-ordinary-toprimitive.js