diff --git a/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.c b/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.c index a87928e9b..55152b4de 100644 --- a/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.c +++ b/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.c @@ -90,6 +90,7 @@ enum ECMA_STRING_PROTOTYPE_ENDS_WITH, ECMA_STRING_PROTOTYPE_ITERATOR, + ECMA_STRING_PROTOTYPE_REPLACE_ALL, }; #define BUILTIN_INC_HEADER_NAME "ecma-builtin-string-prototype.inc.h" @@ -379,24 +380,108 @@ ecma_builtin_string_prototype_object_match (ecma_value_t this_argument, /**< thi #endif /* ENABLED (JERRY_ESNEXT) */ } /* ecma_builtin_string_prototype_object_match */ +#if ENABLED (JERRY_ESNEXT) /** - * The String.prototype object's 'replace' routine + * Helper method to find a specific character in a string + * + * Used by: + * ecma_builtin_string_prototype_object_replace_helper + * + * @return true - if the given character is in the string + * false - otherwise + */ +static bool +ecma_find_char_in_string (ecma_string_t *str_p, /**< source string */ + lit_utf8_byte_t c) /**< character to find*/ +{ + ECMA_STRING_TO_UTF8_STRING (str_p, start_p, start_size); + + const lit_utf8_byte_t *str_curr_p = start_p; + const lit_utf8_byte_t *str_end_p = start_p + start_size; + bool have_char = false; + + while (str_curr_p < str_end_p) + { + if (*str_curr_p++ == c) + { + have_char = true; + break; + } + } + + ECMA_FINALIZE_UTF8_STRING (start_p, start_size); + + return have_char; +} /* ecma_find_char_in_string */ +#endif /* ENABLED (JERRY_ESNEXT) */ + +/** + * The String.prototype object's 'replace' and 'replaceAll' routine * * See also: - * ECMA-262 v5, 15.5.4.11 - * ECMA-262 v6, 21.1.3.14 + * ECMA-262 v5, 15.5.4.11 (replace ES5) + * ECMA-262 v6, 21.1.3.14 (replace ES6) + * ECMA-262 v12, 21.1.3.18 (replaceAll) * * @return ecma value * Returned value must be freed with ecma_free_value. */ static ecma_value_t -ecma_builtin_string_prototype_object_replace (ecma_value_t this_value, /**< this argument */ - ecma_value_t search_value, /**< routine's first argument */ - ecma_value_t replace_value) /**< routine's second argument */ +ecma_builtin_string_prototype_object_replace_helper (ecma_value_t this_value, /**< this argument */ + ecma_value_t search_value, /**< routine's first argument */ + ecma_value_t replace_value, /**< routine's second argument */ + bool replace_all) { #if ENABLED (JERRY_ESNEXT) if (!(ecma_is_value_undefined (search_value) || ecma_is_value_null (search_value))) { + if (replace_all) + { + ecma_value_t is_regexp = ecma_op_is_regexp (search_value); + + if (ECMA_IS_VALUE_ERROR (is_regexp)) + { + return is_regexp; + } + + if (ecma_is_value_true (is_regexp)) + { + ecma_object_t *regexp_obj_p = ecma_get_object_from_value (search_value); + ecma_value_t get_flags = ecma_op_object_get_by_magic_id (regexp_obj_p, LIT_MAGIC_STRING_FLAGS); + + if (ECMA_IS_VALUE_ERROR (get_flags)) + { + return get_flags; + } + + ecma_value_t coercible = ecma_op_check_object_coercible (get_flags); + + if (ECMA_IS_VALUE_ERROR (coercible)) + { + ecma_free_value (get_flags); + return coercible; + } + + ecma_string_t *flags = ecma_op_to_string (get_flags); + + ecma_free_value (get_flags); + + if (JERRY_UNLIKELY (flags == NULL)) + { + return ECMA_VALUE_ERROR; + } + + bool have_global_flag = ecma_find_char_in_string (flags, LIT_CHAR_LOWERCASE_G); + + ecma_deref_ecma_string (flags); + + if (!have_global_flag) + { + return ecma_raise_type_error (ECMA_ERR_MSG ("RegExp argument should have global flag.")); + } + } + } + ecma_object_t *obj_p = ecma_get_object_from_value (ecma_op_to_object (search_value)); ecma_value_t replace_symbol = ecma_op_object_get_by_symbol_id (obj_p, LIT_GLOBAL_SYMBOL_REPLACE); ecma_deref_object (obj_p); @@ -430,13 +515,19 @@ ecma_builtin_string_prototype_object_replace (ecma_value_t this_value, /**< this } #endif /* ENABLED (JERRY_ESNEXT) */ - ecma_string_t *input_str_p = ecma_get_string_from_value (this_value); + ecma_string_t *input_str_p = ecma_op_to_string (this_value); + + if (input_str_p == NULL) + { + return ECMA_VALUE_ERROR; + } ecma_value_t result = ECMA_VALUE_ERROR; ecma_string_t *search_str_p = ecma_op_to_string (search_value); if (search_str_p == NULL) { + ecma_deref_ecma_string (input_str_p); return result; } @@ -462,10 +553,11 @@ ecma_builtin_string_prototype_object_replace (ecma_value_t this_value, /**< this &input_flags); lit_utf8_size_t search_size; + lit_utf8_size_t search_length; uint8_t search_flags = ECMA_STRING_FLAG_IS_ASCII; const lit_utf8_byte_t *search_buf_p = ecma_string_get_chars (search_str_p, &search_size, - NULL, + &search_length, NULL, &search_flags); @@ -473,19 +565,22 @@ ecma_builtin_string_prototype_object_replace (ecma_value_t this_value, /**< this if (replace_ctx.string_size >= search_size) { + replace_ctx.builder = ecma_stringbuilder_create (); replace_ctx.matched_size = search_size; const lit_utf8_byte_t *const input_end_p = replace_ctx.string_p + replace_ctx.string_size; const lit_utf8_byte_t *const loop_end_p = input_end_p - search_size; + const lit_utf8_byte_t *last_match_end_p = replace_ctx.string_p; + const lit_utf8_byte_t *curr_p = replace_ctx.string_p; lit_utf8_size_t pos = 0; - for (const lit_utf8_byte_t *curr_p = replace_ctx.string_p; - curr_p <= loop_end_p; - lit_utf8_incr (&curr_p), pos++) + while (curr_p <= loop_end_p) { if (!memcmp (curr_p, search_buf_p, search_size)) { - const lit_utf8_size_t byte_offset = (lit_utf8_size_t) (curr_p - replace_ctx.string_p); - replace_ctx.builder = ecma_stringbuilder_create_raw (replace_ctx.string_p, byte_offset); + const lit_utf8_size_t prefix_size = (lit_utf8_size_t) (curr_p - last_match_end_p); + ecma_stringbuilder_append_raw (&replace_ctx.builder, last_match_end_p, prefix_size); + + last_match_end_p = curr_p + search_size; if (replace_ctx.replace_str_p == NULL) { @@ -525,19 +620,34 @@ ecma_builtin_string_prototype_object_replace (ecma_value_t this_value, /**< this else { replace_ctx.matched_p = curr_p; - replace_ctx.match_byte_pos = byte_offset; + replace_ctx.match_byte_pos = (lit_utf8_size_t) (curr_p - replace_ctx.string_p); ecma_builtin_replace_substitute (&replace_ctx); } - const lit_utf8_byte_t *const match_end_p = curr_p + search_size; - ecma_stringbuilder_append_raw (&replace_ctx.builder, - match_end_p, - (lit_utf8_size_t) (input_end_p - match_end_p)); - result_string_p = ecma_stringbuilder_finalize (&replace_ctx.builder); - break; + if (!replace_all + || last_match_end_p == input_end_p) + { + break; + } + + if (search_size != 0) + { + curr_p = last_match_end_p; + pos += search_length; + continue; + } } + + pos++; + lit_utf8_incr (&curr_p); } + + ecma_stringbuilder_append_raw (&replace_ctx.builder, + last_match_end_p, + (lit_utf8_size_t) (input_end_p - last_match_end_p)); + result_string_p = ecma_stringbuilder_finalize (&replace_ctx.builder); + } if (result_string_p == NULL) @@ -566,8 +676,10 @@ cleanup_replace: cleanup_search: ecma_deref_ecma_string (search_str_p); + ecma_deref_ecma_string (input_str_p); + return result; -} /* ecma_builtin_string_prototype_object_replace */ +} /* ecma_builtin_string_prototype_object_replace_helper */ /** * The String.prototype object's 'search' routine @@ -1293,6 +1405,19 @@ ecma_builtin_string_prototype_dispatch_routine (uint16_t builtin_routine_id, /** builtin_routine_id == ECMA_STRING_PROTOTYPE_CHAR_CODE_AT); } +#if ENABLED (JERRY_BUILTIN_REGEXP) + if (builtin_routine_id == ECMA_STRING_PROTOTYPE_REPLACE) + { + return ecma_builtin_string_prototype_object_replace_helper (this_arg, arg1, arg2, false); + } +#if ENABLED (JERRY_ESNEXT) + else if (builtin_routine_id == ECMA_STRING_PROTOTYPE_REPLACE_ALL) + { + return ecma_builtin_string_prototype_object_replace_helper (this_arg, arg1, arg2, true); + } +#endif /* ENABLED (JERRY_ESNEXT) */ +#endif /* ENABLED (JERRY_BUILTIN_REGEXP) */ + ecma_string_t *string_p = ecma_op_to_string (this_arg); if (JERRY_UNLIKELY (string_p == NULL)) @@ -1334,11 +1459,6 @@ ecma_builtin_string_prototype_dispatch_routine (uint16_t builtin_routine_id, /** break; } #if ENABLED (JERRY_BUILTIN_REGEXP) - case ECMA_STRING_PROTOTYPE_REPLACE: - { - ret_value = ecma_builtin_string_prototype_object_replace (to_string_val, arg1, arg2); - break; - } case ECMA_STRING_PROTOTYPE_SEARCH: { ret_value = ecma_builtin_string_prototype_object_search (to_string_val, arg1); diff --git a/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.inc.h b/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.inc.h index 17663d3f1..9a537fb05 100644 --- a/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.inc.h +++ b/jerry-core/ecma/builtin-objects/ecma-builtin-string-prototype.inc.h @@ -52,6 +52,9 @@ ROUTINE (LIT_MAGIC_STRING_LOCALE_COMPARE_UL, ECMA_STRING_PROTOTYPE_LOCALE_COMPAR #if ENABLED (JERRY_BUILTIN_REGEXP) ROUTINE (LIT_MAGIC_STRING_MATCH, ECMA_STRING_PROTOTYPE_MATCH, 1, 1) ROUTINE (LIT_MAGIC_STRING_REPLACE, ECMA_STRING_PROTOTYPE_REPLACE, 2, 2) +#if ENABLED (JERRY_ESNEXT) +ROUTINE (LIT_MAGIC_STRING_REPLACE_ALL, ECMA_STRING_PROTOTYPE_REPLACE_ALL, 2, 2) +#endif /* ENABLED (JERRY_ESNEXT) */ ROUTINE (LIT_MAGIC_STRING_SEARCH, ECMA_STRING_PROTOTYPE_SEARCH, 1, 1) #endif /* ENABLED (JERRY_BUILTIN_REGEXP) */ diff --git a/jerry-core/lit/lit-magic-strings.inc.h b/jerry-core/lit/lit-magic-strings.inc.h index 36e59c7ff..9772976b6 100644 --- a/jerry-core/lit/lit-magic-strings.inc.h +++ b/jerry-core/lit/lit-magic-strings.inc.h @@ -725,6 +725,9 @@ LIT_MAGIC_STRING_DEF (LIT_MAGIC_STRING_IGNORECASE_UL, "ignoreCase") || !(ENABLED (JERRY_ESNEXT)) LIT_MAGIC_STRING_DEF (LIT_MAGIC_STRING_PARSE_FLOAT, "parseFloat") #endif +#if ENABLED (JERRY_BUILTIN_REGEXP) && ENABLED (JERRY_BUILTIN_STRING) && ENABLED (JERRY_ESNEXT) +LIT_MAGIC_STRING_DEF (LIT_MAGIC_STRING_REPLACE_ALL, "replaceAll") +#endif #if ENABLED (JERRY_BUILTIN_DATAVIEW) LIT_MAGIC_STRING_DEF (LIT_MAGIC_STRING_SET_FLOAT_32_UL, "setFloat32") #endif diff --git a/jerry-core/lit/lit-magic-strings.ini b/jerry-core/lit/lit-magic-strings.ini index 50f22c9e2..37fc77eb3 100644 --- a/jerry-core/lit/lit-magic-strings.ini +++ b/jerry-core/lit/lit-magic-strings.ini @@ -289,6 +289,7 @@ LIT_MAGIC_STRING_GET_SECONDS_UL = "getSeconds" LIT_MAGIC_STRING_GET_UTC_DATE_UL = "getUTCDate" LIT_MAGIC_STRING_IGNORECASE_UL = "ignoreCase" LIT_MAGIC_STRING_PARSE_FLOAT = "parseFloat" +LIT_MAGIC_STRING_REPLACE_ALL = "replaceAll" LIT_MAGIC_STRING_SET_FLOAT_32_UL = "setFloat32" LIT_MAGIC_STRING_SET_FLOAT_64_UL = "setFloat64" LIT_MAGIC_STRING_SET_MINUTES_UL = "setMinutes" diff --git a/tests/jerry/es.next/string-prototype-replace-all.js b/tests/jerry/es.next/string-prototype-replace-all.js new file mode 100644 index 000000000..a5dbdf0f7 --- /dev/null +++ b/tests/jerry/es.next/string-prototype-replace-all.js @@ -0,0 +1,258 @@ +// Copyright JS Foundation and other contributors, http://js.foundation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copyright (C) 2019 Leo Balter. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +assert(String.prototype.replaceAll.length === 2); +var desc = Object.getOwnPropertyDescriptor(String.prototype.replaceAll, "length"); +assert(!desc.enumerable); +assert(!desc.writable); +assert(desc.configurable); + +assert(String.prototype.replaceAll.name === "replaceAll"); +var desc = Object.getOwnPropertyDescriptor(String.prototype.replaceAll, "name"); +assert(!desc.enumerable); +assert(!desc.writable); +assert(desc.configurable); + +/** + * Note: The RegExp based replaceAll works the same as the replace method except one special case, + * when the regexp argument doesn't have a global flag, because then it throws an error + */ + +try { + "foo".replaceAll(/./); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +try { + "foo".replaceAll(/./i); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +try { + "foo".replaceAll(/./m); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +try { + "foo".replaceAll(/./u); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +try { + "foo".replaceAll(/./y); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +var regexp = /a/; +Object.defineProperty(regexp, 'flags', { + value: 'muyi' +}); + +try { + "foo".replaceAll(regexp); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +var regexp = /a/g; +Object.defineProperty(regexp, 'flags', { value: undefined }); + +try { + "foo".replaceAll(regexp); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +// test basic functionality +var str = "foobarfoo"; +assert(str.replaceAll("foo", "bar") === "barbarbar"); + +var str = "ab c ab cdab cab c" +assert(str.replaceAll("ab c", "z") === "z zdzz"); + +var str = "ab c" +assert(str.replaceAll("ab c", "z") === "z"); + +var str = "ab c " +assert(str.replaceAll("ab c", "z") === "z "); + +assert('aaab a a aac'.replaceAll('aa', 'z') === 'zab a a zc'); +assert('aaab a a aac'.replaceAll('aa', 'a') === 'aab a a ac'); +assert('aaab a a aac'.replaceAll('a', 'a') === 'aaab a a aac'); +assert('aaab a a aac'.replaceAll('a', 'z') === 'zzzb z z zzc'); + +function replaceValue() { + throw 42; +} + +assert('a'.replaceAll('b', replaceValue) === "a"); +assert('a'.replaceAll('aa', replaceValue) === "a") + +assert('aab c \nx'.replaceAll('', '_') === '_a_a_b_ _c_ _ _\n_x_'); +assert('a'.replaceAll('', '_') === '_a_'); + +// test with replacement string +var str = 'Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar.'; +assert(str.replaceAll('ninguém', '$$') ==='Ninguém é igual a $. Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('é', '$$') === 'Ningu$m $ igual a ningu$m. Todo o ser humano $ um estranho ímpar.'); +assert(str.replaceAll('é', '$$ -') === 'Ningu$ -m $ - igual a ningu$ -m. Todo o ser humano $ - um estranho ímpar.'); +assert(str.replaceAll('é', '$$&') === 'Ningu$&m $& igual a ningu$&m. Todo o ser humano $& um estranho ímpar.'); +assert(str.replaceAll('é', '$$$') === 'Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.'); +assert(str.replaceAll('é', '$$$$') === 'Ningu$$m $$ igual a ningu$$m. Todo o ser humano $$ um estranho ímpar.'); + +assert(str.replaceAll('ninguém', '$&') === 'Ninguém é igual a ninguém. Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('ninguém', '($&)') === 'Ninguém é igual a (ninguém). Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('é', '($&)') === 'Ningu(é)m (é) igual a ningu(é)m. Todo o ser humano (é) um estranho ímpar.'); +assert(str.replaceAll('é', '($&) $&') === 'Ningu(é) ém (é) é igual a ningu(é) ém. Todo o ser humano (é) é um estranho ímpar.'); + +assert(str.replaceAll('ninguém', '$\'') === 'Ninguém é igual a . Todo o ser humano é um estranho ímpar.. Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('.', '--- $\'') === 'Ninguém é igual a ninguém--- Todo o ser humano é um estranho ímpar. Todo o ser humano é um estranho ímpar--- '); +assert(str.replaceAll('é', '($\')') === 'Ningu(m é igual a ninguém. Todo o ser humano é um estranho ímpar.)m ( igual a ninguém. Todo o ser humano é um estranho ímpar.) igual a ningu(m. Todo o ser humano é um estranho ímpar.)m. Todo o ser humano ( um estranho ímpar.) um estranho ímpar.'); +assert(str.replaceAll('é', '($\') $\'') === 'Ningu(m é igual a ninguém. Todo o ser humano é um estranho ímpar.) m é igual a ninguém. Todo o ser humano é um estranho ímpar.m ( igual a ninguém. Todo o ser humano é um estranho ímpar.) igual a ninguém. Todo o ser humano é um estranho ímpar. igual a ningu(m. Todo o ser humano é um estranho ímpar.) m. Todo o ser humano é um estranho ímpar.m. Todo o ser humano ( um estranho ímpar.) um estranho ímpar. um estranho ímpar.'); + +assert(str.replaceAll('ninguém', '$`') === 'Ninguém é igual a Ninguém é igual a . Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('Ninguém', '$`') === ' é igual a ninguém. Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('ninguém', '($`)') === 'Ninguém é igual a (Ninguém é igual a ). Todo o ser humano é um estranho ímpar.'); +assert(str.replaceAll('é', '($`)') === 'Ningu(Ningu)m (Ninguém ) igual a ningu(Ninguém é igual a ningu)m. Todo o ser humano (Ninguém é igual a ninguém. Todo o ser humano ) um estranho ímpar.'); +assert(str.replaceAll('é', '($`) $`') === 'Ningu(Ningu) Ningum (Ninguém ) Ninguém igual a ningu(Ninguém é igual a ningu) Ninguém é igual a ningum. Todo o ser humano (Ninguém é igual a ninguém. Todo o ser humano ) Ninguém é igual a ninguém. Todo o ser humano um estranho ímpar.'); + +// test when functional replacer toString throws error +function custom() { + return { + toString() { + throw 42; + } + } +} + +try { + 'a'.replaceAll('a', custom); + assert(false); +} catch (e) { + assert(e === 42); +} + +function symbol() { + return { + toString() { + return Symbol(); + } + } +} + +try { + 'a'.replaceAll('a', symbol); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +// test when functional replacer Symbol.replace throws error +var poisoned = 0; +var poison = { + toString() { + poisoned += 1; + throw 'Should not call toString on this/replaceValue'; + }, +}; + +var searchValue = { + [Symbol.match]: false, + flags: 'g', + [Symbol.replace]() { + throw 42; + }, + toString() { + throw 'Should not call toString on searchValue'; + } +}; + +try { + ''.replaceAll.call(poison, searchValue, poison); + assert(false); +} catch (e) { + assert(e === 42); +} + +assert(poisoned === 0); + +// test when flags value is undefined or null +var poisoned = 0; +var poison = { + toString() { + poisoned += 1; + throw 'Should not call toString on this/replaceValue'; + }, +}; + +var called = 0; +var value = undefined; +var searchValue = { + [Symbol.match]: true, + get flags() { + called += 1; + return value; + } +}; + +try { + ''.replaceAll.call(poison, searchValue, poison); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +assert(called === 1) // 1); + +called = 0; +value = null; + +try { + ''.replaceAll.call(poison, searchValue, poison); + assert(false); +} catch (e) { + assert(e instanceof TypeError); +} + +assert(called === 1); +assert(poisoned === 0); + +// test when Symbol.match throws error +var searchValue = { + get [Symbol.match]() { + throw 42; + } +}; + +try { + ''.replaceAll.call(poison, searchValue, poison); + assert(false); +} catch (e) { + assert(e === 42); +} diff --git a/tests/test262-esnext-excludelist.xml b/tests/test262-esnext-excludelist.xml index 36ef44ffb..21ae9c096 100644 --- a/tests/test262-esnext-excludelist.xml +++ b/tests/test262-esnext-excludelist.xml @@ -9061,46 +9061,7 @@ https://github.com/tc39/proposal-string-replaceall --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -