Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sticky RegExp 'y' and RegExp#test delegation #732

Merged
merged 5 commits into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
};
Copy link
Owner

@zloirock zloirock Dec 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also required stable/regexp/sticky, features/regexp/sticky, the same for RegExp#test and adding them to tests/commonjs.

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 });
Copy link
Owner

@zloirock zloirock Dec 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to set it only when sticky is true?


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