Source code

Revision control

Copy as Markdown

Other Tools

// Basic surface tests.
assertEq(typeof String.prototype.matchAll, "function");
assertEq(String.prototype.matchAll.name, "matchAll");
assertEq(String.prototype.matchAll.length, 1);
assertEq(typeof Symbol.matchAll, "symbol");
assertEq(typeof RegExp.prototype[Symbol.matchAll], "function");
assertEq(RegExp.prototype[Symbol.matchAll].name, "[Symbol.matchAll]");
assertEq(RegExp.prototype[Symbol.matchAll].length, 1);
const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
const RegExpStringIteratorPrototype = Object.getPrototypeOf("".matchAll(""));
assertEq(Object.getPrototypeOf(RegExpStringIteratorPrototype), IteratorPrototype);
assertEq(typeof RegExpStringIteratorPrototype.next, "function");
assertEq(RegExpStringIteratorPrototype.next.name, "next");
assertEq(RegExpStringIteratorPrototype.next.length, 0);
assertEq(RegExpStringIteratorPrototype[Symbol.toStringTag], "RegExp String Iterator");
// Basic functional tests.
const RegExp_prototype_exec = RegExp.prototype.exec;
const RegExp_prototype_match = RegExp.prototype[Symbol.match];
function assertEqIterMatchResult(actual, expected) {
assertEq(actual.done, expected.done);
if (actual.value === undefined || expected.value === undefined) {
assertEq(actual.value, expected.value);
} else {
assertEqArray(actual.value, expected.value);
assertEq(actual.value.input, expected.value.input);
assertEq(actual.value.index, expected.value.index);
}
}
function assertEqMatchResults(actual, expected) {
var actualIter = actual[Symbol.iterator]();
var expectedIter = expected[Symbol.iterator]();
while (true) {
var actualResult = actualIter.next();
var expectedResult = expectedIter.next();
assertEqIterMatchResult(actualResult, expectedResult);
if (actualResult.done && expectedResult.done)
return;
}
}
function* matchResults(string, regexp, lastIndex = 0) {
regexp.lastIndex = lastIndex;
while (true) {
var match = Reflect.apply(RegExp_prototype_exec, regexp, [string]);
if (match === null)
return;
yield match;
if (!regexp.global)
return;
}
}
assertEqMatchResults(/a/[Symbol.matchAll]("ababcca"), matchResults("ababcca", /a/));
assertEqMatchResults("ababcca".matchAll(/a/g), matchResults("ababcca", /a/g));
assertEqMatchResults("ababcca".matchAll("a"), matchResults("ababcca", /a/g));
// Cross-compartment tests.
{
let otherGlobal = newGlobal();
let iterator = otherGlobal.eval(`"ababcca".matchAll(/a/g)`);
let expected = matchResults("ababcca", /a/g);
assertEqIterMatchResult(RegExpStringIteratorPrototype.next.call(iterator),
expected.next());
}
// Optimization tests.
//
// The optimized path for MatchAllIterator reuses the input RegExp to avoid
// extra RegExp allocations. To make this optimization undetectable from
// user code, we need to ensure that:
// 1. Modifications to the input RegExp through RegExp.prototype.compile are
// detected and properly handled (= ignored).
// 2. The RegExpStringIterator doesn't modify the input RegExp, for example
// by updating the lastIndex property.
// 3. Guards against modifications of built-in RegExp.prototype are installed.
// Recompile RegExp (source) before first match.
{
let regexp = /a+/g;
let iterator = "aabb".matchAll(regexp);
regexp.compile("b+", "g");
assertEqMatchResults(iterator, matchResults("aabb", /a+/g));
}
// Recompile RegExp (flags) before first match.
{
let regexp = /a+/gi;
let iterator = "aAbb".matchAll(regexp);
regexp.compile("a+", "");
assertEqMatchResults(iterator, matchResults("aAbb", /a+/gi));
}
// Recompile RegExp (source) after first match.
{
let regexp = /a+/g;
let iterator = "aabbaa".matchAll(regexp);
let expected = matchResults("aabbaa", /a+/g);
assertEqIterMatchResult(iterator.next(), expected.next());
regexp.compile("b+", "g");
assertEqIterMatchResult(iterator.next(), expected.next());
}
// Recompile RegExp (flags) after first match.
{
let regexp = /a+/g;
let iterator = "aabbAA".matchAll(regexp);
let expected = matchResults("aabbAA", /a+/g);
assertEqIterMatchResult(iterator.next(), expected.next());
regexp.compile("a+", "i");
assertEqIterMatchResult(iterator.next(), expected.next());
}
// lastIndex property of input RegExp not modified when optimized path used.
{
let regexp = /a+/g;
regexp.lastIndex = 1;
let iterator = "aabbaa".matchAll(regexp);
let expected = matchResults("aabbaa", /a+/g, 1);
assertEq(regexp.lastIndex, 1);
assertEqIterMatchResult(iterator.next(), expected.next());
assertEq(regexp.lastIndex, 1);
assertEqIterMatchResult(iterator.next(), expected.next());
assertEq(regexp.lastIndex, 1);
}
// Modifications to lastIndex property of input RegExp ignored when optimized path used.
{
let regexp = /a+/g;
let iterator = "aabbaa".matchAll(regexp);
regexp.lastIndex = 1;
let expected = matchResults("aabbaa", /a+/g);
assertEq(regexp.lastIndex, 1);
assertEqIterMatchResult(iterator.next(), expected.next());
assertEq(regexp.lastIndex, 1);
assertEqIterMatchResult(iterator.next(), expected.next());
assertEq(regexp.lastIndex, 1);
}
// RegExp.prototype[Symbol.match] is modified to a getter, ensure this getter
// is called exactly twice.
try {
let regexp = /a+/g;
let callCount = 0;
Object.defineProperty(RegExp.prototype, Symbol.match, {
get() {
assertEq(this, regexp);
callCount++;
return RegExp_prototype_match;
}
});
let iterator = "aabbaa".matchAll(regexp);
assertEq(callCount, 2);
} finally {
// Restore optimizable RegExp.prototype shape.
Object.defineProperty(RegExp.prototype, Symbol.match, {
value: RegExp_prototype_match,
writable: true, enumerable: false, configurable: true
});
}
// RegExp.prototype.exec is changed to a getter.
try {
let regexp = /a+/g;
let iterator = "aabbaa".matchAll(regexp);
let lastIndices = [0, 2, 6][Symbol.iterator]();
let iteratorRegExp = null;
let callCount = 0;
Object.defineProperty(RegExp.prototype, "exec", {
get() {
callCount++;
if (iteratorRegExp === null)
iteratorRegExp = this;
assertEq(this === regexp, false);
assertEq(this, iteratorRegExp);
assertEq(this.source, regexp.source);
assertEq(this.flags, regexp.flags);
assertEq(this.lastIndex, lastIndices.next().value);
return RegExp_prototype_exec;
}
});
assertEqMatchResults(iterator, matchResults("aabbaa", /a+/g));
assertEq(callCount, 3);
} finally {
// Restore optimizable RegExp.prototype shape.
Object.defineProperty(RegExp.prototype, "exec", {
value: RegExp_prototype_exec,
writable: true, enumerable: false, configurable: true
});
}
// RegExp.prototype.exec is changed to a value property.
try {
let regexp = /a+/g;
let iterator = "aabbaa".matchAll(regexp);
let lastIndices = [0, 2, 6][Symbol.iterator]();
let iteratorRegExp = null;
let callCount = 0;
RegExp.prototype.exec = function(...args) {
callCount++;
if (iteratorRegExp === null)
iteratorRegExp = this;
assertEq(this === regexp, false);
assertEq(this, iteratorRegExp);
assertEq(this.source, regexp.source);
assertEq(this.flags, regexp.flags);
assertEq(this.lastIndex, lastIndices.next().value);
return Reflect.apply(RegExp_prototype_exec, this, args);
};
assertEqMatchResults(iterator, matchResults("aabbaa", /a+/g));
assertEq(callCount, 3);
} finally {
// Restore optimizable RegExp.prototype shape.
Object.defineProperty(RegExp.prototype, "exec", {
value: RegExp_prototype_exec,
writable: true, enumerable: false, configurable: true
});
}
// Initial 'lastIndex' is zero if the RegExp is neither global nor sticky (1).
{
let regexp = /a+/;
regexp.lastIndex = 2;
let iterator = regexp[Symbol.matchAll]("aaaaa");
assertEqMatchResults(iterator, matchResults("aaaaa", /a+/g, 0));
}
// Initial 'lastIndex' is zero if the RegExp is neither global nor sticky (2).
{
let regexp = /a+/g;
regexp.lastIndex = 2;
let iterator = regexp[Symbol.matchAll]("aaaaa");
assertEqMatchResults(iterator, matchResults("aaaaa", /a+/g, 2));
}
// Initial 'lastIndex' is zero if the RegExp is neither global nor sticky (3).
{
let regexp = /a+/y;
regexp.lastIndex = 2;
let iterator = regexp[Symbol.matchAll]("aaaaa");
assertEqMatchResults(iterator, matchResults("aaaaa", /a+/g, 2));
}
//Initial 'lastIndex' is zero if the RegExp is neither global nor sticky (4).
{
let regexp = /a+/gy;
regexp.lastIndex = 2;
let iterator = regexp[Symbol.matchAll]("aaaaa");
assertEqMatchResults(iterator, matchResults("aaaaa", /a+/g, 2));
}
if (typeof reportCompare === "function")
reportCompare(true, true);