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

Add support for the 'y' flag to the RegExp constructor #492

Closed
wants to merge 11 commits into from
6 changes: 6 additions & 0 deletions packages/core-js-compat/src/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,12 @@ module.exports = {
firefox: '37',
safari: '9.0',
},
'es.regexp.sticky': {
chrome: '49',
edge: '13',
firefox: '3',
safari: '10.0',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I got this data from MDN. Note that on Firefox 3 the y flags doesn't correctly work with regexp methods, but this compat entry is only about the .sticky accessor.

Copy link
Owner

Choose a reason for hiding this comment

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

We don't need data for modern opera - it's generated from chrome.

},
'es.regexp.to-string': {
chrome: '50',
firefox: '46',
Expand Down
1 change: 1 addition & 0 deletions packages/core-js/es/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,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');
Expand Down
1 change: 1 addition & 0 deletions packages/core-js/es/regexp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
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;
};
14 changes: 9 additions & 5 deletions packages/core-js/internals/fix-regexp-well-known-symbol-logic.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict';
var hide = require('../internals/hide');
var redefine = require('../internals/redefine');
var fails = require('../internals/fails');
var wellKnownSymbol = require('../internals/well-known-symbol');
Expand All @@ -20,6 +19,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 () {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you prefer I can move this fix to a separate PR. I fixed it here just because I saw the tests failing on IE11 and it is related to RegExps.

Copy link
Owner

Choose a reason for hiding this comment

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

Nope, it's fine 👍

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 All @@ -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 () {
Expand Down Expand Up @@ -60,7 +65,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 @@ -75,7 +80,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 @@ -88,6 +93,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) hide(RegExp.prototype[SYMBOL], 'sham', true);
}
};
24 changes: 21 additions & 3 deletions packages/core-js/internals/regexp-exec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'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 @@ -18,24 +19,41 @@ 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) {
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;
});
42 changes: 34 additions & 8 deletions packages/core-js/modules/es.regexp.constructor.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
var DESCRIPTORS = require('../internals/descriptors');
var MATCH = require('../internals/well-known-symbol')('match');
var hide = require('../internals/hide');
var global = require('../internals/global');
var isForced = require('../internals/is-forced');
var inheritIfRequired = require('../internals/inherit-if-required');
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 NativeRegExp = global.RegExp;
var RegExpPrototype = NativeRegExp.prototype;
var re1 = /a/g;
Expand All @@ -17,7 +20,9 @@ var re2 = /a/g;
// "new" should create a new object, old webkit bug
var CORRECT_NEW = new NativeRegExp(re1) !== re1;

var FORCED = isForced('RegExp', DESCRIPTORS && (!CORRECT_NEW || fails(function () {
var UNSUPPORTED_Y = stickyHelpers.UNSUPPORTED_Y;

var FORCED = isForced('RegExp', DESCRIPTORS && (!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 @@ -30,13 +35,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 All @@ -51,6 +75,8 @@ if (FORCED) {
RegExpPrototype.constructor = RegExpWrapper;
RegExpWrapper.prototype = RegExpPrototype;
redefine(global, 'RegExp', RegExpWrapper);

if (UNSUPPORTED_Y) hide(RegExpWrapper, 'sham', true);
}

// https://tc39.github.io/ecma262/#sec-get-regexp-@@species
Expand Down
7 changes: 6 additions & 1 deletion packages/core-js/modules/es.regexp.flags.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
// `RegExp.prototype.flags` getter
// https://tc39.github.io/ecma262/#sec-get-regexp.prototype.flags
if (require('../internals/descriptors') && /./g.flags != 'g') {
if (
require('../internals/descriptors') && (
/./g.flags != 'g' ||
require('../internals/regexp-sticky-helpers').UNSUPPORTED_Y
)
) {
require('../internals/object-define-property').f(RegExp.prototype, 'flags', {
configurable: true,
get: require('../internals/regexp-flags')
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');
}
});
}
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 @@ -21,7 +21,7 @@ var maybeToString = function (it) {
require('../internals/fix-regexp-well-known-symbol-logic')(
'replace',
2,
function (REPLACE, nativeReplace, maybeCallNative) {
function (REPLACE, nativeReplace, maybeCallNative, reason) {
return [
// `String.prototype.replace` method
// https://tc39.github.io/ecma262/#sec-string.prototype.replace
Expand All @@ -35,8 +35,14 @@ require('../internals/fix-regexp-well-known-symbol-logic')(
// `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
19 changes: 6 additions & 13 deletions packages/core-js/modules/es.string.split.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,10 @@ var advanceStringIndex = require('../internals/advance-string-index');
var toLength = require('../internals/to-length');
var callRegExpExec = require('../internals/regexp-exec-abstract');
var regexpExec = require('../internals/regexp-exec');
var fails = require('../internals/fails');
var arrayPush = [].push;
var min = Math.min;
var MAX_UINT32 = 0xffffffff;

// babel-minify transpiles RegExp('x', 'y') -> /x/y and it causes SyntaxError
var SUPPORTS_Y = !fails(function () { return !RegExp(MAX_UINT32, 'y'); });

// @@split logic
require('../internals/fix-regexp-well-known-symbol-logic')(
'split',
Copy link
Owner

Choose a reason for hiding this comment

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

In internalSplit, we have possible RegExp call with y flag.

Expand Down Expand Up @@ -99,24 +95,22 @@ require('../internals/fix-regexp-well-known-symbol-logic')(
var flags = (rx.ignoreCase ? 'i' : '') +
(rx.multiline ? 'm' : '') +
(rx.unicode ? 'u' : '') +
(SUPPORTS_Y ? 'y' : 'g');
Copy link
Owner

Choose a reason for hiding this comment

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

!

'y';

// ^(? + 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 splitter = new C(rx, flags);
var lim = limit === undefined ? MAX_UINT32 : limit >>> 0;
if (lim === 0) return [];
if (S.length === 0) return callRegExpExec(splitter, S) === null ? [S] : [];
var p = 0;
var q = 0;
var A = [];
while (q < S.length) {
splitter.lastIndex = SUPPORTS_Y ? q : 0;
var z = callRegExpExec(splitter, SUPPORTS_Y ? S : S.slice(q));
splitter.lastIndex = q;
var z = callRegExpExec(splitter, S);
var e;
if (
z === null ||
(e = min(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p
(e = min(toLength(splitter.lastIndex), S.length)) === p
) {
q = advanceStringIndex(S, q, unicodeMatching);
} else {
Expand All @@ -133,6 +127,5 @@ require('../internals/fix-regexp-well-known-symbol-logic')(
return A;
}
];
},
!SUPPORTS_Y
}
);
7 changes: 7 additions & 0 deletions tests/compat/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,13 @@ GLOBAL.tests = {
'es.regexp.flags': function () {
Copy link
Owner

@zloirock zloirock Mar 13, 2019

Choose a reason for hiding this comment

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

Seems we should also update this and RegExp constructor tests and results, maybe something else.

return /./g.flags === 'g';
},
'es.regexp.sticky': function () {
try {
return new RegExp('a', 'y').sticky === true;
} catch (e) {
return false;
}
Copy link
Owner

Choose a reason for hiding this comment

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

We don't need try / catch here - we have it in tests runner.

},
'es.regexp.to-string': function () {
return RegExp.prototype.toString.call({ source: 'a', flags: 'b' }) === '/a/b'
&& RegExp.prototype.toString.name === 'toString';
Expand Down
Loading