diff --git a/packages/core-js/internals/advance-string-index.js b/packages/core-js/internals/advance-string-index.js new file mode 100644 index 000000000000..7dd63ebef541 --- /dev/null +++ b/packages/core-js/internals/advance-string-index.js @@ -0,0 +1,8 @@ +'use strict'; +var at = require('../internals/string-at')(true); + +// `AdvanceStringIndex` abstract operation +// https://tc39.github.io/ecma262/#sec-advancestringindex +module.exports = function (S, index, unicode) { + return index + (unicode ? at(S, index).length : 1); +}; diff --git a/packages/core-js/internals/fix-regexp-well-known-symbol-logic.js b/packages/core-js/internals/fix-regexp-well-known-symbol-logic.js index c1bcc742d40e..c19f4f47d929 100644 --- a/packages/core-js/internals/fix-regexp-well-known-symbol-logic.js +++ b/packages/core-js/internals/fix-regexp-well-known-symbol-logic.js @@ -5,18 +5,36 @@ var fails = require('../internals/fails'); var requireObjectCoercible = require('../internals/require-object-coercible'); var wellKnownSymbol = require('../internals/well-known-symbol'); -module.exports = function (KEY, length, exec) { +var SPECIES = wellKnownSymbol('species'); + +module.exports = function (KEY, length, exec, sham) { var SYMBOL = wellKnownSymbol(KEY); var methods = exec(requireObjectCoercible, SYMBOL, ''[KEY]); var stringMethod = methods[0]; var regexMethod = methods[1]; if (fails(function () { + // String methods call symbol-named RegEp methods var O = {}; O[SYMBOL] = function () { return 7; }; return ''[KEY](O) != 7; + }) || fails(function () { + // Symbol-named RegExp methods call .exec + var execCalled = false; + var re = /a/; + re.exec = function () { execCalled = true; return null; }; + + if (KEY === 'split') { + // RegExp[@@split] doesn't call the regex's exec method, but first creates + // a new one. We need to return the patched regex when creating the new one. + re.constructor = {}; + re.constructor[SPECIES] = function () { return re; }; + } + + re[SYMBOL](''); + return !execCalled; })) { redefine(String.prototype, KEY, stringMethod); - hide(RegExp.prototype, SYMBOL, length == 2 + redefine(RegExp.prototype, SYMBOL, length == 2 // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) // 21.2.5.11 RegExp.prototype[@@split](string, limit) ? function (string, arg) { return regexMethod.call(string, this, arg); } @@ -24,5 +42,6 @@ module.exports = function (KEY, length, exec) { // 21.2.5.9 RegExp.prototype[@@search](string) : function (string) { return regexMethod.call(string, this); } ); + if (sham) hide(RegExp.prototype[SYMBOL], 'sham', true); } }; diff --git a/packages/core-js/internals/regexp-exec.js b/packages/core-js/internals/regexp-exec.js new file mode 100644 index 000000000000..7e8658a023fe --- /dev/null +++ b/packages/core-js/internals/regexp-exec.js @@ -0,0 +1,22 @@ +var classof = require('../internals/classof-raw'); +var builtinExec = RegExp.prototype.exec; + +// `RegExpExec` abstract operation +// https://tc39.github.io/ecma262/#sec-regexpexec +module.exports = function (R, S) { + var exec = R.exec; + if (typeof exec === 'function') { + var result = exec.call(R, S); + if (typeof result !== 'object') { + throw new TypeError('RegExp exec method returned something other than an Object or null'); + } + return result; + } + + if (classof(R) !== 'RegExp') { + throw new TypeError('RegExp#exec called on incompatible receiver'); + } + + return builtinExec.call(R, S); +}; + diff --git a/packages/core-js/modules/es.string.match.js b/packages/core-js/modules/es.string.match.js index 93737d56e407..a396ab7caf35 100644 --- a/packages/core-js/modules/es.string.match.js +++ b/packages/core-js/modules/es.string.match.js @@ -1,11 +1,43 @@ 'use strict'; + +var anObject = require('../internals/an-object'); +var toLength = require('../internals/to-length'); +var advanceStringIndex = require('../internals/advance-string-index'); +var regExpExec = require('../internals/regexp-exec'); +var nativeExec = RegExp.prototype.exec; + // @@match logic require('../internals/fix-regexp-well-known-symbol-logic')('match', 1, function (defined, MATCH, nativeMatch) { - // `String.prototype.match` method - // https://tc39.github.io/ecma262/#sec-string.prototype.match - return [function match(regexp) { - var O = defined(this); - var matcher = regexp == undefined ? undefined : regexp[MATCH]; - return matcher !== undefined ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); - }, nativeMatch]; + return [ + // `String.prototype.match` method + // https://tc39.github.io/ecma262/#sec-string.prototype.match + function match(regexp) { + var O = defined(this); + var matcher = regexp == undefined ? undefined : regexp[MATCH]; + return matcher !== undefined ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); + }, + // `RegExp.prototype[@@match]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@match + function (regexp) { + if (regexp.exec === nativeExec) return nativeMatch.call(this, regexp); + + var rx = anObject(regexp); + var S = String(this); + + if (!rx.global) return regExpExec(rx, S); + + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + var A = []; + var n = 0; + var result; + while ((result = regExpExec(rx, S)) !== null) { + var matchStr = String(result[0]); + A[n] = matchStr; + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + n++; + } + return n === 0 ? null : A; + } + ]; }); diff --git a/packages/core-js/modules/es.string.replace.js b/packages/core-js/modules/es.string.replace.js index 1c912969499d..9a5c35f5d2ad 100644 --- a/packages/core-js/modules/es.string.replace.js +++ b/packages/core-js/modules/es.string.replace.js @@ -1,13 +1,118 @@ 'use strict'; + +var anObject = require('../internals/an-object'); +var toObject = require('../internals/to-object'); +var toLength = require('../internals/to-length'); +var toInteger = require('../internals/to-integer'); +var advanceStringIndex = require('../internals/advance-string-index'); +var regExpExec = require('../internals/regexp-exec'); +var nativeExec = RegExp.prototype.exec; +var max = Math.max; +var min = Math.min; +var floor = Math.floor; +var SUBSTITUTION_SYMBOLS = /\$([$&`']|\d\d?|<[^>]*>)/g; +var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&`']|\d\d?)/g; + +var maybeToString = function (it) { + return it === undefined ? it : String(it); +}; + // @@replace logic require('../internals/fix-regexp-well-known-symbol-logic')('replace', 2, function (defined, REPLACE, nativeReplace) { - // `String.prototype.replace` method - // https://tc39.github.io/ecma262/#sec-string.prototype.replace - return [function replace(searchValue, replaceValue) { - var O = defined(this); - var replacer = searchValue == undefined ? undefined : searchValue[REPLACE]; - return replacer !== undefined + return [ + // `String.prototype.replace` method + // https://tc39.github.io/ecma262/#sec-string.prototype.replace + function replace(searchValue, replaceValue) { + var O = defined(this); + var replacer = searchValue == undefined ? undefined : searchValue[REPLACE]; + return replacer !== undefined ? replacer.call(searchValue, O, replaceValue) : nativeReplace.call(String(O), searchValue, replaceValue); - }, nativeReplace]; + }, + // `RegExp.prototype[@@replace]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@replace + function (regexp, replaceValue) { + if (regexp.exec === nativeExec) return nativeReplace.call(this, regexp, replaceValue); + + var rx = anObject(regexp); + var S = String(this); + + var functionalReplace = typeof replaceValue === 'function'; + if (!functionalReplace) replaceValue = String(replaceValue); + + var global = rx.global; + if (global) { + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + } + var results = []; + while (true) { + var result = regExpExec(rx, S); + if (result === null) break; + + results.push(result); + if (!global) break; + + var matchStr = String(result[0]); + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + } + + var accumulatedResult = ''; + var nextSourcePosition = 0; + for (var i = 0; i < results.length; i++) { + result = results[i]; + + var matched = String(result[0]); + var position = max(min(toInteger(result.index), S.length), 0); + var captures = result.slice(1).map(maybeToString); + var namedCaptures = result.groups; + if (functionalReplace) { + var replacerArgs = [matched].concat(captures, position, S); + if (namedCaptures !== undefined) replacerArgs.push(namedCaptures); + var replacement = String(replaceValue.apply(undefined, replacerArgs)); + } else { + replacement = getSubstitution(matched, S, position, captures, namedCaptures, replaceValue); + } + if (position >= nextSourcePosition) { + accumulatedResult += S.slice(nextSourcePosition, position) + replacement; + nextSourcePosition = position + matched.length; + } + } + return accumulatedResult + S.slice(nextSourcePosition); + } + ]; + + // https://tc39.github.io/ecma262/#sec-getsubstitution + function getSubstitution(matched, str, position, captures, namedCaptures, replacement) { + var tailPos = position + matched.length; + var m = captures.length; + var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED; + if (namedCaptures !== undefined) { + namedCaptures = toObject(namedCaptures); + symbols = SUBSTITUTION_SYMBOLS; + } + return nativeReplace.call(replacement, symbols, function (match, ch) { + var capture; + switch (ch[0]) { + case '$': return '$'; + case '&': return matched; + case '`': return str.slice(0, position); + case "'": return str.slice(tailPos); + case '<': + capture = namedCaptures[ch.slice(1, -1)]; + break; + default: // \d\d? + var n = +ch; + if (n === 0) return ch; + if (n > m) { + var f = floor(n / 10); + if (f === 0) return ch; + if (f <= m) return captures[f - 1] === undefined ? ch[1] : captures[f - 1] + ch[1]; + return ch; + } + capture = captures[n - 1]; + } + return capture === undefined ? '' : capture; + }); + } }); diff --git a/packages/core-js/modules/es.string.search.js b/packages/core-js/modules/es.string.search.js index 9714e1fc24b3..d12f70bd650c 100644 --- a/packages/core-js/modules/es.string.search.js +++ b/packages/core-js/modules/es.string.search.js @@ -1,11 +1,33 @@ 'use strict'; + +var anObject = require('../internals/an-object'); +var sameValue = require('../internals/same-value'); +var regExpExec = require('../internals/regexp-exec'); +var nativeExec = RegExp.prototype.exec; + // @@search logic require('../internals/fix-regexp-well-known-symbol-logic')('search', 1, function (defined, SEARCH, nativeSearch) { - // `String.prototype.search` method - // https://tc39.github.io/ecma262/#sec-string.prototype.search - return [function search(regexp) { - var O = defined(this); - var searcher = regexp == undefined ? undefined : regexp[SEARCH]; - return searcher !== undefined ? searcher.call(regexp, O) : new RegExp(regexp)[SEARCH](String(O)); - }, nativeSearch]; + return [ + // `String.prototype.search` method + // https://tc39.github.io/ecma262/#sec-string.prototype.search + function search(regexp) { + var O = defined(this); + var searcher = regexp == undefined ? undefined : regexp[SEARCH]; + return searcher !== undefined ? searcher.call(regexp, O) : new RegExp(regexp)[SEARCH](String(O)); + }, + // `RegExp.prototype[@@search]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@search + function (regexp) { + if (regexp.exec === nativeExec) return nativeSearch.call(this, regexp); + + var rx = anObject(regexp); + var S = String(this); + + var previousLastIndex = rx.lastIndex; + if (!sameValue(previousLastIndex, 0)) rx.lastIndex = 0; + var result = regExpExec(rx, S); + if (!sameValue(rx.lastIndex, previousLastIndex)) rx.lastIndex = previousLastIndex; + return result === null ? -1 : result.index; + } + ]; }); diff --git a/packages/core-js/modules/es.string.split.js b/packages/core-js/modules/es.string.split.js index d28c4f904c42..420477890d38 100644 --- a/packages/core-js/modules/es.string.split.js +++ b/packages/core-js/modules/es.string.split.js @@ -1,10 +1,22 @@ 'use strict'; + +var isRegExp = require('../internals/is-regexp'); +var anObject = require('../internals/an-object'); +var speciesConstructor = require('../internals/species-constructor'); +var advanceStringIndex = require('../internals/advance-string-index'); +var toLength = require('../internals/to-length'); +var regExpExec = require('../internals/regexp-exec'); +var nativeExec = RegExp.prototype.exec; +var arrayPush = [].push; +var min = Math.min; +var LENGTH = 'length'; + +// eslint-disable-next-line no-empty +var SUPPORTS_Y = !!(function () { try { return new RegExp('x', 'y'); } catch (e) {} })(); + // @@split logic require('../internals/fix-regexp-well-known-symbol-logic')('split', 2, function (defined, SPLIT, nativeSplit) { - var isRegExp = require('../internals/is-regexp'); var internalSplit = nativeSplit; - var arrayPush = [].push; - var LENGTH = 'length'; if ( 'abbc'.split(/(b)*/)[1] == 'c' || 'test'.split(/(?:)/, -1)[LENGTH] != 4 || @@ -62,13 +74,68 @@ require('../internals/fix-regexp-well-known-symbol-logic')('split', 2, function return separator === undefined && limit === 0 ? [] : nativeSplit.call(this, separator, limit); }; } - // `String.prototype.split` method - // https://tc39.github.io/ecma262/#sec-string.prototype.split - return [function split(separator, limit) { - var O = defined(this); - var splitter = separator == undefined ? undefined : separator[SPLIT]; - return splitter !== undefined + + return [ + // `String.prototype.split` method + // https://tc39.github.io/ecma262/#sec-string.prototype.split + function split(separator, limit) { + var O = defined(this); + var splitter = separator == undefined ? undefined : separator[SPLIT]; + return splitter !== undefined ? splitter.call(separator, O, limit) : internalSplit.call(String(O), separator, limit); - }, internalSplit]; -}); + }, + // `RegExp.prototype[@@split]` method + // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@split + // + // NOTE: This cannot be properly polyfilled in engines that don't support + // the 'y' flag. + function (regexp, limit) { + // We can never use `internalSplit` if exec has been changed, because + // internalSplit contains workarounds for things which might have been + // purposely changed by the developer. + if (regexp.exec === nativeExec) return internalSplit.call(this, regexp, limit); + + var rx = anObject(regexp); + var S = String(this); + var C = speciesConstructor(rx, RegExp); + + var unicodeMatching = rx.unicode; + var flags = (rx.ignoreCase ? 'i' : '') + + (rx.multiline ? 'm' : '') + + (rx.unicode ? 'u' : '') + + (SUPPORTS_Y ? 'y' : 'g'); + + // ^(? + rx + ) is needed, in combination with some S slicing, to + // simulate the 'y' flag. + var splitter = new C(SUPPORTS_Y ? rx : '^(?:' + rx.source + ')', flags); + var lim = limit === undefined ? 0xffffffff : limit >>> 0; + if (lim === 0) return []; + if (S.length === 0) return regExpExec(splitter, S) === null ? [S] : []; + var p = 0; + var q = 0; + var A = []; + while (q < S.length) { + splitter.lastIndex = SUPPORTS_Y ? q : 0; + var z = regExpExec(splitter, SUPPORTS_Y ? S : S.slice(q)); + var e; + if ( + z === null || + (e = min(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p + ) { + q = advanceStringIndex(S, q, unicodeMatching); + } else { + A.push(S.slice(p, q)); + if (A.length === lim) return A; + for (var i = 1; i <= z.length - 1; i++) { + A.push(z[i]); + if (A.length === lim) return A; + } + q = p = e; + } + } + A.push(S.slice(p)); + return A; + } + ]; +}, !SUPPORTS_Y); diff --git a/packages/core-js/modules/esnext.string.match-all.js b/packages/core-js/modules/esnext.string.match-all.js index b50089af629e..6e46cf0c2cea 100644 --- a/packages/core-js/modules/esnext.string.match-all.js +++ b/packages/core-js/modules/esnext.string.match-all.js @@ -8,7 +8,7 @@ var classof = require('../internals/classof'); var getFlags = require('../internals/regexp-flags'); var hide = require('../internals/hide'); var speciesConstructor = require('../internals/species-constructor'); -var at = require('../internals/string-at')(true); +var advanceStringIndex = require('../internals/advance-string-index'); var MATCH_ALL = require('../internals/well-known-symbol')('matchAll'); var IS_PURE = require('../internals/is-pure'); var REGEXP_STRING = 'RegExp String'; @@ -19,10 +19,6 @@ var getInternalState = InternalStateModule.getterFor(REGEXP_STRING_ITERATOR); var RegExpPrototype = RegExp.prototype; var regExpBuiltinExec = RegExpPrototype.exec; -var advanceStringIndex = function (S, index, unicode) { - return index + (unicode ? at(S, index).length : 1); -}; - var regExpExec = function (R, S) { var exec = R.exec; var result; diff --git a/tests/helpers/helpers.js b/tests/helpers/helpers.js index 992510e585b6..2fb92f40d0ee 100644 --- a/tests/helpers/helpers.js +++ b/tests/helpers/helpers.js @@ -54,3 +54,24 @@ export function timeLimitedPromise(time, fn) { }), ]); } + +// This function is used to force RegExp.prototype[Symbol.*] methods +// to not use the native implementation. +export function patchRegExp$exec(run) { + return assert => { + const originalExec = RegExp.prototype.exec; + // eslint-disable-next-line no-extend-native + RegExp.prototype.exec = function (...args) { + return originalExec.apply(this, args); + }; + try { + return run(assert); + } catch (e) { + // In very old IE try / finally does not work without catch. + throw e; + } finally { + // eslint-disable-next-line no-extend-native + RegExp.prototype.exec = originalExec; + } + }; +} diff --git a/tests/tests/es.string.match.js b/tests/tests/es.string.match.js index e8b17f759670..c675c495ce90 100644 --- a/tests/tests/es.string.match.js +++ b/tests/tests/es.string.match.js @@ -1,10 +1,11 @@ // TODO: fix escaping in regexps /* eslint-disable no-useless-escape */ import { GLOBAL, NATIVE, STRICT } from '../helpers/constants'; +import { patchRegExp$exec } from '../helpers/helpers'; const Symbol = GLOBAL.Symbol || {}; -QUnit.test('String#match regression', assert => { +const run = assert => { assert.isFunction(''.match); assert.arity(''.match, 1); assert.name(''.match, 'match'); @@ -185,11 +186,20 @@ QUnit.test('String#match regression', assert => { assert.strictEqual(''.match.call(number, regexp).length, 1, 'S15.5.4.10_A2_T18 #2'); assert.strictEqual(''.match.call(number, regexp).index, 1, 'S15.5.4.10_A2_T18 #3'); assert.strictEqual(''.match.call(number, regexp).input, String(number), 'S15.5.4.10_A2_T18 #4'); +}; + +QUnit.test('String#match regression', run); + +QUnit.test('RegExp#@@match appearance', assert => { + const match = /./[Symbol.match]; + assert.isFunction(match); + // assert.name(match, '[Symbol.match]'); + assert.arity(match, 1); + assert.looksNative(match); + assert.nonEnumerable(RegExp.prototype, Symbol.match); }); -QUnit.test('RegExp#@@match', assert => { - assert.isFunction(/./[Symbol.match]); - assert.arity(/./[Symbol.match], 1); +QUnit.test('RegExp#@@match basic behavior', assert => { const string = '123456abcde7890'; const matches = ['12', '34', '56', '78', '90']; assert.strictEqual(/\d{2}/g[Symbol.match](string).length, 5); @@ -198,7 +208,7 @@ QUnit.test('RegExp#@@match', assert => { } }); -QUnit.test('@@match logic', assert => { +QUnit.test('String#match delegates to @@match', assert => { const string = STRICT ? 'string' : Object('string'); const number = STRICT ? 42 : Object(42); const object = {}; @@ -214,3 +224,28 @@ QUnit.test('@@match logic', assert => { assert.strictEqual(string.match(regexp).value, string); assert.strictEqual(''.match.call(number, regexp).value, number); }); + +QUnit.test('RegExp#@@match delegates to exec', assert => { + const exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + + let execCalled = false; + let re = /[ac]/; + re.exec = exec; + assert.deepEqual(re[Symbol.match]('abc'), ['a']); + assert.ok(execCalled); + + re = /a/; + // Not a function, should be ignored + re.exec = 3; + assert.deepEqual(re[Symbol.match]('abc'), ['a']); + + re = /a/; + // Does not return an object, should throw + re.exec = () => 3; + assert.throws(() => re[Symbol.match]('abc')); +}); + +QUnit.test('RegExp#@@match implementation', patchRegExp$exec(run)); diff --git a/tests/tests/es.string.replace.js b/tests/tests/es.string.replace.js index ff92ead45e16..61ceb8d72d67 100644 --- a/tests/tests/es.string.replace.js +++ b/tests/tests/es.string.replace.js @@ -1,8 +1,9 @@ import { GLOBAL, NATIVE, STRICT } from '../helpers/constants'; +import { patchRegExp$exec } from '../helpers/helpers'; const Symbol = GLOBAL.Symbol || {}; -QUnit.test('String#replace regression', assert => { +const run = assert => { assert.isFunction(''.replace); assert.arity(''.replace, 2); assert.name(''.replace, 'replace'); @@ -125,15 +126,24 @@ QUnit.test('String#replace regression', assert => { assert.strictEqual('uid=31'.replace(/(uid=)(\d+)/, '$11A15'), 'uid=1A15', 'S15.5.4.11_A3_T3'); assert.strictEqual('abc12 def34'.replace(/([a-z]+)([0-9]+)/, (a, b, c) => c + b), '12abc def34', 'S15.5.4.11_A4_T1'); assert.strictEqual('aaaaaaaaaa,aaaaaaaaaaaaaaa'.replace(/^(a+)\1*,\1+$/, '$1'), 'aaaaa', 'S15.5.4.11_A5_T1'); +}; + +QUnit.test('String#replace regression', run); + +QUnit.test('RegExp#@@replace appearance', assert => { + const replace = /./[Symbol.replace]; + assert.isFunction(replace); + // assert.name(replace, '[Symbol.replace]'); + assert.arity(replace, 2); + assert.looksNative(replace); + assert.nonEnumerable(RegExp.prototype, Symbol.replace); }); -QUnit.test('RegExp#@@replace', assert => { - assert.isFunction(/./[Symbol.replace]); - assert.arity(/./[Symbol.replace], 2); +QUnit.test('RegExp#@@replace basic behavior', assert => { assert.strictEqual(/([a-z]+)([0-9]+)/[Symbol.replace]('abc12 def34', (a, b, c) => c + b), '12abc def34'); }); -QUnit.test('@@replace logic', assert => { +QUnit.test('String.replace delegates to @@replace', assert => { const string = STRICT ? 'string' : Object('string'); const number = STRICT ? 42 : Object(42); const object = {}; @@ -153,3 +163,57 @@ QUnit.test('@@replace logic', assert => { assert.strictEqual(''.replace.call(number, regexp, 42).a, number); assert.strictEqual(''.replace.call(number, regexp, 42).b, 42); }); + +QUnit.test('RegExp#@@replace delegates to exec', assert => { + const exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + + let execCalled = false; + let re = /[ac]/; + re.exec = exec; + assert.deepEqual(re[Symbol.replace]('abc', 'f'), 'fbc'); + assert.ok(execCalled); + assert.strictEqual(re.lastIndex, 0); + + execCalled = false; + re = /[ac]/g; + re.exec = exec; + assert.deepEqual(re[Symbol.replace]('abc', 'f'), 'fbf'); + assert.ok(execCalled); + assert.strictEqual(re.lastIndex, 0); + + re = /a/; + // Not a function, should be ignored + re.exec = 3; + assert.deepEqual(re[Symbol.replace]('abc', 'f'), 'fbc'); + + re = /a/; + // Does not return an object, should throw + re.exec = () => 3; + assert.throws(() => re[Symbol.replace]('abc', 'f')); +}); + +QUnit.test('RegExp#@@replace correctly handles substitutions', assert => { + const re = /./; + re.exec = function () { + const result = ['23', '7']; + result.groups = { '!!!': '7' }; + result.index = 1; + return result; + }; + assert.strictEqual('1234'.replace(re, '$1'), '174'); + assert.strictEqual('1234'.replace(re, '$'), '174'); + assert.strictEqual('1234'.replace(re, '$`'), '114'); + assert.strictEqual('1234'.replace(re, '$\''), '144'); + assert.strictEqual('1234'.replace(re, '$$'), '1$4'); + assert.strictEqual('1234'.replace(re, '$&'), '1234'); + assert.strictEqual('1234'.replace(re, '$x'), '1$x4'); + + let args; + assert.strictEqual('1234'.replace(re, (..._args) => { args = _args; return 'x'; }), '1x4'); + assert.deepEqual(args, ['23', '7', 1, '1234', { '!!!': '7' }]); +}); + +QUnit.test('RegExp#@@replace implementation', patchRegExp$exec(run)); diff --git a/tests/tests/es.string.search.js b/tests/tests/es.string.search.js index fe739afe4939..3672fcf32c52 100644 --- a/tests/tests/es.string.search.js +++ b/tests/tests/es.string.search.js @@ -1,8 +1,9 @@ import { GLOBAL, STRICT } from '../helpers/constants'; +import { patchRegExp$exec } from '../helpers/helpers'; const Symbol = GLOBAL.Symbol || {}; -QUnit.test('String#search regression', assert => { +const run = assert => { assert.isFunction(''.search); assert.arity(''.search, 1); assert.name(''.search, 'search'); @@ -70,16 +71,25 @@ QUnit.test('String#search regression', assert => { assert.strictEqual(string.search(/the/), string.search(/the/g), 'S15.5.4.12_A3_T1'); string = Object('power \u006F\u0066 the power of the power \u006F\u0066 the power of the power \u006F\u0066 the power of the great sword'); assert.strictEqual(string.search(/of/), string.search(/of/g), 'S15.5.4.12_A3_T2'); +}; + +QUnit.test('String#search regression', run); + +QUnit.test('RegExp#@@search appearance', assert => { + const search = /./[Symbol.search]; + assert.isFunction(search); + // assert.name(search, '[Symbol.search]'); + assert.arity(search, 1); + assert.looksNative(search); + assert.nonEnumerable(RegExp.prototype, Symbol.search); }); -QUnit.test('RegExp#@@search', assert => { - assert.isFunction(/./[Symbol.search]); - assert.arity(/./[Symbol.search], 1); +QUnit.test('RegExp#@@search basic behavior', assert => { assert.strictEqual(/four/[Symbol.search]('one two three four five'), 14); assert.strictEqual(/Four/[Symbol.search]('one two three four five'), -1); }); -QUnit.test('@@search logic', assert => { +QUnit.test('String#search delegates to @@search', assert => { const string = STRICT ? 'string' : Object('string'); const number = STRICT ? 42 : Object(42); const object = {}; @@ -95,3 +105,28 @@ QUnit.test('@@search logic', assert => { assert.strictEqual(string.search(regexp).value, string); assert.strictEqual(''.search.call(number, regexp).value, number); }); + +QUnit.test('RegExp#@@search delegates to exec', assert => { + let execCalled = false; + let re = /b/; + re.lastIndex = 7; + re.exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + assert.deepEqual(re[Symbol.search]('abc'), 1); + assert.ok(execCalled); + assert.strictEqual(re.lastIndex, 7); + + re = /b/; + // Not a function, should be ignored + re.exec = 3; + assert.deepEqual(re[Symbol.search]('abc'), 1); + + re = /b/; + // Does not return an object, should throw + re.exec = () => 3; + assert.throws(() => re[Symbol.search]('abc')); +}); + +QUnit.test('RegExp#@@search implementation', patchRegExp$exec(run)); diff --git a/tests/tests/es.string.split.js b/tests/tests/es.string.split.js index 4a24b3aea3b8..2edb629eeb82 100644 --- a/tests/tests/es.string.split.js +++ b/tests/tests/es.string.split.js @@ -1,8 +1,9 @@ import { GLOBAL, NATIVE, STRICT } from '../helpers/constants'; +import { patchRegExp$exec } from '../helpers/helpers'; const Symbol = GLOBAL.Symbol || {}; -QUnit.test('String#split regression', assert => { +const run = assert => { assert.isFunction(''.split); assert.arity(''.split, 2); assert.name(''.split, 'split'); @@ -687,18 +688,27 @@ QUnit.test('String#split regression', assert => { assert.strictEqual(expected[i], split[i], `S15.5.4.14_A4_T24 #${ i + 3 }`); } } +}; + +QUnit.test('String#split regression', run); + +QUnit.test('RegExp#@@split appearance', assert => { + const split = /./[Symbol.split]; + assert.isFunction(split); + // assert.name(split, '[Symbol.split]'); + assert.arity(split, 2); + assert.looksNative(split); + assert.nonEnumerable(RegExp.prototype, Symbol.split); }); -QUnit.test('RegExp#@@split', assert => { - assert.isFunction(/./[Symbol.split]); - assert.arity(/./[Symbol.split], 2); +QUnit.test('RegExp#@@split basic behavior', assert => { assert.strictEqual(/\s/[Symbol.split]('a b c de f').length, 5); assert.strictEqual(/\s/[Symbol.split]('a b c de f', undefined).length, 5); assert.strictEqual(/\s/[Symbol.split]('a b c de f', 1).length, 1); assert.strictEqual(/\s/[Symbol.split]('a b c de f', 10).length, 5); }); -QUnit.test('@@split logic', assert => { +QUnit.test('String#split delegates to @@split', assert => { const string = STRICT ? 'string' : Object('string'); const number = STRICT ? 42 : Object(42); const object = {}; @@ -718,3 +728,54 @@ QUnit.test('@@split logic', assert => { assert.strictEqual(''.split.call(number, regexp, 42).a, number); assert.strictEqual(''.split.call(number, regexp, 42).b, 42); }); + +QUnit.test('RegExp#@@split delegates to exec', assert => { + let execCalled = false; + let speciesCalled = false; + let execSpeciesCalled = false; + const re = /[24]/; + re.exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + re.constructor = { + // eslint-disable-next-line object-shorthand + [Symbol.species]: function (source, flags) { + const re2 = new RegExp(source, flags); + speciesCalled = true; + re2.exec = function () { + execSpeciesCalled = true; + return /./.exec.apply(this, arguments); + }; + return re2; + }, + }; + assert.deepEqual(re[Symbol.split]('123451234'), ['1', '3', '51', '3', '']); + assert.ok(!execCalled); + assert.ok(speciesCalled); + assert.ok(execSpeciesCalled); + + re.constructor = { + // eslint-disable-next-line object-shorthand + [Symbol.species]: function (source, flags) { + const re2 = new RegExp(source, flags); + // Not a function, should be ignored + re2.exec = 3; + return re2; + }, + }; + assert.deepEqual(re[Symbol.split]('123451234'), ['1', '3', '51', '3', '']); + + re.constructor = { + // eslint-disable-next-line object-shorthand + [Symbol.species]: function (source, flags) { + const re2 = new RegExp(source, flags); + // Does not return an object, should throw + re2.exec = () => 3; + return re2; + }, + }; + assert.throws(() => re[Symbol.split]('123451234')); +}); + +QUnit.test('RegExp#@@split implementation', patchRegExp$exec(run));