diff --git a/src/org/mozilla/javascript/NativeString.java b/src/org/mozilla/javascript/NativeString.java index 8ee76d7168..ba1b9cda64 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: { @@ -599,14 +605,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 +1295,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 +1392,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 +1427,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/regexp/RegExpImpl.java b/src/org/mozilla/javascript/regexp/RegExpImpl.java index df778b6da7..cd52f610ac 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,9 @@ 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); @@ -100,33 +104,52 @@ public Object action( data.charBuf = null; data.leftIndex = 0; - 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 || !result.equals(Boolean.TRUE)) { + /* 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.rightContext = - new SubString(str, index + slen, str.length() - index - slen); - val = Boolean.TRUE; - } else { - val = Boolean.FALSE; - } - } + this.lastMatch = new SubString(str, index, searchLen); + this.rightContext = 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); @@ -192,7 +215,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/es6/NativeString2Test.java b/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java index 97c64d4616..57fc126c74 100644 --- a/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java +++ b/testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java @@ -12,6 +12,7 @@ import org.junit.Test; import org.mozilla.javascript.Context; +import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.tests.Utils; @@ -395,4 +396,64 @@ public void tagifyPrototypeUndefined() { 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 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() })"); + } + + private static void assertEvaluates(final Object expected, final String source) { + Utils.runWithAllOptimizationLevels( + cx -> { + final Scriptable scope = cx.initStandardObjects(); + final Object rep = cx.evaluateString(scope, source, "test.js", 0, null); + assertEquals(expected, rep); + return null; + }); + } }