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
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 Down Expand Up @@ -30,7 +29,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 @@ -88,6 +87,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;
});
43 changes: 35 additions & 8 deletions packages/core-js/modules/es.regexp.constructor.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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 NativeRegExp = global.RegExp;
Expand All @@ -17,7 +19,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 +34,35 @@ 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) defineProperty(result, 'sticky', {
Copy link
Owner

Choose a reason for hiding this comment

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

It should be placed on the prototype. I'm not sure that it's 100% safe, but before users issues, we can try it.

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 we place it in the prototype, I'm not sure how we could check if the regexp is sticky.
Maybe I could add a __core-js_sticky__ own property and then make sticky a prototype accessor for that property? I don't want to use a weakmap since it would always need to be polyfilled.

Copy link
Owner

Choose a reason for hiding this comment

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

configurable: true,
get: function () { return sticky; }
});

return result;
};
var proxy = function (key) {
key in RegExpWrapper || defineProperty(RegExpWrapper, key, {
Expand All @@ -51,6 +77,7 @@ 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
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
}
);
6 changes: 6 additions & 0 deletions tests/tests/es.regexp.constructor.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,10 @@ if (DESCRIPTORS) {
assert.ok(new Subclass('^abc$').test('abc'), 'correct subclassing with native classes #3');
}
});

QUnit.test('RegExp sticky', assert => {
const re = new RegExp('a', 'y');
assert.ok(re.sticky, '.sticky is true');
assert.strictEqual(re.flags, 'y', '.flags contains y');
});
}
30 changes: 30 additions & 0 deletions tests/tests/es.regexp.exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,33 @@ 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]');
});

QUnit.test('RegExp#exec sticky', assert => {
const re = new RegExp('a', 'y');
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.

We do not polyfill RegExp constructor in IE8- since we can't add accessors. At least, this test should be ignored in engines without descriptors.

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'm also moving it to a separate file.

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');
});