diff --git a/packages/core-js-compat/src/data.js b/packages/core-js-compat/src/data.js index b41981a4bcb8..ffdea38bbe0c 100644 --- a/packages/core-js-compat/src/data.js +++ b/packages/core-js-compat/src/data.js @@ -806,15 +806,26 @@ const data = { }, 'es.regexp.exec': { chrome: '26', - firefox: '4', - ie: '9', - safari: '8.0', + firefox: '44', + edge: '13', + safari: '10.0', }, 'es.regexp.flags': { chrome: '49', firefox: '37', safari: '9.0', }, + 'es.regexp.sticky': { + chrome: '49', + edge: '13', + firefox: '3', + safari: '10.0', + }, + 'es.regexp.test': { + chrome: '51', + firefox: '46', + 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..9436a3787280 100644 --- a/packages/core-js/es/index.js +++ b/packages/core-js/es/index.js @@ -105,6 +105,8 @@ 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.test'); 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..92c170a026c9 100644 --- a/packages/core-js/es/regexp/index.js +++ b/packages/core-js/es/regexp/index.js @@ -2,6 +2,8 @@ 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.regexp.test'); 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..8b523726e949 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,9 +1,9 @@ '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'); var regexpExec = require('../internals/regexp-exec'); +var createNonEnumerableProperty = require('../internals/create-non-enumerable-property'); var SPECIES = wellKnownSymbol('species'); @@ -20,6 +20,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 () { @@ -67,7 +73,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 +88,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 +101,7 @@ 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); } + + 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..1dee69fbae0e 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,56 @@ 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; + var flags = regexpFlags.call(re); + var source = re.source; + var charsAdded = 0; + var strCopy = str; + + if (sticky) { + flags = flags.replace('y', ''); + if (flags.indexOf('g') === -1) { + flags += 'g'; + } + + strCopy = String(str).slice(re.lastIndex); + // Support anchored sticky behavior. + if (re.lastIndex > 0 && (!re.multiline || re.multiline && str[re.lastIndex - 1] !== '\n')) { + source = '(?: ' + source + ')'; + strCopy = ' ' + strCopy; + charsAdded++; + } + // ^(? + rx + ) is needed, in combination with some str slicing, to + // simulate the 'y' flag. + reCopy = new RegExp('^(?:' + source + ')', flags); + } if (NPCG_INCLUDED) { - reCopy = new RegExp('^' + re.source + '$(?!\\s)', regexpFlags.call(re)); + reCopy = new RegExp('^' + source + '$(?!\\s)', flags); } if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex; - match = nativeExec.call(re, str); + match = nativeExec.call(sticky ? reCopy : re, strCopy); - if (UPDATES_LAST_INDEX_WRONG && match) { + if (sticky) { + if (match) { + match.input = match.input.slice(charsAdded); + match[0] = match[0].slice(charsAdded); + match.index = re.lastIndex; + 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..da7641bb79f7 --- /dev/null +++ b/packages/core-js/internals/regexp-sticky-helpers.js @@ -0,0 +1,23 @@ +'use strict'; + +var fails = require('./fails'); + +// 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; +}); diff --git a/packages/core-js/modules/es.regexp.constructor.js b/packages/core-js/modules/es.regexp.constructor.js index 05a3370f2994..a3d10529a6a7 100644 --- a/packages/core-js/modules/es.regexp.constructor.js +++ b/packages/core-js/modules/es.regexp.constructor.js @@ -6,8 +6,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 +22,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 +37,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, { 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.regexp.test.js b/packages/core-js/modules/es.regexp.test.js new file mode 100644 index 000000000000..07c38d9a6be8 --- /dev/null +++ b/packages/core-js/modules/es.regexp.test.js @@ -0,0 +1,28 @@ +'use strict'; +var $ = require('../internals/export'); +var isObject = require('../internals/is-object'); + +var DELEGATES_TO_EXEC = function () { + var execCalled = false; + var re = /[ac]/; + re.exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + return re.test('abc') === true && execCalled; +}(); + +var nativeTest = /./.test; + +$({ target: 'RegExp', proto: true, forced: !DELEGATES_TO_EXEC }, { + test: function (str) { + if (typeof this.exec !== 'function') { + return nativeTest.call(this, str); + } + var result = this.exec(str); + if (result !== null && !isObject(result)) { + throw new Error('RegExp exec method returned something other than an Object or null'); + } + return !!result; + } +}); 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..c2eb740d3847 100644 --- a/tests/compat/tests.js +++ b/tests/compat/tests.js @@ -733,18 +733,37 @@ 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'); + var reStickyAnchored = 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') + && (reStickyAnchored.lastIndex = 2, reStickyAnchored.exec('cba') === null); }, '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.test': function () { + var execCalled = false; + var re = /[ac]/; + re.exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + return re.test('abc') === true && execCalled; }, '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..7054c51c5cb5 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,52 @@ 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; + const result = re.exec(str); + assert.deepEqual(result, ['a'], '#6'); + assert.strictEqual(result.index, 2, '#7'); + assert.strictEqual(re.lastIndex, 3, '#8'); + + assert.strictEqual(re.exec(str), null, '#9'); + assert.strictEqual(re.lastIndex, 0, '#10'); + + re.lastIndex = 4; + assert.deepEqual(re.exec(str), ['a'], '#11'); + assert.strictEqual(re.lastIndex, 5, '#12'); + + assert.deepEqual(re.exec(str), ['a'], '#13'); + assert.strictEqual(re.lastIndex, 6, '#14'); + + assert.strictEqual(re.exec(str), null, '#15'); + assert.strictEqual(re.lastIndex, 0, '#16'); + }); + QUnit.test('RegExp#exec sticky anchored', assert => { + const regex = new RegExp('^foo', 'y'); + assert.deepEqual(regex.exec('foo'), ['foo'], '#1'); + regex.lastIndex = 2; + assert.strictEqual(regex.exec('..foo'), null, '#2'); + regex.lastIndex = 2; + assert.strictEqual(regex.exec('.\nfoo'), null, '#3'); + + const regex2 = new RegExp('^foo', 'my'); + regex2.lastIndex = 2; + assert.strictEqual(regex2.exec('..foo'), null, '#4'); + regex2.lastIndex = 2; + assert.deepEqual(regex2.exec('.\nfoo'), ['foo'], '#5'); + assert.strictEqual(regex2.lastIndex, 5, '#6'); + }); +} 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/es.regexp.test.js b/tests/tests/es.regexp.test.js new file mode 100644 index 000000000000..7841ac4eec45 --- /dev/null +++ b/tests/tests/es.regexp.test.js @@ -0,0 +1,23 @@ + +QUnit.test('RegExp#test delegates to exec', assert => { + const exec = function () { + execCalled = true; + return /./.exec.apply(this, arguments); + }; + + let execCalled = false; + let re = /[ac]/; + re.exec = exec; + assert.strictEqual(re.test('abc'), true, '#1'); + assert.ok(execCalled, '#2'); + + re = /a/; + // Not a function, should be ignored + re.exec = 3; + assert.strictEqual(re.test('abc'), true, '#3'); + + re = /a/; + // Does not return an object, should throw + re.exec = () => 3; + assert.throws(() => re.test('abc', '#4')); +}); diff --git a/tests/tests/index.js b/tests/tests/index.js index 10f27e54b7fc..9e1ac40ccbf7 100644 --- a/tests/tests/index.js +++ b/tests/tests/index.js @@ -112,6 +112,8 @@ import './es.reflect.set'; import './es.regexp.constructor'; import './es.regexp.exec'; import './es.regexp.flags'; +import './es.regexp.sticky'; +import './es.regexp.test'; import './es.regexp.to-string'; import './es.set'; import './es.string.anchor';