Skip to content

Commit

Permalink
NativeString/NativeRegExp: implement String.prototype.replaceAll
Browse files Browse the repository at this point in the history
  • Loading branch information
duonglaiquang committed Jan 4, 2024
1 parent afb1962 commit a5d0a2f
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 41 deletions.
52 changes: 33 additions & 19 deletions src/org/mozilla/javascript/NativeString.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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:
{
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion src/org/mozilla/javascript/RegExpProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
65 changes: 44 additions & 21 deletions src/org/mozilla/javascript/regexp/RegExpImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public Object action(
}

case RA_REPLACE:
case RA_REPLACE_ALL:
{
boolean useRE = args.length > 0 && args[0] instanceof NativeRegExp;

Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
61 changes: 61 additions & 0 deletions testsrc/org/mozilla/javascript/tests/es6/NativeString2Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
});
}
}

0 comments on commit a5d0a2f

Please sign in to comment.