Skip to content

Commit

Permalink
Sticky RegExp 'y' and RegExp#test delegation (#732)
Browse files Browse the repository at this point in the history
Sticky RegExp 'y' and RegExp#test delegation, close #372, close #492
  • Loading branch information
zloirock authored Dec 17, 2019
2 parents 62716b6 + 35e400f commit 80d7bfe
Show file tree
Hide file tree
Showing 17 changed files with 318 additions and 25 deletions.
17 changes: 14 additions & 3 deletions packages/core-js-compat/src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/core-js/es/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions packages/core-js/es/regexp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
5 changes: 5 additions & 0 deletions packages/core-js/es/regexp/sticky.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require('../../modules/es.regexp.sticky');

module.exports = function (it) {
return it.sticky;
};
15 changes: 11 additions & 4 deletions packages/core-js/internals/fix-regexp-well-known-symbol-logic.js
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -20,6 +20,12 @@ var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () {
return ''.replace(re, '$<a>') !== '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 () {
Expand Down Expand Up @@ -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];
Expand All @@ -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];

Expand All @@ -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);
};
41 changes: 37 additions & 4 deletions packages/core-js/internals/regexp-exec.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand Down
23 changes: 23 additions & 0 deletions packages/core-js/internals/regexp-sticky-helpers.js
Original file line number Diff line number Diff line change
@@ -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;
});
39 changes: 31 additions & 8 deletions packages/core-js/modules/es.regexp.constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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';
Expand All @@ -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, {
Expand Down
3 changes: 2 additions & 1 deletion packages/core-js/modules/es.regexp.flags.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 21 additions & 0 deletions packages/core-js/modules/es.regexp.sticky.js
Original file line number Diff line number Diff line change
@@ -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');
}
});
}
28 changes: 28 additions & 0 deletions packages/core-js/modules/es.regexp.test.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
12 changes: 9 additions & 3 deletions packages/core-js/modules/es.string.replace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
Expand Down
23 changes: 21 additions & 2 deletions tests/compat/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading

0 comments on commit 80d7bfe

Please sign in to comment.