Skip to content

Commit

Permalink
Implement String.prototype.replaceAll (#1155)
Browse files Browse the repository at this point in the history
  • Loading branch information
lahma committed Apr 29, 2022
1 parent e0328f2 commit a2f0bec
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 44 deletions.
4 changes: 3 additions & 1 deletion Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"resizable-arraybuffer",
"ShadowRealm",
"SharedArrayBuffer",
"String.prototype.replaceAll",
"tail-call-optimization",
"top-level-await",
"Temporal",
Expand Down Expand Up @@ -74,6 +73,9 @@
// Issue with \r in source string
"built-ins/RegExp/dotall/without-dotall.js",
"built-ins/RegExp/dotall/without-dotall-unicode.js",

// regex named groups
"built-ins/String/prototype/replaceAll/searchValue-replacer-RegExp-call.js",

// requires investigation how to process complex function name evaluation for property
"built-ins/Function/prototype/toString/method-computed-property-name.js",
Expand Down
2 changes: 1 addition & 1 deletion Jint/JsValueExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static bool IsPromise(this JsValue value)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsRegExp(this JsValue value)
{
if (!(value is ObjectInstance oi))
if (value is not ObjectInstance oi)
{
return false;
}
Expand Down
65 changes: 32 additions & 33 deletions Jint/Native/RegExp/RegExpPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,29 +309,29 @@ private static string CallFunctionalReplace(JsValue replacer, List<JsValue> repl
// $` Inserts the portion of the string that precedes the matched substring.
// $' Inserts the portion of the string that follows the matched substring.
// $n or $nn Where n or nn are decimal digits, inserts the nth parenthesized submatch string, provided the first argument was a RegExp object.
using (var replacementBuilder = StringBuilderPool.Rent())
using var replacementBuilder = StringBuilderPool.Rent();
var sb = replacementBuilder.Builder;
for (var i = 0; i < replacement.Length; i++)
{
for (int i = 0; i < replacement.Length; i++)
char c = replacement[i];
if (c == '$' && i < replacement.Length - 1)
{
char c = replacement[i];
if (c == '$' && i < replacement.Length - 1)
c = replacement[++i];
switch (c)
{
c = replacement[++i];
switch (c)
{
case '$':
replacementBuilder.Builder.Append('$');
break;
case '&':
replacementBuilder.Builder.Append(matched);
break;
case '`':
replacementBuilder.Builder.Append(str.Substring(0, position));
break;
case '\'':
replacementBuilder.Builder.Append(str.Substring(position + matched.Length));
break;
default:
case '$':
sb.Append('$');
break;
case '&':
sb.Append(matched);
break;
case '`':
sb.Append(str.Substring(0, position));
break;
case '\'':
sb.Append(str.Substring(position + matched.Length));
break;
default:
{
if (char.IsDigit(c))
{
Expand All @@ -348,40 +348,39 @@ private static string CallFunctionalReplace(JsValue replacer, List<JsValue> repl
if (matchNumber2 > 0 && matchNumber2 <= captures.Length)
{
// Two digit capture replacement.
replacementBuilder.Builder.Append(TypeConverter.ToString(captures[matchNumber2 - 1]));
sb.Append(TypeConverter.ToString(captures[matchNumber2 - 1]));
i++;
}
else if (matchNumber1 > 0 && matchNumber1 <= captures.Length)
{
// Single digit capture replacement.
replacementBuilder.Builder.Append(TypeConverter.ToString(captures[matchNumber1 - 1]));
sb.Append(TypeConverter.ToString(captures[matchNumber1 - 1]));
}
else
{
// Capture does not exist.
replacementBuilder.Builder.Append('$');
sb.Append('$');
i--;
}
}
else
{
// Unknown replacement pattern.
replacementBuilder.Builder.Append('$');
replacementBuilder.Builder.Append(c);
sb.Append('$');
sb.Append(c);
}

break;
}
}
}
else
{
replacementBuilder.Builder.Append(c);
}
}

return replacementBuilder.ToString();
else
{
sb.Append(c);
}
}

return replacementBuilder.ToString();
}

/// <summary>
Expand Down Expand Up @@ -935,4 +934,4 @@ private JsValue Exec(JsValue thisObj, JsValue[] arguments)
return RegExpBuiltinExec(r, s);
}
}
}
}
119 changes: 112 additions & 7 deletions Jint/Native/String/StringPrototype.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
Expand Down Expand Up @@ -59,6 +60,7 @@ protected override void Initialize()
["match"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "match", Match, 1, lengthFlags), propertyFlags),
["matchAll"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "matchAll", MatchAll, 1, lengthFlags), propertyFlags),
["replace"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "replace", Replace, 2, lengthFlags), propertyFlags),
["replaceAll"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "replaceAll", ReplaceAll, 2, lengthFlags), propertyFlags),
["search"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "search", Search, 1, lengthFlags), propertyFlags),
["slice"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "slice", Slice, 2, lengthFlags), propertyFlags),
["split"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "split", Split, 2, lengthFlags), propertyFlags),
Expand Down Expand Up @@ -501,6 +503,9 @@ private JsValue Search(JsValue thisObj, JsValue[] arguments)
return _engine.Invoke(rx, GlobalSymbolRegistry.Search, new JsValue[] { s });
}

/// <summary>
/// https://tc39.es/ecma262/#sec-string.prototype.replace
/// </summary>
private JsValue Replace(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
Expand All @@ -513,7 +518,7 @@ private JsValue Replace(JsValue thisObj, JsValue[] arguments)
var replacer = GetMethod(_realm, searchValue, GlobalSymbolRegistry.Replace);
if (replacer != null)
{
return replacer.Call(searchValue, new[] { thisObj, replaceValue});
return replacer.Call(searchValue, thisObj, replaceValue);
}
}

