diff --git a/packages/core-js-compat/src/data.js b/packages/core-js-compat/src/data.js index b41981a4bcb8..e537f057e07f 100644 --- a/packages/core-js-compat/src/data.js +++ b/packages/core-js-compat/src/data.js @@ -815,6 +815,12 @@ const data = { firefox: '37', safari: '9.0', }, + 'es.regexp.sticky': { + chrome: '49', + edge: '13', + firefox: '3', + safari: '10.0', + }, 'es.regexp.to-string': { chrome: '50', firefox: '46', diff --git a/packages/core-js/es/index.js b/packages/core-js/es/index.js index 0236877d3607..9d48e4977a6a 100644 --- a/packages/core-js/es/index.js +++ b/packages/core-js/es/index.js @@ -105,6 +105,7 @@ require('../modules/es.string.sup'); require('../modules/es.regexp.constructor'); require('../modules/es.regexp.exec'); require('../modules/es.regexp.flags'); +require('../modules/es.regexp.sticky'); require('../modules/es.regexp.to-string'); require('../modules/es.parse-int'); require('../modules/es.parse-float'); diff --git a/packages/core-js/es/regexp/index.js b/packages/core-js/es/regexp/index.js index 69467e49e33c..ac93a6cde70a 100644 --- a/packages/core-js/es/regexp/index.js +++ b/packages/core-js/es/regexp/index.js @@ -2,6 +2,7 @@ require('../../modules/es.regexp.constructor'); require('../../modules/es.regexp.to-string'); require('../../modules/es.regexp.exec'); require('../../modules/es.regexp.flags'); +require('../../modules/es.regexp.sticky'); require('../../modules/es.string.match'); require('../../modules/es.string.replace'); require('../../modules/es.string.search'); diff --git a/packages/core-js/es/regexp/sticky.js b/packages/core-js/es/regexp/sticky.js new file mode 100644 index 000000000000..eb33fb10565b --- /dev/null +++ b/packages/core-js/es/regexp/sticky.js @@ -0,0 +1,5 @@ +require('../../modules/es.regexp.sticky'); + +module.exports = function (it) { + return it.sticky; +}; 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 04b9f2dd4baa..52bc63a0ac38 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 @@ -1,5 +1,4 @@ 'use strict'; -var createNonEnumerableProperty = require('../internals/create-non-enumerable-property'); var redefine = require('../internals/redefine'); var fails = require('../internals/fails'); var wellKnownSymbol = require('../internals/well-known-symbol'); @@ -20,6 +19,12 @@ var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { return ''.replace(re, '$') !== '7'; }); +// IE <= 11 replaces $0 with the whole match, as if it was $& +// https://stackoverflow.com/questions/6024666/getting-ie-to-replace-a-regex-with-the-literal-string-0 +var REPLACE_KEEPS_$0 = (function () { + return 'a'.replace(/./, '$0') === '$0'; +})(); + // Chrome 51 has a buggy "split" implementation when RegExp#exec !== nativeExec // Weex JS has frozen built-in prototypes, so use try / catch wrapper var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = !fails(function () { @@ -30,7 +35,7 @@ var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = !fails(function () { return result.length !== 2 || result[0] !== 'a' || result[1] !== 'b'; }); -module.exports = function (KEY, length, exec, sham) { +module.exports = function (KEY, length, exec) { var SYMBOL = wellKnownSymbol(KEY); var DELEGATES_TO_SYMBOL = !fails(function () { @@ -67,7 +72,7 @@ module.exports = function (KEY, length, exec, sham) { if ( !DELEGATES_TO_SYMBOL || !DELEGATES_TO_EXEC || - (KEY === 'replace' && !REPLACE_SUPPORTS_NAMED_GROUPS) || + (KEY === 'replace' && !(REPLACE_SUPPORTS_NAMED_GROUPS && REPLACE_KEEPS_$0)) || (KEY === 'split' && !SPLIT_WORKS_WITH_OVERWRITTEN_EXEC) ) { var nativeRegExpMethod = /./[SYMBOL]; @@ -82,7 +87,7 @@ module.exports = function (KEY, length, exec, sham) { return { done: true, value: nativeMethod.call(str, regexp, arg2) }; } return { done: false }; - }); + }, { REPLACE_KEEPS_$0: REPLACE_KEEPS_$0 }); var stringMethod = methods[0]; var regexMethod = methods[1]; @@ -95,6 +100,5 @@ module.exports = function (KEY, length, exec, sham) { // 21.2.5.9 RegExp.prototype[@@search](string) : function (string) { return regexMethod.call(string, this); } ); - if (sham) createNonEnumerableProperty(RegExp.prototype[SYMBOL], 'sham', true); } }; diff --git a/packages/core-js/internals/regexp-exec.js b/packages/core-js/internals/regexp-exec.js index 7b9aa24daf3b..eb1ae1e19974 100644 --- a/packages/core-js/internals/regexp-exec.js +++ b/packages/core-js/internals/regexp-exec.js @@ -1,5 +1,6 @@ 'use strict'; var regexpFlags = require('./regexp-flags'); +var stickyHelpers = require('./regexp-sticky-helpers'); var nativeExec = RegExp.prototype.exec; // This always refers to the native implementation, because the @@ -17,24 +18,42 @@ var UPDATES_LAST_INDEX_WRONG = (function () { return re1.lastIndex !== 0 || re2.lastIndex !== 0; })(); +var UNSUPPORTED_Y = stickyHelpers.UNSUPPORTED_Y || stickyHelpers.BROKEN_CARET; + // nonparticipating capturing group, copied from es5-shim's String#split patch. var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined; -var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED; +var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED || UNSUPPORTED_Y; if (PATCH) { patchedExec = function exec(str) { var re = this; var lastIndex, reCopy, match, i; + var sticky = UNSUPPORTED_Y && re.sticky; + + if (sticky) { + var flags = (re.ignoreCase ? 'i' : '') + + (re.multiline ? 'm' : '') + + (re.unicode ? 'u' : '') + + 'g'; + + // ^(? + rx + ) is needed, in combination with some str slicing, to + // simulate the 'y' flag. + reCopy = new RegExp('^(?:' + re.source + ')', flags); + str = String(str).slice(re.lastIndex); + } if (NPCG_INCLUDED) { reCopy = new RegExp('^' + re.source + '$(?!\\s)', regexpFlags.call(re)); } if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex; - match = nativeExec.call(re, str); + match = nativeExec.call(sticky ? reCopy : re, str); - if (UPDATES_LAST_INDEX_WRONG && match) { + if (sticky) { + if (match) re.lastIndex += match[0].length; + else re.lastIndex = 0; + } else if (UPDATES_LAST_INDEX_WRONG && match) { re.lastIndex = re.global ? match.index + match[0].length : lastIndex; } if (NPCG_INCLUDED && match && match.length > 1) { diff --git a/packages/core-js/internals/regexp-sticky-helpers.js b/packages/core-js/internals/regexp-sticky-helpers.js new file mode 100644 index 000000000000..3292daa6856d --- /dev/null +++ b/packages/core-js/internals/regexp-sticky-helpers.js @@ -0,0 +1,42 @@ +'use strict'; + +var fails = require('./fails'); +var speciesConstructor = require('../internals/species-constructor'); + +// babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError, +// so we use an intermediate function. +function RE(s, f) { + return RegExp(s, f); +} + +exports.UNSUPPORTED_Y = fails(function () { + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError + var re = RE('a', 'y'); + re.lastIndex = 2; + return re.exec('abcd') != null; +}); + +exports.BROKEN_CARET = fails(function () { + // https://bugzilla.mozilla.org/show_bug.cgi?id=773687 + var re = RE('^r', 'gy'); + re.lastIndex = 2; + return re.exec('str') != null; +}); + +exports.createStickyRegExp = function (re, otherFlags) { + var C = speciesConstructor(re, RegExp); + + if (C !== RegExp) return new C(re, otherFlags + 'y'); + + // y is either supported or polyfilled + if (!exports.UNSUPPORTED_Y || RegExp.sham) { + return new RegExp(re, otherFlags + 'y'); + } + + // If y hasn't been polyfilled and it isn't supported, assigning + // to .sticky won't throw. + // This usually happens in engines where descriptors aren't supported. + var fakeRe = new RegExp(re, otherFlags); + fakeRe.sticky = true; + return fakeRe; +}; diff --git a/packages/core-js/modules/es.regexp.constructor.js b/packages/core-js/modules/es.regexp.constructor.js index 05a3370f2994..4662d8256253 100644 --- a/packages/core-js/modules/es.regexp.constructor.js +++ b/packages/core-js/modules/es.regexp.constructor.js @@ -1,4 +1,5 @@ var DESCRIPTORS = require('../internals/descriptors'); +var createNonEnumerableProperty = require('../internals/create-non-enumerable-property'); var global = require('../internals/global'); var isForced = require('../internals/is-forced'); var inheritIfRequired = require('../internals/inherit-if-required'); @@ -6,8 +7,10 @@ var defineProperty = require('../internals/object-define-property').f; var getOwnPropertyNames = require('../internals/object-get-own-property-names').f; var isRegExp = require('../internals/is-regexp'); var getFlags = require('../internals/regexp-flags'); +var stickyHelpers = require('../internals/regexp-sticky-helpers'); var redefine = require('../internals/redefine'); var fails = require('../internals/fails'); +var setInternalState = require('../internals/internal-state').set; var setSpecies = require('../internals/set-species'); var wellKnownSymbol = require('../internals/well-known-symbol'); @@ -20,7 +23,9 @@ var re2 = /a/g; // "new" should create a new object, old webkit bug var CORRECT_NEW = new NativeRegExp(re1) !== re1; -var FORCED = DESCRIPTORS && isForced('RegExp', (!CORRECT_NEW || fails(function () { +var UNSUPPORTED_Y = stickyHelpers.UNSUPPORTED_Y; + +var FORCED = DESCRIPTORS && isForced('RegExp', (!CORRECT_NEW || UNSUPPORTED_Y || fails(function () { re2[MATCH] = false; // RegExp constructor can alter flags and IsRegExp works correct with @@match return NativeRegExp(re1) != re1 || NativeRegExp(re2) == re2 || NativeRegExp(re1, 'i') != '/a/i'; @@ -33,13 +38,32 @@ if (FORCED) { var thisIsRegExp = this instanceof RegExpWrapper; var patternIsRegExp = isRegExp(pattern); var flagsAreUndefined = flags === undefined; - return !thisIsRegExp && patternIsRegExp && pattern.constructor === RegExpWrapper && flagsAreUndefined ? pattern - : inheritIfRequired(CORRECT_NEW - ? new NativeRegExp(patternIsRegExp && !flagsAreUndefined ? pattern.source : pattern, flags) - : NativeRegExp((patternIsRegExp = pattern instanceof RegExpWrapper) - ? pattern.source - : pattern, patternIsRegExp && flagsAreUndefined ? getFlags.call(pattern) : flags) - , thisIsRegExp ? this : RegExpPrototype, RegExpWrapper); + + if (!thisIsRegExp && patternIsRegExp && pattern.constructor === RegExpWrapper && flagsAreUndefined) { + return pattern; + } + + if (CORRECT_NEW) { + if (patternIsRegExp && !flagsAreUndefined) pattern = pattern.source; + } else if (pattern instanceof RegExpWrapper) { + if (flagsAreUndefined) flags = getFlags.call(pattern); + pattern = pattern.source; + } + + if (UNSUPPORTED_Y) { + var sticky = !!flags && flags.indexOf('y') > -1; + if (sticky) flags = flags.replace(/y/g, ''); + } + + var result = inheritIfRequired( + CORRECT_NEW ? new NativeRegExp(pattern, flags) : NativeRegExp(pattern, flags), + thisIsRegExp ? this : RegExpPrototype, + RegExpWrapper + ); + + if (UNSUPPORTED_Y) setInternalState(result, { sticky: sticky }); + + return result; }; var proxy = function (key) { key in RegExpWrapper || defineProperty(RegExpWrapper, key, { @@ -54,6 +78,8 @@ if (FORCED) { RegExpPrototype.constructor = RegExpWrapper; RegExpWrapper.prototype = RegExpPrototype; redefine(global, 'RegExp', RegExpWrapper); + + if (UNSUPPORTED_Y) createNonEnumerableProperty(RegExpWrapper, 'sham', true); } // https://tc39.github.io/ecma262/#sec-get-regexp-@@species diff --git a/packages/core-js/modules/es.regexp.flags.js b/packages/core-js/modules/es.regexp.flags.js index 693b22a8b950..2ad5f267e642 100644 --- a/packages/core-js/modules/es.regexp.flags.js +++ b/packages/core-js/modules/es.regexp.flags.js @@ -1,10 +1,11 @@ var DESCRIPTORS = require('../internals/descriptors'); var objectDefinePropertyModule = require('../internals/object-define-property'); var regExpFlags = require('../internals/regexp-flags'); +var UNSUPPORTED_Y = require('../internals/regexp-sticky-helpers').UNSUPPORTED_Y; // `RegExp.prototype.flags` getter // https://tc39.github.io/ecma262/#sec-get-regexp.prototype.flags -if (DESCRIPTORS && /./g.flags != 'g') { +if (DESCRIPTORS && (/./g.flags != 'g' || UNSUPPORTED_Y)) { objectDefinePropertyModule.f(RegExp.prototype, 'flags', { configurable: true, get: regExpFlags diff --git a/packages/core-js/modules/es.regexp.sticky.js b/packages/core-js/modules/es.regexp.sticky.js new file mode 100644 index 000000000000..02da204fef27 --- /dev/null +++ b/packages/core-js/modules/es.regexp.sticky.js @@ -0,0 +1,21 @@ +var DESCRIPTORS = require('../internals/descriptors'); +var UNSUPPORTED_Y = require('../internals/regexp-sticky-helpers').UNSUPPORTED_Y; +var defineProperty = require('../internals/object-define-property').f; +var getInternalState = require('../internals/internal-state').get; +var RegExpPrototype = RegExp.prototype; + +// `RegExp.prototype.sticky` getter +if (DESCRIPTORS && UNSUPPORTED_Y) { + defineProperty(RegExp.prototype, 'sticky', { + configurable: true, + get: function () { + if (this === RegExpPrototype) return undefined; + // We can't use InternalStateModule.getterFor because + // we don't add metadata for regexps created by a literal. + if (this instanceof RegExp) { + return !!getInternalState(this).sticky; + } + throw TypeError('Incompatible receiver, RegExp required'); + } + }); +} diff --git a/packages/core-js/modules/es.string.replace.js b/packages/core-js/modules/es.string.replace.js index 42d37af7ae78..11e9baaa1abb 100644 --- a/packages/core-js/modules/es.string.replace.js +++ b/packages/core-js/modules/es.string.replace.js @@ -19,7 +19,7 @@ var maybeToString = function (it) { }; // @@replace logic -fixRegExpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, maybeCallNative) { +fixRegExpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, maybeCallNative, reason) { return [ // `String.prototype.replace` method // https://tc39.github.io/ecma262/#sec-string.prototype.replace @@ -33,8 +33,14 @@ fixRegExpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, ma // `RegExp.prototype[@@replace]` method // https://tc39.github.io/ecma262/#sec-regexp.prototype-@@replace function (regexp, replaceValue) { - var res = maybeCallNative(nativeReplace, regexp, this, replaceValue); - if (res.done) return res.value; + if ( + reason.REPLACE_KEEPS_$0 || ( + typeof replaceValue === 'string' && replaceValue.indexOf('$0') === -1 + ) + ) { + var res = maybeCallNative(nativeReplace, regexp, this, replaceValue); + if (res.done) return res.value; + } var rx = anObject(regexp); var S = String(this); diff --git a/tests/compat/tests.js b/tests/compat/tests.js index bbff66fd25a3..cc0fa5f75192 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -733,18 +733,26 @@ GLOBAL.tests = { && RegExp(re1) === re1 && RegExp(re2) !== re2 && RegExp(re1, 'i') == '/a/i' + && new RegExp('a', 'y') // just check that it doesn't throw && RegExp[Symbol.species]; }, 'es.regexp.exec': function () { var re1 = /a/; var re2 = /b*/g; + var reSticky = new RegExp('a', 'y'); re1.exec('a'); re2.exec('a'); return re1.lastIndex === 0 && re2.lastIndex === 0 - && /()??/.exec('')[1] === undefined; + && /()??/.exec('')[1] === undefined + && reSticky.exec('abc')[0] === 'a' + && reSticky.exec('abc') === null + && (reSticky.lastIndex = 1, reSticky.exec('bac')[0] === 'a'); }, 'es.regexp.flags': function () { - return /./g.flags === 'g'; + return /./g.flags === 'g' && new RegExp('a', 'y').flags === 'y'; + }, + 'es.regexp.sticky': function () { + return new RegExp('a', 'y').sticky === true; }, 'es.regexp.to-string': function () { return RegExp.prototype.toString.call({ source: 'a', flags: 'b' }) === '/a/b' diff --git a/tests/tests/es.regexp.exec.js b/tests/tests/es.regexp.exec.js index fd9222a01373..cd4b8d5e1969 100644 --- a/tests/tests/es.regexp.exec.js +++ b/tests/tests/es.regexp.exec.js @@ -1,3 +1,5 @@ +import { DESCRIPTORS } from '../helpers/constants'; + QUnit.test('RegExp#exec lastIndex updating', assert => { let re = /b/; assert.strictEqual(re.lastIndex, 0, '.lastIndex starts at 0 for non-global regexps'); @@ -26,3 +28,35 @@ QUnit.test('RegExp#exec capturing groups', assert => { // #replace, but here also #replace is buggy :( // assert.deepEqual(/(a?)?/.exec('x'), ['', undefined], '/(a?)?/.exec("x") returns ["", undefined]'); }); + +if (DESCRIPTORS) { + QUnit.test('RegExp#exec sticky', assert => { + const re = new RegExp('a', 'y'); + const str = 'bbabaab'; + assert.strictEqual(re.lastIndex, 0, '#1'); + + assert.strictEqual(re.exec(str), null, '#2'); + assert.strictEqual(re.lastIndex, 0, '#3'); + + re.lastIndex = 1; + assert.strictEqual(re.exec(str), null, '#4'); + assert.strictEqual(re.lastIndex, 0, '#5'); + + re.lastIndex = 2; + assert.deepEqual(re.exec(str), ['a'], '#6'); + assert.strictEqual(re.lastIndex, 3, '#7'); + + assert.strictEqual(re.exec(str), null, '#8'); + assert.strictEqual(re.lastIndex, 0, '#9'); + + re.lastIndex = 4; + assert.deepEqual(re.exec(str), ['a'], '#10'); + assert.strictEqual(re.lastIndex, 5, '#11'); + + assert.deepEqual(re.exec(str), ['a'], '#12'); + assert.strictEqual(re.lastIndex, 6, '#13'); + + assert.strictEqual(re.exec(str), null, '#14'); + assert.strictEqual(re.lastIndex, 0, '#15'); + }); +} diff --git a/tests/tests/es.regexp.sticky.js b/tests/tests/es.regexp.sticky.js new file mode 100644 index 000000000000..fe6ab369e7b6 --- /dev/null +++ b/tests/tests/es.regexp.sticky.js @@ -0,0 +1,36 @@ +import { DESCRIPTORS } from '../helpers/constants'; + +if (DESCRIPTORS) { + QUnit.test('RegExp#sticky', assert => { + const re = new RegExp('a', 'y'); + assert.strictEqual(re.sticky, true, '.sticky is true'); + assert.strictEqual(re.flags, 'y', '.flags contains y'); + assert.strictEqual(/a/.sticky, false); + + const stickyGetter = Object.getOwnPropertyDescriptor(RegExp.prototype, 'sticky').get; + if (typeof stickyGetter === 'function') { + // Old firefox versions set a non-configurable non-writable .sticky property + // It works correctly, but it isn't a getter and it can't be polyfilled. + // We need to skip these tests. + + assert.throws(() => { + stickyGetter.call({}); + }, undefined, '.sticky getter can only be called on RegExp instances'); + try { + stickyGetter.call(/a/); + assert.ok(true, '.sticky getter works on literals'); + } catch (error) { + assert.ok(false, '.sticky getter works on literals'); + } + try { + stickyGetter.call(new RegExp('a')); + assert.ok(true, '.sticky getter works on instances'); + } catch (error) { + assert.ok(false, '.sticky getter works on instances'); + } + + assert.ok(Object.hasOwnProperty.call(RegExp.prototype, 'sticky'), 'prototype has .sticky property'); + assert.strictEqual(RegExp.prototype.sticky, undefined, '.sticky is undefined on prototype'); + } + }); +} diff --git a/tests/tests/index.js b/tests/tests/index.js index 10f27e54b7fc..dfca4cad3f06 100644 --- a/tests/tests/index.js +++ b/tests/tests/index.js @@ -112,6 +112,7 @@ import './es.reflect.set'; import './es.regexp.constructor'; import './es.regexp.exec'; import './es.regexp.flags'; +import './es.regexp.sticky'; import './es.regexp.to-string'; import './es.set'; import './es.string.anchor';