Issue #1726 - Implement String.replaceAll()

This also implements IsRegExp, as this demands it.
Ported from https://bugzilla.mozilla.org/show_bug.cgi?id=1540021
This commit is contained in:
meatloaf 2021-02-03 20:53:17 -06:00 committed by roytam1
commit 00a689060f
8 changed files with 478 additions and 25 deletions

View file

@ -2111,9 +2111,13 @@ AppendDollarReplacement(StringBuffer& newReplaceChars, size_t firstDollarIndex,
const CharT* repChars, size_t repLength)
{
MOZ_ASSERT(firstDollarIndex < repLength);
MOZ_ASSERT(matchStart <= matchLimit);
MOZ_ASSERT(matchLimit <= text->length());
/* Move the pre-dollar chunk in bulk. */
newReplaceChars.infallibleAppend(repChars, firstDollarIndex);
if (!newReplaceChars.append(repChars, firstDollarIndex)) {
return false;
}
/* Move the rest char-by-char, interpreting dollars as we encounter them. */
const CharT* repLimit = repChars + repLength;
@ -2329,6 +2333,200 @@ js::str_flat_replace_string(JSContext *cx, HandleString string, HandleString pat
return str;
}
// https://tc39.es/proposal-string-replaceall/#sec-string.prototype.replaceall
// Steps 7-16 when functionalReplace is false and searchString is not empty.
//
// The steps are quite different, for performance. Loops in steps 11 and 14
// are fused. GetSubstitution is optimized away when possible.
template <typename StrChar, typename RepChar>
static JSString* ReplaceAll(JSContext* cx, JSLinearString* str,
JSLinearString* pat,
JSLinearString* rep) {
// Step 7.
const size_t stringLength = str->length();
const size_t searchLength = pat->length();
const size_t replaceLength = rep->length();
MOZ_ASSERT(stringLength > 0);
MOZ_ASSERT(searchLength > 0);
MOZ_ASSERT(stringLength >= searchLength);
// Step 8 (advanceBy is equal to searchLength when searchLength > 0).
// Step 9 (not needed in this implementation).
// Step 10.
// Find the first match.
int32_t position = StringMatch(str, pat, 0);
// Nothing to replace, so return early.
if (position < 0) {
return str;
}
// Step 11 (moved below).
// Step 12.
uint32_t endOfLastMatch = 0;
// Step 13.
StringBuffer result(cx);
if (std::is_same<StrChar, char16_t>::value ||
std::is_same<RepChar, char16_t>::value) {
if (!result.ensureTwoByteChars()) {
return nullptr;
}
}
{
AutoCheckCannotGC nogc;
const StrChar* strChars = str->chars<StrChar>(nogc);
const RepChar* repChars = rep->chars<RepChar>(nogc);
uint32_t dollarIndex = FindDollarIndex(repChars, replaceLength);
// If it's true, we are sure that the result's length is, at least, the same
// length as |str->length()|.
if (replaceLength >= searchLength) {
if (!result.reserve(stringLength)) {
return nullptr;
}
}
do {
// Step 14.c.
// Append the substring before the current match.
if (!result.append(strChars + endOfLastMatch,
position - endOfLastMatch)) {
return nullptr;
}
// Steps 14.a-b and 14.d.
// Append the replacement.
if (dollarIndex != UINT32_MAX) {
size_t matchLimit = position + searchLength;
if (!AppendDollarReplacement(result, dollarIndex, position, matchLimit,
str, repChars, replaceLength)) {
return nullptr;
}
} else {
if (!result.append(repChars, replaceLength)) {
return nullptr;
}
}
// Step 14.e.
endOfLastMatch = position + searchLength;
// Step 11.
// Find the next match.
position = StringMatch(str, pat, endOfLastMatch);
} while (position >= 0);
// Step 15.
// Append the substring after the last match.
if (!result.append(strChars + endOfLastMatch,
stringLength - endOfLastMatch)) {
return nullptr;
}
}
// Step 16.
return result.finishString();
}
// https://tc39.es/proposal-string-replaceall/#sec-string.prototype.replaceall
// Steps 7-16 when functionalReplace is false and searchString is the empty
// string.
//
// The steps are quite different, for performance. Loops in steps 11 and 14
// are fused. GetSubstitution is optimized away when possible.
template <typename StrChar, typename RepChar>
static JSString* ReplaceAllInterleave(JSContext* cx, JSLinearString* str,
JSLinearString* rep) {
// Step 7.
const size_t stringLength = str->length();
const size_t replaceLength = rep->length();
// Step 8 (advanceBy is 1 when searchString is the empty string).
// Steps 9-12 (trivial when searchString is the empty string).
// Step 13.
StringBuffer result(cx);
if (std::is_same<StrChar, char16_t>::value ||
std::is_same<RepChar, char16_t>::value) {
if (!result.ensureTwoByteChars()) {
return nullptr;
}
}
{
AutoCheckCannotGC nogc;
const StrChar* strChars = str->chars<StrChar>(nogc);
const RepChar* repChars = rep->chars<RepChar>(nogc);
uint32_t dollarIndex = FindDollarIndex(repChars, replaceLength);
if (dollarIndex != UINT32_MAX) {
if (!result.reserve(stringLength)) {
return nullptr;
}
} else {
// Compute the exact result length when no substitutions take place.
CheckedInt<uint32_t> strLength(stringLength);
CheckedInt<uint32_t> repLength(replaceLength);
CheckedInt<uint32_t> length = strLength + (strLength + 1) * repLength;
if (!length.isValid()) {
ReportAllocationOverflow(cx);
return nullptr;
}
if (!result.reserve(length.value())) {
return nullptr;
}
}
auto appendReplacement = [&](size_t match) {
if (dollarIndex != UINT32_MAX) {
return AppendDollarReplacement(result, dollarIndex, match, match,
str, repChars, replaceLength);
}
return result.append(repChars, replaceLength);
};
for (size_t index = 0; index < stringLength; index++) {
// Steps 11, 14.a-b and 14.d.
// The empty string matches before each character.
if (!appendReplacement(index)) {
return nullptr;
}
// Step 14.c.
if (!result.append(strChars[index])) {
return nullptr;
}
}
// Steps 11, 14.a-b and 14.d.
// The empty string also matches at the end of the string.
if (!appendReplacement(stringLength)) {
return nullptr;
}
// Step 15 (not applicable when searchString is the empty string).
}
// Step 16.
return result.finishString();
}
// String.prototype.replaceAll (Stage 3 proposal)
// https://tc39.es/proposal-string-replaceall/
//
// String.prototype.replaceAll ( searchValue, replaceValue )
//
// Steps 7-16 when functionalReplace is false.o
JSString*
js::str_replace_string_raw(JSContext* cx, HandleString string, HandleString pattern,
HandleString replacement)
@ -2371,6 +2569,62 @@ js::str_replace_string_raw(JSContext* cx, HandleString string, HandleString patt
return BuildFlatReplacement(cx, string, repl, match, patternLength);
}
JSString*
js::str_replaceAll_string_raw(JSContext* cx, HandleString string, HandleString pattern,
HandleString replacement)
{
const size_t stringLength = string->length();
const size_t searchLength = pattern->length();
// Directly return when we're guaranteed to find no match.
if (searchLength > stringLength) {
return string;
}
RootedLinearString str(cx, string->ensureLinear(cx));
if (!str) {
return nullptr;
}
RootedLinearString repl(cx, replacement->ensureLinear(cx));
if (!repl) {
return nullptr;
}
RootedLinearString search(cx, pattern->ensureLinear(cx));
if (!search) {
return nullptr;
}
// The pattern is empty, so we interleave the replacement string in-between
// each character.
if (searchLength == 0) {
if (str->hasTwoByteChars()) {
if (repl->hasTwoByteChars()) {
return ReplaceAllInterleave<char16_t, char16_t>(cx, str, repl);
}
return ReplaceAllInterleave<char16_t, Latin1Char>(cx, str, repl);
}
if (repl->hasTwoByteChars()) {
return ReplaceAllInterleave<Latin1Char, char16_t>(cx, str, repl);
}
return ReplaceAllInterleave<Latin1Char, Latin1Char>(cx, str, repl);
}
MOZ_ASSERT(stringLength > 0);
if (str->hasTwoByteChars()) {
if (repl->hasTwoByteChars()) {
return ReplaceAll<char16_t, char16_t>(cx, str, search, repl);
}
return ReplaceAll<char16_t, Latin1Char>(cx, str, search, repl);
}
if (repl->hasTwoByteChars()) {
return ReplaceAll<Latin1Char, char16_t>(cx, str, search, repl);
}
return ReplaceAll<Latin1Char, Latin1Char>(cx, str, search, repl);
}
// ES 2016 draft Mar 25, 2016 21.1.3.17 steps 4, 8, 12-18.
static JSObject*
SplitHelper(JSContext* cx, HandleLinearString str, uint32_t limit, HandleLinearString sep,
@ -2572,9 +2826,9 @@ static const JSFunctionSpec string_methods[] = {
JS_FN("endsWith", str_endsWith, 1,0),
JS_FN("trim", str_trim, 0,0),
JS_FN("trimLeft", str_trimStart, 0,0),
JS_FN("trimStart", str_trimStart, 0,0),
JS_FN("trimStart", str_trimStart, 0,0),
JS_FN("trimRight", str_trimEnd, 0,0),
JS_FN("trimEnd", str_trimEnd, 0,0),
JS_FN("trimEnd", str_trimEnd, 0,0),
JS_FN("toLocaleLowerCase", str_toLocaleLowerCase, 0,0),
JS_FN("toLocaleUpperCase", str_toLocaleUpperCase, 0,0),
JS_SELF_HOSTED_FN("localeCompare", "String_localeCompare", 1,0),
@ -2586,6 +2840,7 @@ static const JSFunctionSpec string_methods[] = {
JS_SELF_HOSTED_FN("matchAll", "String_matchAll", 1,0),
JS_SELF_HOSTED_FN("search", "String_search", 1,0),
JS_SELF_HOSTED_FN("replace", "String_replace", 2,0),
JS_SELF_HOSTED_FN("replaceAll", "String_replaceAll", 2,0),
JS_SELF_HOSTED_FN("split", "String_split", 2,0),
JS_SELF_HOSTED_FN("substr", "String_substr", 2,0),