Source code
Revision control
Copy as Markdown
Other Tools
// RegExp.prototype[Symbol.match, Symbol.replace]: Test lastIndex changes for ES2017.
// RegExp-like class to test the RegExp method slow paths.
class DuckRegExp extends RegExp {
constructor(pattern, flags) {
return Object.create(DuckRegExp.prototype, {
regExp: {
value: new RegExp(pattern, flags)
},
lastIndex: {
value: 0, writable: true, enumerable: false, configurable: false
}
});
}
exec(...args) {
this.regExp.lastIndex = this.lastIndex;
try {
return this.regExp.exec(...args);
} finally {
if (this.global || this.sticky)
this.lastIndex = this.regExp.lastIndex;
}
}
get source() { return this.regExp.source; }
get flags() { return this.regExp.flags; }
get global() { return this.regExp.global; }
get ignoreCase() { return this.regExp.ignoreCase; }
get multiline() { return this.regExp.multiline; }
get sticky() { return this.regExp.sticky; }
get unicode() { return this.regExp.unicode; }
}
// Test various combinations of:
// - Pattern matches or doesn't match
// - Global and/or sticky flag is set.
// - lastIndex exceeds the input string length
// - lastIndex is +-0
const testCases = [
{ regExp: /a/, lastIndex: 0, input: "a", result: 0 },
{ regExp: /a/g, lastIndex: 0, input: "a", result: 0 },
{ regExp: /a/y, lastIndex: 0, input: "a", result: 1 },
{ regExp: /a/, lastIndex: 0, input: "b", result: 0 },
{ regExp: /a/g, lastIndex: 0, input: "b", result: 0 },
{ regExp: /a/y, lastIndex: 0, input: "b", result: 0 },
{ regExp: /a/, lastIndex: -0, input: "a", result: -0 },
{ regExp: /a/g, lastIndex: -0, input: "a", result: 0 },
{ regExp: /a/y, lastIndex: -0, input: "a", result: 1 },
{ regExp: /a/, lastIndex: -0, input: "b", result: -0 },
{ regExp: /a/g, lastIndex: -0, input: "b", result: 0 },
{ regExp: /a/y, lastIndex: -0, input: "b", result: 0 },
{ regExp: /a/, lastIndex: -1, input: "a", result: -1 },
{ regExp: /a/g, lastIndex: -1, input: "a", result: 0 },
{ regExp: /a/y, lastIndex: -1, input: "a", result: 1 },
{ regExp: /a/, lastIndex: -1, input: "b", result: -1 },
{ regExp: /a/g, lastIndex: -1, input: "b", result: 0 },
{ regExp: /a/y, lastIndex: -1, input: "b", result: 0 },
{ regExp: /a/, lastIndex: 100, input: "a", result: 100 },
{ regExp: /a/g, lastIndex: 100, input: "a", result: 0 },
{ regExp: /a/y, lastIndex: 100, input: "a", result: 0 },
];
for (let method of [RegExp.prototype[Symbol.match], RegExp.prototype[Symbol.replace]]) {
for (let Constructor of [RegExp, DuckRegExp]) {
// Basic test.
for (let {regExp, lastIndex, input, result} of testCases) {
let re = new Constructor(regExp);
re.lastIndex = lastIndex;
Reflect.apply(method, re, [input]);
assertEq(re.lastIndex, result);
}
// Test when lastIndex is non-writable.
for (let {regExp, lastIndex, input} of testCases) {
let re = new Constructor(regExp);
Object.defineProperty(re, "lastIndex", { value: lastIndex, writable: false });
if (re.global || re.sticky) {
assertThrowsInstanceOf(() => Reflect.apply(method, re, [input]), TypeError);
} else {
Reflect.apply(method, re, [input]);
}
assertEq(re.lastIndex, lastIndex);
}
// Test when lastIndex is changed to non-writable as a side-effect.
for (let {regExp, lastIndex, input, result} of testCases) {
let re = new Constructor(regExp);
let called = false;
re.lastIndex = {
valueOf() {
assertEq(called, false);
called = true;
Object.defineProperty(re, "lastIndex", { value: 9000, writable: false });
return lastIndex;
}
};
if (re.sticky) {
assertThrowsInstanceOf(() => Reflect.apply(method, re, [input]), TypeError);
assertEq(called, true);
assertEq(re.lastIndex, 9000);
} else if (re.global) {
Reflect.apply(method, re, [input]);
assertEq(called, false);
assertEq(re.lastIndex, result);
} else {
Reflect.apply(method, re, [input]);
assertEq(called, true);
assertEq(re.lastIndex, 9000);
}
}
}
}
if (typeof reportCompare === "function")
reportCompare(true, true);