Expand All @@ -526,31 +531,128 @@ private JsValue Replace(JsValue thisObj, JsValue[] arguments)
replaceValue = TypeConverter.ToJsString(replaceValue);
}

var pos = thisString.IndexOf(searchString, StringComparison.Ordinal);
var position = thisString.IndexOf(searchString, StringComparison.Ordinal);
var matched = searchString;
if (pos < 0)
if (position < 0)
{
return thisString;
}

string replStr;
if (functionalReplace)
{
var replValue = ((ICallable) replaceValue).Call(Undefined, new JsValue[] {matched, pos, thisString});
var replValue = ((ICallable) replaceValue).Call(Undefined, matched, position, thisString);
replStr = TypeConverter.ToString(replValue);
}
else
{
var captures = System.Array.Empty<string>();
replStr = RegExpPrototype.GetSubstitution(matched, thisString.ToString(), pos, captures, Undefined, TypeConverter.ToString(replaceValue));
replStr = RegExpPrototype.GetSubstitution(matched, thisString.ToString(), position, captures, Undefined, TypeConverter.ToString(replaceValue));
}

var tailPos = pos + matched.Length;
var newString = thisString.Substring(0, pos) + replStr + thisString.Substring(tailPos);
var tailPos = position + matched.Length;
var newString = thisString.Substring(0, position) + replStr + thisString.Substring(tailPos);

return newString;
}

/// <summary>
/// https://tc39.es/ecma262/#sec-string.prototype.replaceall
/// </summary>
private JsValue ReplaceAll(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);

var searchValue = arguments.At(0);
var replaceValue = arguments.At(1);

if (!searchValue.IsNullOrUndefined())
{
if (searchValue.IsRegExp())
{
var flags = searchValue.Get(RegExpPrototype.PropertyFlags);
TypeConverter.CheckObjectCoercible(_engine, flags);
if (TypeConverter.ToString(flags).IndexOf('g') < 0)
{
ExceptionHelper.ThrowTypeError(_realm, "String.prototype.replaceAll called with a non-global RegExp argument");
}
}

var replacer = GetMethod(_realm, searchValue, GlobalSymbolRegistry.Replace);
if (replacer != null)
{
return replacer.Call(searchValue, thisObj, replaceValue);
}
}

var thisString = TypeConverter.ToString(thisObj);
var searchString = TypeConverter.ToString(searchValue);

var functionalReplace = replaceValue is ICallable;

if (!functionalReplace)
{
replaceValue = TypeConverter.ToJsString(replaceValue);

// check fast case
var newValue = replaceValue.ToString();
if (newValue.IndexOf('$') < 0 && searchString.Length > 0)
{
// just plain old string replace
return thisString.Replace(searchString, newValue);
}
}

// https://tc39.es/ecma262/#sec-stringindexof
static int StringIndexOf(string s, string search, int fromIndex)
{
if (search.Length == 0 && fromIndex <= s.Length)
{
return fromIndex;
}

return fromIndex < s.Length
? s.IndexOf(search, fromIndex, StringComparison.Ordinal)
: -1;
}

var searchLength = searchString.Length;
var advanceBy = System.Math.Max(1, searchLength);

var endOfLastMatch = 0;
using var pool = StringBuilderPool.Rent();
var result = pool.Builder;

var position = StringIndexOf(thisString, searchString, 0);
while (position != -1)
{
string replacement;
var preserved = thisString.Substring(endOfLastMatch, position - endOfLastMatch);
if (functionalReplace)
{
var replValue = ((ICallable) replaceValue).Call(Undefined, searchString, position, thisString);
replacement = TypeConverter.ToString(replValue);
}
else
{
var captures = System.Array.Empty<string>();
replacement = RegExpPrototype.GetSubstitution(searchString, thisString, position, captures, Undefined, TypeConverter.ToString(replaceValue));
}

result.Append(preserved).Append(replacement);
endOfLastMatch = position + searchLength;

position = StringIndexOf(thisString, searchString, position + advanceBy);
}

if (endOfLastMatch < thisString.Length)
{
result.Append(thisString.Substring(endOfLastMatch));
}

return result.ToString();
}

private JsValue Match(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
Expand Down Expand Up @@ -657,6 +759,9 @@ private JsValue LastIndexOf(JsValue thisObj, JsValue[] arguments)
return i;
}

/// <summary>
/// https://tc39.es/ecma262/#sec-string.prototype.indexof
/// </summary>
private JsValue IndexOf(JsValue thisObj, JsValue[] arguments)
{
TypeConverter.CheckObjectCoercible(Engine, thisObj);
Expand Down
2 changes: 1 addition & 1 deletion Jint/Runtime/TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,7 @@ public static void CheckObjectCoercible(Engine engine, JsValue o)
{
if (o._type < InternalTypes.Boolean)
{
ExceptionHelper.ThrowTypeError(engine.Realm);
ExceptionHelper.ThrowTypeError(engine.Realm, "Cannot call method on " + o);
}
}

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The entire execution engine was rebuild with performance in mind, in many cases
- ✔ Logical Assignment Operators (`&&=` `||=` `??=`)
- ✔ Numeric Separators (`1_000`)
-`Promise.any` and `AggregateError`
- `String.prototype.replaceAll`
- `String.prototype.replaceAll`
-`WeakRef` and `FinalizationRegistry`

#### ECMAScript 2022
Expand Down

0 comments on commit a2f0bec

Please sign in to comment.