From 3f5f6cc21e52020c44b827c1675d97e911edd990 Mon Sep 17 00:00:00 2001 From: RBRi Date: Wed, 24 Jan 2024 03:33:56 +0100 Subject: [PATCH] Various fixes and implementations to NativeRegExp (#1434) * NativeRegExp: fix RegExp.prototype.toString throwing TypeError on NativeObject * NativeRegExp: implement dotAll flag * NativeString/NativeRegExp: implement String.prototype.replaceAll * NativeString: fix unwanted behavior when Symbol.match of RegExp is set to false Original contributions by @duonglaiquang --- src/org/mozilla/javascript/NativeString.java | 65 ++- src/org/mozilla/javascript/RegExpProxy.java | 3 +- src/org/mozilla/javascript/TokenStream.java | 4 +- .../javascript/regexp/NativeRegExp.java | 37 +- .../mozilla/javascript/regexp/RegExpImpl.java | 66 ++- .../javascript/tests/NativeRegExpTest.java | 151 ++++- .../tests/es6/NativeString2Test.java | 542 +++++++----------- testsrc/test262.properties | 22 +- 8 files changed, 493 insertions(+), 397 deletions(-) diff --git a/src/org/mozilla/javascript/NativeString.java b/src/org/mozilla/javascript/NativeString.java index 8ee76d7168..75c7e5b99e 100644 --- a/src/org/mozilla/javascript/NativeString.java +++ b/src/org/mozilla/javascript/NativeString.java @@ -98,6 +98,7 @@ protected void fillConstructorProperties(IdFunctionObject ctor) { addIdFunctionProperty(ctor, STRING_TAG, ConstructorId_match, "match", 2); addIdFunctionProperty(ctor, STRING_TAG, ConstructorId_search, "search", 2); addIdFunctionProperty(ctor, STRING_TAG, ConstructorId_replace, "replace", 2); + addIdFunctionProperty(ctor, STRING_TAG, ConstructorId_replaceAll, "replaceAll", 2); addIdFunctionProperty(ctor, STRING_TAG, ConstructorId_localeCompare, "localeCompare", 2); addIdFunctionProperty( ctor, STRING_TAG, ConstructorId_toLocaleLowerCase, "toLocaleLowerCase", 1); @@ -246,6 +247,10 @@ protected void initPrototypeId(int id) { arity = 2; s = "replace"; break; + case Id_replaceAll: + arity = 2; + s = "replaceAll"; + break; case Id_at: arity = 1; s = "at"; @@ -345,6 +350,7 @@ public Object execIdCall( case ConstructorId_match: case ConstructorId_search: case ConstructorId_replace: + case ConstructorId_replaceAll: case ConstructorId_localeCompare: case ConstructorId_toLocaleLowerCase: { @@ -463,10 +469,15 @@ public Object execIdCall( String thisString = ScriptRuntime.toString(requireObjectCoercible(cx, thisObj, f)); if (args.length > 0 && args[0] instanceof NativeRegExp) { - throw ScriptRuntime.typeErrorById( - "msg.first.arg.not.regexp", - String.class.getSimpleName(), - f.getFunctionName()); + if (ScriptableObject.isTrue( + ScriptableObject.getProperty( + ScriptableObject.ensureScriptable(args[0]), + SymbolKey.MATCH))) { + throw ScriptRuntime.typeErrorById( + "msg.first.arg.not.regexp", + String.class.getSimpleName(), + f.getFunctionName()); + } } int idx = js_indexOf(id, thisString, args); @@ -599,14 +610,17 @@ public Object execIdCall( case Id_match: case Id_search: case Id_replace: + case Id_replaceAll: { int actionType; if (id == Id_match) { actionType = RegExpProxy.RA_MATCH; } else if (id == Id_search) { actionType = RegExpProxy.RA_SEARCH; - } else { + } else if (id == Id_replace) { actionType = RegExpProxy.RA_REPLACE; + } else { + actionType = RegExpProxy.RA_REPLACE_ALL; } requireObjectCoercible(cx, thisObj, f); @@ -1286,6 +1300,9 @@ protected int findPrototypeId(String s) { case "replace": id = Id_replace; break; + case "replaceAll": + id = Id_replaceAll; + break; case "localeCompare": id = Id_localeCompare; break; @@ -1380,24 +1397,25 @@ protected int findPrototypeId(String s) { Id_match = 31, Id_search = 32, Id_replace = 33, - Id_localeCompare = 34, - Id_toLocaleLowerCase = 35, - Id_toLocaleUpperCase = 36, - Id_trim = 37, - Id_trimLeft = 38, - Id_trimRight = 39, - Id_includes = 40, - Id_startsWith = 41, - Id_endsWith = 42, - Id_normalize = 43, - Id_repeat = 44, - Id_codePointAt = 45, - Id_padStart = 46, - Id_padEnd = 47, - SymbolId_iterator = 48, - Id_trimStart = 49, - Id_trimEnd = 50, - Id_at = 51, + Id_replaceAll = 34, + Id_localeCompare = 35, + Id_toLocaleLowerCase = 36, + Id_toLocaleUpperCase = 37, + Id_trim = 38, + Id_trimLeft = 39, + Id_trimRight = 40, + Id_includes = 41, + Id_startsWith = 42, + Id_endsWith = 43, + Id_normalize = 44, + Id_repeat = 45, + Id_codePointAt = 46, + Id_padStart = 47, + Id_padEnd = 48, + SymbolId_iterator = 49, + Id_trimStart = 50, + Id_trimEnd = 51, + Id_at = 52, MAX_PROTOTYPE_ID = Id_at; private static final int ConstructorId_charAt = -Id_charAt, ConstructorId_charCodeAt = -Id_charCodeAt, @@ -1414,6 +1432,7 @@ protected int findPrototypeId(String s) { ConstructorId_match = -Id_match, ConstructorId_search = -Id_search, ConstructorId_replace = -Id_replace, + ConstructorId_replaceAll = -Id_replaceAll, ConstructorId_localeCompare = -Id_localeCompare, ConstructorId_toLocaleLowerCase = -Id_toLocaleLowerCase; diff --git a/src/org/mozilla/javascript/RegExpProxy.java b/src/org/mozilla/javascript/RegExpProxy.java index b158b629f9..092a93f136 100644 --- a/src/org/mozilla/javascript/RegExpProxy.java +++ b/src/org/mozilla/javascript/RegExpProxy.java @@ -15,7 +15,8 @@ public interface RegExpProxy { // Types of regexp actions public static final int RA_MATCH = 1; public static final int RA_REPLACE = 2; - public static final int RA_SEARCH = 3; + public static final int RA_REPLACE_ALL = 3; + public static final int RA_SEARCH = 4; public boolean isRegExp(Scriptable obj); diff --git a/src/org/mozilla/javascript/TokenStream.java b/src/org/mozilla/javascript/TokenStream.java index 2e5292d5ba..e84d1c9a07 100644 --- a/src/org/mozilla/javascript/TokenStream.java +++ b/src/org/mozilla/javascript/TokenStream.java @@ -1496,8 +1496,8 @@ void readRegExp(int startToken) throws IOException { if (matchChar('g')) addToString('g'); else if (matchChar('i')) addToString('i'); else if (matchChar('m')) addToString('m'); - else if (matchChar('y')) // FireFox 3 - addToString('y'); + else if (matchChar('s')) addToString('s'); + else if (matchChar('y')) addToString('y'); else break; } tokenEnd = start + stringBufferTop + 2; // include slashes diff --git a/src/org/mozilla/javascript/regexp/NativeRegExp.java b/src/org/mozilla/javascript/regexp/NativeRegExp.java index 2b7cffdefe..d707679bf0 100644 --- a/src/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/src/org/mozilla/javascript/regexp/NativeRegExp.java @@ -11,6 +11,7 @@ import org.mozilla.javascript.IdFunctionObject; import org.mozilla.javascript.IdScriptableObject; import org.mozilla.javascript.Kit; +import org.mozilla.javascript.NativeObject; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; @@ -37,7 +38,8 @@ public class NativeRegExp extends IdScriptableObject { public static final int JSREG_GLOB = 0x1; // 'g' flag: global public static final int JSREG_FOLD = 0x2; // 'i' flag: fold public static final int JSREG_MULTILINE = 0x4; // 'm' flag: multiline - public static final int JSREG_STICKY = 0x8; // 'y' flag: sticky + public static final int JSREG_DOTALL = 0x8; // 's' flag: dotAll + public static final int JSREG_STICKY = 0x10; // 'y' flag: sticky // type of match to perform public static final int TEST = 0; @@ -196,6 +198,7 @@ private void appendFlags(StringBuilder buf) { if ((re.flags & JSREG_GLOB) != 0) buf.append('g'); if ((re.flags & JSREG_FOLD) != 0) buf.append('i'); if ((re.flags & JSREG_MULTILINE) != 0) buf.append('m'); + if ((re.flags & JSREG_DOTALL) != 0) buf.append('s'); if ((re.flags & JSREG_STICKY) != 0) buf.append('y'); } @@ -278,6 +281,8 @@ static RECompiled compileRE(Context cx, String str, String global, boolean flat) f = JSREG_FOLD; } else if (c == 'm') { f = JSREG_MULTILINE; + } else if (c == 's') { + f = JSREG_DOTALL; } else if (c == 'y') { f = JSREG_STICKY; } else { @@ -1709,7 +1714,9 @@ private static int simpleMatch( ^ ((gData.cp < end) && isWord(input.charAt(gData.cp)))); break; case REOP_DOT: - if (gData.cp != end && !isLineTerm(input.charAt(gData.cp))) { + if (gData.cp != end + && ((gData.regexp.flags & JSREG_DOTALL) != 0 + || !isLineTerm(input.charAt(gData.cp)))) { result = true; gData.cp++; } @@ -2521,8 +2528,9 @@ private static void reportError(String messageId, String arg) { Id_global = 4, Id_ignoreCase = 5, Id_multiline = 6, - Id_sticky = 7, - MAX_INSTANCE_ID = 7; + Id_dotAll = 7, + Id_sticky = 8, + MAX_INSTANCE_ID = 8; @Override protected int getMaxInstanceId() { @@ -2551,6 +2559,9 @@ protected int findInstanceIdInfo(String s) { case "multiline": id = Id_multiline; break; + case "dotAll": + id = Id_dotAll; + break; case "sticky": id = Id_sticky; break; @@ -2571,6 +2582,7 @@ protected int findInstanceIdInfo(String s) { case Id_global: case Id_ignoreCase: case Id_multiline: + case Id_dotAll: case Id_sticky: attr = PERMANENT | READONLY | DONTENUM; break; @@ -2595,6 +2607,8 @@ protected String getInstanceIdName(int id) { return "ignoreCase"; case Id_multiline: return "multiline"; + case Id_dotAll: + return "dotAll"; case Id_sticky: return "sticky"; } @@ -2620,6 +2634,8 @@ protected Object getInstanceIdValue(int id) { return ScriptRuntime.wrapBoolean((re.flags & JSREG_FOLD) != 0); case Id_multiline: return ScriptRuntime.wrapBoolean((re.flags & JSREG_MULTILINE) != 0); + case Id_dotAll: + return ScriptRuntime.wrapBoolean((re.flags & JSREG_DOTALL) != 0); case Id_sticky: return ScriptRuntime.wrapBoolean((re.flags & JSREG_STICKY) != 0); } @@ -2644,6 +2660,7 @@ protected void setInstanceIdValue(int id, Object value) { case Id_global: case Id_ignoreCase: case Id_multiline: + case Id_dotAll: case Id_sticky: return; } @@ -2716,6 +2733,18 @@ public Object execIdCall( return realThis(thisObj, f).compile(cx, scope, args); case Id_toString: + // thisObj != scope is a strange hack but i had no better idea for the moment + if (thisObj != scope && thisObj instanceof NativeObject) { + Object sourceObj = thisObj.get("source", thisObj); + String source = + sourceObj.equals(NOT_FOUND) ? "undefined" : escapeRegExp(sourceObj); + Object flagsObj = thisObj.get("flags", thisObj); + String flags = flagsObj.equals(NOT_FOUND) ? "undefined" : flagsObj.toString(); + + return "/" + source + "/" + flags; + } + return realThis(thisObj, f).toString(); + case Id_toSource: return realThis(thisObj, f).toString(); diff --git a/src/org/mozilla/javascript/regexp/RegExpImpl.java b/src/org/mozilla/javascript/regexp/RegExpImpl.java index df778b6da7..a2c388a1dd 100644 --- a/src/org/mozilla/javascript/regexp/RegExpImpl.java +++ b/src/org/mozilla/javascript/regexp/RegExpImpl.java @@ -66,6 +66,7 @@ public Object action( } case RA_REPLACE: + case RA_REPLACE_ALL: { boolean useRE = args.length > 0 && args[0] instanceof NativeRegExp; @@ -78,6 +79,11 @@ public Object action( String search = null; if (useRE) { re = createRegExp(cx, scope, args, 2, true); + if (RA_REPLACE_ALL == actionType + && (re.getFlags() & NativeRegExp.JSREG_GLOB) == 0) { + throw ScriptRuntime.typeError( + "replaceAll must be called with a global RegExp"); + } } else { Object arg0 = args.length < 1 ? Undefined.instance : args[0]; search = ScriptRuntime.toString(arg0); @@ -102,32 +108,54 @@ public Object action( Object val; if (useRE) { - val = matchOrReplace(cx, scope, thisObj, args, this, data, re); + Object result = matchOrReplace(cx, scope, thisObj, args, this, data, re); + if (data.charBuf == null) { + if (data.global || result == null || !Boolean.TRUE.equals(result)) { + /* Didn't match even once. */ + return data.str; + } + SubString lc = this.leftContext; + replace_glob(data, cx, scope, this, lc.index, lc.length); + } } else { - String str = data.str; - int index = str.indexOf(search); - if (index >= 0) { - int slen = search.length(); + final String str = data.str; + final int strLen = str.length(), searchLen = search.length(); + int index = -1, lastIndex = 0; + for (; ; ) { + if (search.isEmpty()) { + if (index == -1) { + index = 0; + } else { + index = (lastIndex < strLen) ? lastIndex + 1 : -1; + } + } else { + index = str.indexOf(search, lastIndex); + } + + if (index == -1) { + if (data.charBuf == null) { + return str; + } + break; + } + this.parens = null; this.lastParen = null; this.leftContext = new SubString(str, 0, index); - this.lastMatch = new SubString(str, index, slen); + this.lastMatch = new SubString(str, index, searchLen); this.rightContext = - new SubString(str, index + slen, str.length() - index - slen); - val = Boolean.TRUE; - } else { - val = Boolean.FALSE; - } - } + new SubString( + str, index + searchLen, strLen - index - searchLen); + + replace_glob(data, cx, scope, this, lastIndex, index - lastIndex); + lastIndex = index + searchLen; - if (data.charBuf == null) { - if (data.global || val == null || !val.equals(Boolean.TRUE)) { - /* Didn't match even once. */ - return data.str; + if (actionType != RA_REPLACE_ALL) { + break; + } } - SubString lc = this.leftContext; - replace_glob(data, cx, scope, this, lc.index, lc.length); } + SubString rc = this.rightContext; data.charBuf.append(rc.str, rc.index, rc.index + rc.length); return data.charBuf.toString(); @@ -192,7 +220,7 @@ private static Object matchOrReplace( if (data.mode == RA_MATCH) { match_glob(data, cx, scope, count, reImpl); } else { - if (data.mode != RA_REPLACE) Kit.codeBug(); + if (data.mode != RA_REPLACE && data.mode != RA_REPLACE_ALL) Kit.codeBug(); SubString lastMatch = reImpl.lastMatch; int leftIndex = data.leftIndex; int leftlen = lastMatch.index - leftIndex; diff --git a/testsrc/org/mozilla/javascript/tests/NativeRegExpTest.java b/testsrc/org/mozilla/javascript/tests/NativeRegExpTest.java index 944629438d..c9816fdb72 100644 --- a/testsrc/org/mozilla/javascript/tests/NativeRegExpTest.java +++ b/testsrc/org/mozilla/javascript/tests/NativeRegExpTest.java @@ -28,157 +28,223 @@ public void openBrace() { /** @throws Exception if an error occurs */ @Test public void globalCtor() throws Exception { - testEvaluate("g-true-false-false-false", "new RegExp('foo', 'g');"); + testEvaluate("g-true-false-false-false-false", "new RegExp('foo', 'g');"); } /** @throws Exception if an error occurs */ @Test public void global() throws Exception { - testEvaluate("g-true-false-false-false", "/foo/g;"); + testEvaluate("g-true-false-false-false-false", "/foo/g;"); } /** @throws Exception if an error occurs */ @Test public void ignoreCaseCtor() throws Exception { - testEvaluate("i-false-true-false-false", "new RegExp('foo', 'i');"); + testEvaluate("i-false-true-false-false-false", "new RegExp('foo', 'i');"); } /** @throws Exception if an error occurs */ @Test public void ignoreCase() throws Exception { - testEvaluate("i-false-true-false-false", "/foo/i;"); + testEvaluate("i-false-true-false-false-false", "/foo/i;"); } /** @throws Exception if an error occurs */ @Test public void multilineCtor() throws Exception { - testEvaluate("m-false-false-true-false", "new RegExp('foo', 'm');"); + testEvaluate("m-false-false-true-false-false", "new RegExp('foo', 'm');"); } /** @throws Exception if an error occurs */ @Test public void multiline() throws Exception { - testEvaluate("m-false-false-true-false", "/foo/m;"); + testEvaluate("m-false-false-true-false-false", "/foo/m;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void dotAllCtor() throws Exception { + testEvaluate("s-false-false-false-true-false", "new RegExp('foo', 's');"); + } + + /** @throws Exception if an error occurs */ + @Test + public void dotAll() throws Exception { + testEvaluate("s-false-false-false-true-false", "/foo/s;"); } /** @throws Exception if an error occurs */ @Test public void stickyCtor() throws Exception { - testEvaluate("y-false-false-false-true", "new RegExp('foo', 'y');"); + testEvaluate("y-false-false-false-false-true", "new RegExp('foo', 'y');"); } /** @throws Exception if an error occurs */ @Test public void sticky() throws Exception { - testEvaluate("y-false-false-false-true", "/foo/y;"); + testEvaluate("y-false-false-false-false-true", "/foo/y;"); } /** @throws Exception if an error occurs */ @Test public void globalMultilineCtor() throws Exception { - testEvaluate("gm-true-false-true-false", "new RegExp('foo', 'gm');"); + testEvaluate("gm-true-false-true-false-false", "new RegExp('foo', 'gm');"); } /** @throws Exception if an error occurs */ @Test public void globalMultiline() throws Exception { - testEvaluate("gm-true-false-true-false", "/foo/gm;"); + testEvaluate("gm-true-false-true-false-false", "/foo/gm;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void globalDotAll() throws Exception { + testEvaluate("gs-true-false-false-true-false", "/foo/gs;"); } /** @throws Exception if an error occurs */ @Test public void globalIgnoreCaseCtor() throws Exception { - testEvaluate("gi-true-true-false-false", "new RegExp('foo', 'ig');"); + testEvaluate("gi-true-true-false-false-false", "new RegExp('foo', 'ig');"); } /** @throws Exception if an error occurs */ @Test public void globalIgnoreCase() throws Exception { - testEvaluate("gi-true-true-false-false", "/foo/ig;"); + testEvaluate("gi-true-true-false-false-false", "/foo/ig;"); } /** @throws Exception if an error occurs */ @Test public void globalStickyCtor() throws Exception { - testEvaluate("gy-true-false-false-true", "new RegExp('foo', 'gy');"); + testEvaluate("gy-true-false-false-false-true", "new RegExp('foo', 'gy');"); } /** @throws Exception if an error occurs */ @Test public void globalSticky() throws Exception { - testEvaluate("gy-true-false-false-true", "/foo/gy;"); + testEvaluate("gy-true-false-false-false-true", "/foo/gy;"); } /** @throws Exception if an error occurs */ @Test public void globalMultilineIgnoreCaseCtor() throws Exception { - testEvaluate("gim-true-true-true-false", "new RegExp('foo', 'mig');"); + testEvaluate("gim-true-true-true-false-false", "new RegExp('foo', 'mig');"); } /** @throws Exception if an error occurs */ @Test public void globalMultilineIgnoreCase() throws Exception { - testEvaluate("gim-true-true-true-false", "/foo/gmi;"); + testEvaluate("gim-true-true-true-false-false", "/foo/gmi;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void globalDotAllIgnoreCaseCtor() throws Exception { + testEvaluate("gis-true-true-false-true-false", "new RegExp('foo', 'gsi');"); + } + + /** @throws Exception if an error occurs */ + @Test + public void globalDotAllIgnoreCase() throws Exception { + testEvaluate("gis-true-true-false-true-false", "/foo/gsi;"); } /** @throws Exception if an error occurs */ @Test public void globalIgnoreCaseStickyCtor() throws Exception { - testEvaluate("giy-true-true-false-true", "new RegExp('foo', 'yig');"); + testEvaluate("giy-true-true-false-false-true", "new RegExp('foo', 'yig');"); } /** @throws Exception if an error occurs */ @Test public void globalIgnoreCaseSticky() throws Exception { - testEvaluate("giy-true-true-false-true", "/foo/ygi;"); + testEvaluate("giy-true-true-false-false-true", "/foo/ygi;"); } /** @throws Exception if an error occurs */ @Test public void globalMultilineStickyCtor() throws Exception { - testEvaluate("gmy-true-false-true-true", "new RegExp('foo', 'gmy');"); + testEvaluate("gmy-true-false-true-false-true", "new RegExp('foo', 'gmy');"); } /** @throws Exception if an error occurs */ @Test public void globalMultilineSticky() throws Exception { - testEvaluate("gmy-true-false-true-true", "/foo/gmy;"); + testEvaluate("gmy-true-false-true-false-true", "/foo/gmy;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void globalDotAllStickyCtor() throws Exception { + testEvaluate("gsy-true-false-false-true-true", "new RegExp('foo', 'gys');"); + } + + /** @throws Exception if an error occurs */ + @Test + public void globalDotAllSticky() throws Exception { + testEvaluate("gsy-true-false-false-true-true", "/foo/gys;"); } /** @throws Exception if an error occurs */ @Test public void ignoreCaseMultilineCtor() throws Exception { - testEvaluate("im-false-true-true-false", "new RegExp('foo', 'im');"); + testEvaluate("im-false-true-true-false-false", "new RegExp('foo', 'im');"); } /** @throws Exception if an error occurs */ @Test public void ignoreCaseMultiline() throws Exception { - testEvaluate("im-false-true-true-false", "/foo/mi;"); + testEvaluate("im-false-true-true-false-false", "/foo/mi;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void ignoreCaseDotAllCtor() throws Exception { + testEvaluate("is-false-true-false-true-false", "new RegExp('foo', 'si');"); + } + + /** @throws Exception if an error occurs */ + @Test + public void ignoreCaseDotAll() throws Exception { + testEvaluate("is-false-true-false-true-false", "/foo/si;"); } /** @throws Exception if an error occurs */ @Test public void ignoreCaseStickyCtor() throws Exception { - testEvaluate("iy-false-true-false-true", "new RegExp('foo', 'yi');"); + testEvaluate("iy-false-true-false-false-true", "new RegExp('foo', 'yi');"); } /** @throws Exception if an error occurs */ @Test public void ignoreCaseSticky() throws Exception { - testEvaluate("iy-false-true-false-true", "/foo/iy;"); + testEvaluate("iy-false-true-false-false-true", "/foo/iy;"); } /** @throws Exception if an error occurs */ @Test public void multilineStickyCtor() throws Exception { - testEvaluate("my-false-false-true-true", "new RegExp('foo', 'my');"); + testEvaluate("my-false-false-true-false-true", "new RegExp('foo', 'my');"); } /** @throws Exception if an error occurs */ @Test public void multilineSticky() throws Exception { - testEvaluate("my-false-false-true-true", "/foo/my;"); + testEvaluate("my-false-false-true-false-true", "/foo/my;"); + } + + /** @throws Exception if an error occurs */ + @Test + public void dotAllStickyCtor() throws Exception { + testEvaluate("sy-false-false-false-true-true", "new RegExp('foo', 'ys');"); + } + + /** @throws Exception if an error occurs */ + @Test + public void dotAllSticky() throws Exception { + testEvaluate("sy-false-false-false-true-true", "/foo/ys;"); } private static void testEvaluate(final String expected, final String regex) { @@ -195,6 +261,8 @@ private static void testEvaluate(final String expected, final String regex) { + "res += '-';\n" + "res += regex.multiline;\n" + "res += '-';\n" + + "res += regex.dotAll;\n" + + "res += '-';\n" + "res += regex.sticky;\n" + "res"; @@ -268,6 +336,17 @@ public void matchGlobalSymbol() throws Exception { test("3-a-a-a", script); } + /** @throws Exception if an error occurs */ + @Test + public void matchDotAll() throws Exception { + final String script = + "var result = 'bar\\nfoo'.match(/bar.foo/s);\n" + + "var res = '' + result.length;\n" + + "res = res + '-' + result[0];\n" + + "res;"; + test("1-bar\nfoo", script); + } + /** @throws Exception if an error occurs */ @Test public void matchSticky() throws Exception { @@ -328,6 +407,26 @@ public void flagsPropery() throws Exception { test("0-undefined-true-false-undefined", script); } + /** @throws Exception if an error occurs */ + @Test + public void objectToString() throws Exception { + test("/undefined/undefined", "RegExp.prototype.toString.call({})"); + test("/Foo/undefined", "RegExp.prototype.toString.call({source: 'Foo'})"); + test("/undefined/gy", "RegExp.prototype.toString.call({flags: 'gy'})"); + test("/Foo/g", "RegExp.prototype.toString.call({source: 'Foo', flags: 'g'})"); + test("/Foo/g", "RegExp.prototype.toString.call({source: 'Foo', flags: 'g', sticky: true})"); + + test( + "TypeError: Method \"toString\" called on incompatible object", + "try { RegExp.prototype.toString.call(''); } catch (e) { ('' + e).substr(0, 58) }"); + test( + "TypeError: Method \"toString\" called on incompatible object", + "try { RegExp.prototype.toString.call(undefined); } catch (e) { ('' + e).substr(0, 58) }"); + test( + "TypeError: Method \"toString\" called on incompatible object", + "var toString = RegExp.prototype.toString; try { toString(); } catch (e) { ('' + e).substr(0, 58) }"); + } + private static void test(final String expected, final String script) { Utils.runWithAllOptimizationLevels( cx -> { diff --git a/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java b/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java index 97c64d4616..b31a16bf7f 100644 --- a/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java +++ b/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java @@ -8,11 +8,10 @@ package org.mozilla.javascript.tests.es6; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import org.junit.Test; import org.mozilla.javascript.Context; -import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.tests.Utils; /** Test for handling const variables. */ @@ -20,378 +19,281 @@ public class NativeString2Test { @Test public void getOwnPropertyDescriptorWithIndex() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = - cx.evaluateString( - scope, - " var res = 'hello'.hasOwnProperty('0');" - + " res += ';';" - + " desc = Object.getOwnPropertyDescriptor('hello', '0');" - + " res += desc.value;" - + " res += ';';" - + " res += desc.writable;" - + " res += ';';" - + " res += desc.enumerable;" - + " res += ';';" - + " res += desc.configurable;" - + " res += ';';" - + " res;", - "test", - 1, - null); - assertEquals("true;h;false;true;false;", result); - - return null; - }); + String js = + " var res = 'hello'.hasOwnProperty('0');" + + " res += ';';" + + " desc = Object.getOwnPropertyDescriptor('hello', '0');" + + " res += desc.value;" + + " res += ';';" + + " res += desc.writable;" + + " res += ';';" + + " res += desc.enumerable;" + + " res += ';';" + + " res += desc.configurable;" + + " res += ';';" + + " res;"; + assertEvaluatesES6("true;h;false;true;false;", js); } @Test public void normalizeNoParam() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = cx.evaluateString(scope, "'123'.normalize()", "test", 1, null); - assertEquals("123", result); - - return null; - }); + assertEvaluates("123", "'123'.normalize()"); } @Test public void normalizeNoUndefined() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = - cx.evaluateString(scope, "'123'.normalize(undefined)", "test", 1, null); - assertEquals("123", result); - - return null; - }); + assertEvaluates("123", "'123'.normalize(undefined)"); } @Test public void normalizeNoNull() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = - cx.evaluateString( - scope, - "try { " - + " '123'.normalize(null);" - + "} catch (e) { e.message }", - "test", - 1, - null); - assertEquals( - "The normalization form should be one of 'NFC', 'NFD', 'NFKC', 'NFKD'.", - result); - - return null; - }); + String js = "try { " + " '123'.normalize(null);" + "} catch (e) { e.message }"; + assertEvaluates( + "The normalization form should be one of 'NFC', 'NFD', 'NFKC', 'NFKD'.", js); } @Test public void replaceReplacementAsString() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = - cx.evaluateString(scope, "'123'.replace('2', /x/);", "test", 1, null); - assertEquals("1/x/3", result); - - return null; - }); + assertEvaluates("1null3", "'123'.replace('2', /x/);"); + assertEvaluatesES6("1/x/3", "'123'.replace('2', /x/);"); } @Test public void indexOfEmpty() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = - cx.evaluateString(scope, "'1234'.indexOf('', 0);", "test", 1, null); - assertEquals(0, result); - - result = cx.evaluateString(scope, "'1234'.indexOf('', 1);", "test", 1, null); - assertEquals(1, result); - - result = cx.evaluateString(scope, "'1234'.indexOf('', 4);", "test", 1, null); - assertEquals(4, result); - - result = cx.evaluateString(scope, "'1234'.indexOf('', 5);", "test", 1, null); - assertEquals(4, result); - - result = cx.evaluateString(scope, "'1234'.indexOf('', 42);", "test", 1, null); - assertEquals(4, result); - - return null; - }); + assertEvaluates(0, "'1234'.indexOf('', 0);"); + assertEvaluates(1, "'1234'.indexOf('', 1);"); + assertEvaluates(4, "'1234'.indexOf('', 4);"); + assertEvaluates(4, "'1234'.indexOf('', 5);"); + assertEvaluates(4, "'1234'.indexOf('', 42);"); } @Test public void includesEmpty() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Boolean result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('');", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('', 0);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('', 1);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('', 4);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('', 5);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.includes('', 42);", "test", 1, null); - assertTrue(result); + assertEvaluates(true, "'1234'.includes('');"); + assertEvaluates(true, "'1234'.includes('', 0);"); + assertEvaluates(true, "'1234'.includes('', 1);"); + assertEvaluates(true, "'1234'.includes('', 4);"); + assertEvaluates(true, "'1234'.includes('', 5);"); + assertEvaluates(true, "'1234'.includes('', 42);"); + } - return null; - }); + @Test + public void includesRegExpMatch() { + String js = + "var regExp = /./;\n" + + "var res = '';\n" + + "try {\n" + + " res += '/./'.includes(regExp);\n" + + "} catch (e) {\n" + + " res += e;\n" + + "}\n" + + "regExp[Symbol.match] = false;\n" + + "res += ' # ' + '/./'.includes(regExp);\n" + + "res;"; + + assertEvaluatesES6( + "TypeError: First argument to String.prototype.includes must not be a regular expression # true", + js); } @Test public void startsWithEmpty() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Boolean result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('');", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('', 0);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('', 1);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('', 4);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('', 5);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.startsWith('', 42);", "test", 1, null); - assertTrue(result); + assertEvaluates(true, "'1234'.startsWith('');"); + assertEvaluates(true, "'1234'.startsWith('', 0);"); + assertEvaluates(true, "'1234'.startsWith('', 1);"); + assertEvaluates(true, "'1234'.startsWith('', 4);"); + assertEvaluates(true, "'1234'.startsWith('', 5);"); + assertEvaluates(true, "'1234'.startsWith('', 42);"); + } - return null; - }); + @Test + public void startsWithRegExpMatch() { + String js = + "var regExp = /./;\n" + + "var res = '';\n" + + "try {\n" + + " res += '/./'.startsWith(regExp);\n" + + "} catch (e) {\n" + + " res += e;\n" + + "}\n" + + "regExp[Symbol.match] = false;\n" + + "res += ' # ' + '/./'.includes(regExp);\n" + + "res;"; + + assertEvaluatesES6( + "TypeError: First argument to String.prototype.startsWith must not be a regular expression # true", + js); } @Test public void endsWithEmpty() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Boolean result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('');", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('', 0);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('', 1);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('', 4);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('', 5);", "test", 1, null); - assertTrue(result); - - result = - (Boolean) - cx.evaluateString( - scope, "'1234'.endsWith('', 42);", "test", 1, null); - assertTrue(result); + assertEvaluates(true, "'1234'.endsWith('');"); + assertEvaluates(true, "'1234'.endsWith('', 0);"); + assertEvaluates(true, "'1234'.endsWith('', 1);"); + assertEvaluates(true, "'1234'.endsWith('', 4);"); + assertEvaluates(true, "'1234'.endsWith('', 5);"); + assertEvaluates(true, "'1234'.endsWith('', 42);"); + } - return null; - }); + @Test + public void endsWithRegExpMatch() { + String js = + "var regExp = /./;\n" + + "var res = '';\n" + + "try {\n" + + " res += '/./'.startsWith(regExp);\n" + + "} catch (e) {\n" + + " res += e;\n" + + "}\n" + + "regExp[Symbol.match] = false;\n" + + "res += ' # ' + '/./'.includes(regExp);\n" + + "res;"; + + assertEvaluatesES6( + "TypeError: First argument to String.prototype.startsWith must not be a regular expression # true", + js); } @Test public void tagify() { - Utils.runWithAllOptimizationLevels( - cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - Object result = cx.evaluateString(scope, "'tester'.big()", "test", 1, null); - assertEquals("tester", result); - - result = cx.evaluateString(scope, "'\"tester\"'.big()", "test", 1, null); - assertEquals("\"tester\"", result); - - result = cx.evaluateString(scope, "'\"tester\"'.big()", "test", 1, null); - assertEquals("\"tester\"", result); - - result = cx.evaluateString(scope, "'tester'.fontsize()", "test", 1, null); - assertEquals("tester", result); - - result = cx.evaluateString(scope, "'tester'.fontsize(null)", "test", 1, null); - assertEquals("tester", result); - - result = - cx.evaluateString( - scope, "'tester'.fontsize(undefined)", "test", 1, null); - assertEquals("tester", result); + assertEvaluates("tester", "'tester'.big()"); + assertEvaluates("\"tester\"", "'\"tester\"'.big()"); + assertEvaluates("tester", "'tester'.fontsize()"); + assertEvaluates("tester", "'tester'.fontsize(null)"); + assertEvaluates("tester", "'tester'.fontsize(undefined)"); + assertEvaluates("tester", "'tester'.fontsize(123)"); + assertEvaluates( + "tester", "'tester'.fontsize('\"123\"')"); + } - result = cx.evaluateString(scope, "'tester'.fontsize(123)", "test", 1, null); - assertEquals("tester", result); + @Test + public void tagifyPrototypeNull() { + for (String call : + new String[] { + "big", + "blink", + "bold", + "fixed", + "fontcolor", + "fontsize", + "italics", + "link", + "small", + "strike", + "sub", + "sup" + }) { + String js = "try { String.prototype." + call + ".call(null);} catch (e) { e.message }"; + String expected = "String.prototype." + call + " method called on null or undefined"; + + assertEvaluatesES6(expected, js); + } + } - result = - cx.evaluateString( - scope, "'tester'.fontsize('\"123\"')", "test", 1, null); - assertEquals("tester", result); + @Test + public void tagifyPrototypeUndefined() { + for (String call : + new String[] { + "big", + "blink", + "bold", + "fixed", + "fontcolor", + "fontsize", + "italics", + "link", + "small", + "strike", + "sub", + "sup" + }) { + String js = + "try { String.prototype." + call + ".call(undefined);} catch (e) { e.message }"; + String expected = "String.prototype." + call + " method called on null or undefined"; + + assertEvaluatesES6(expected, js); + } + } - return null; - }); + @Test + public void stringReplace() { + assertEvaluates("xyz", "''.replace('', 'xyz')"); + assertEvaluates("1", "'121'.replace('21', '')"); + assertEvaluates("xyz121", "'121'.replace('', 'xyz')"); + assertEvaluates("a$c21", "'121'.replace('1', 'a$c')"); + assertEvaluates("a121", "'121'.replace('1', 'a$&')"); + assertEvaluates("a$c21", "'121'.replace('1', 'a$$c')"); + assertEvaluates("abaabe", "'abcde'.replace('cd', 'a$`')"); + assertEvaluates("a21", "'121'.replace('1', 'a$`')"); + assertEvaluates("abaee", "'abcde'.replace('cd', \"a$'\")"); + assertEvaluates("aba", "'abcd'.replace('cd', \"a$'\")"); + assertEvaluates("aba$0", "'abcd'.replace('cd', 'a$0')"); + assertEvaluates("aba$1", "'abcd'.replace('cd', 'a$1')"); + assertEvaluates( + "abCD", + "'abcd'.replace('cd', function (matched) { return matched.toUpperCase() })"); + assertEvaluates("", "'123456'.replace(/\\d+/, '')"); + assertEvaluates( + "123ABCD321abcd", + "'123abcd321abcd'.replace(/[a-z]+/, function (matched) { return matched.toUpperCase() })"); } @Test - public void tagifyPrototypeNull() { + public void stringReplaceAll() { + assertEvaluates("xyz", "''.replaceAll('', 'xyz')"); + assertEvaluates("1", "'12121'.replaceAll('21', '')"); + assertEvaluates("xyz1xyz2xyz1xyz", "'121'.replaceAll('', 'xyz')"); + assertEvaluates("a$c2a$c", "'121'.replaceAll('1', 'a$c')"); + assertEvaluates("a12a1", "'121'.replaceAll('1', 'a$&')"); + assertEvaluates("a$c2a$c", "'121'.replaceAll('1', 'a$$c')"); + assertEvaluates("aaadaaabcda", "'abcdabc'.replaceAll('bc', 'a$`')"); + assertEvaluates("a2a12", "'121'.replaceAll('1', 'a$`')"); + assertEvaluates("aadabcdaa", "'abcdabc'.replaceAll('bc', \"a$'\")"); + assertEvaluates("aadabcdaa", "'abcdabc'.replaceAll('bc', \"a$'\")"); + assertEvaluates("aa$0daa$0", "'abcdabc'.replaceAll('bc', 'a$0')"); + assertEvaluates("aa$1daa$1", "'abcdabc'.replaceAll('bc', 'a$1')"); + assertEvaluates("", "'123456'.replaceAll(/\\d+/g, '')"); + assertEvaluates("123456", "'123456'.replaceAll(undefined, '')"); + assertEvaluates("afoobarb", "'afoob'.replaceAll(/(foo)/g, '$1bar')"); + assertEvaluates("foobarb", "'foob'.replaceAll(/(foo)/gy, '$1bar')"); + assertEvaluates("hllo", "'hello'.replaceAll(/(h)e/gy, '$1')"); + assertEvaluates("$1llo", "'hello'.replaceAll(/he/g, '$1')"); + assertEvaluates( + "I$want$these$periods$to$be$$s", + "'I.want.these.periods.to.be.$s'.replaceAll(/\\./g, '$')"); + assertEvaluates("food bar", "'foo bar'.replaceAll(/foo/g, '$&d')"); + assertEvaluates("foo foo ", "'foo bar'.replaceAll(/bar/g, '$`')"); + assertEvaluates(" bar bar", "'foo bar'.replaceAll(/foo/g, '$\\'')"); + assertEvaluates("$' bar", "'foo bar'.replaceAll(/foo/g, '$$\\'')"); + assertEvaluates("ad$0db", "'afoob'.replaceAll(/(foo)/g, 'd$0d')"); + assertEvaluates("ad$0db", "'afkxxxkob'.replace(/(f)k(.*)k(o)/g, 'd$0d')"); + assertEvaluates("ad$0dbd$0dc", "'afoobfuoc'.replaceAll(/(f.o)/g, 'd$0d')"); + assertEvaluates( + "123FOOBAR321BARFOO123", + "'123foobar321barfoo123'.replace(/[a-z]+/g, function (matched) { return matched.toUpperCase() })"); + + assertEvaluates( + "TypeError: replaceAll must be called with a global RegExp", + "try { 'hello'.replaceAll(/he/i, 'x'); } catch (e) { '' + e }"); + } + + private static void assertEvaluates(final Object expected, final String source) { Utils.runWithAllOptimizationLevels( cx -> { - cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - for (String call : - new String[] { - "big", - "blink", - "bold", - "fixed", - "fontcolor", - "fontsize", - "italics", - "link", - "small", - "strike", - "sub", - "sup" - }) { - String code = - "try { String.prototype." - + call - + ".call(null);} catch (e) { e.message }"; - Object result = cx.evaluateString(scope, code, "test", 1, null); - assertEquals( - "String.prototype." + call + " method called on null or undefined", - result); - } - + final Scriptable scope = cx.initStandardObjects(); + final Object rep = cx.evaluateString(scope, source, "test.js", 0, null); + assertEquals(expected, rep); return null; }); } - @Test - public void tagifyPrototypeUndefined() { + private static void assertEvaluatesES6(final Object expected, final String source) { Utils.runWithAllOptimizationLevels( cx -> { cx.setLanguageVersion(Context.VERSION_ES6); - ScriptableObject scope = cx.initStandardObjects(); - - for (String call : - new String[] { - "big", - "blink", - "bold", - "fixed", - "fontcolor", - "fontsize", - "italics", - "link", - "small", - "strike", - "sub", - "sup" - }) { - String code = - "try { String.prototype." - + call - + ".call(undefined);} catch (e) { e.message }"; - Object result = cx.evaluateString(scope, code, "test", 1, null); - assertEquals( - "String.prototype." + call + " method called on null or undefined", - result); - } - + final Scriptable scope = cx.initStandardObjects(); + final Object rep = cx.evaluateString(scope, source, "test.js", 0, null); + assertEquals(expected, rep); return null; }); } diff --git a/testsrc/test262.properties b/testsrc/test262.properties index 626371927d..332b9c6711 100644 --- a/testsrc/test262.properties +++ b/testsrc/test262.properties @@ -1552,7 +1552,7 @@ built-ins/SetIteratorPrototype 0/11 (0.0%) ~built-ins/SharedArrayBuffer -built-ins/String 120/1114 (10.77%) +built-ins/String 99/1114 (8.89%) prototype/endsWith/return-abrupt-from-searchstring-regexp-test.js prototype/includes/return-abrupt-from-searchstring-regexp-test.js prototype/indexOf/position-tointeger-bigint.js {unsupported: [computed-property-names]} @@ -1567,7 +1567,25 @@ built-ins/String 120/1114 (10.77%) prototype/match/cstm-matcher-get-err.js prototype/match/cstm-matcher-invocation.js prototype/match/invoke-builtin-match.js - prototype/replaceAll 40/40 (100.0%) + prototype/replaceAll/getSubstitution-0x0024-0x003C.js + prototype/replaceAll/getSubstitution-0x0024N.js + prototype/replaceAll/getSubstitution-0x0024NN.js + prototype/replaceAll/replaceValue-call-each-match-position.js + prototype/replaceAll/replaceValue-call-matching-empty.js + prototype/replaceAll/replaceValue-value-tostring.js + prototype/replaceAll/searchValue-flags-no-g-throws.js + prototype/replaceAll/searchValue-flags-null-undefined-throws.js + prototype/replaceAll/searchValue-flags-toString-abrupt.js + prototype/replaceAll/searchValue-get-flags-abrupt.js + prototype/replaceAll/searchValue-isRegExp-abrupt.js + prototype/replaceAll/searchValue-replacer-before-tostring.js + prototype/replaceAll/searchValue-replacer-call.js + prototype/replaceAll/searchValue-replacer-call-abrupt.js + prototype/replaceAll/searchValue-replacer-method-abrupt.js + prototype/replaceAll/searchValue-replacer-RegExp-call.js {unsupported: [class]} + prototype/replaceAll/searchValue-replacer-RegExp-call-fn.js {unsupported: [class]} + prototype/replaceAll/searchValue-tostring-regexp.js + prototype/replaceAll/this-tostring.js prototype/replace/cstm-replace-get-err.js prototype/replace/cstm-replace-invocation.js prototype/replace/S15.5.4.11_A12.js non-strict