blob: f40d5b755bdf25bfe1157b7d1cc99e0c0fc01f11 [file] [edit]
// Coverage for RegExp.prototype[Symbol.split] now that it is implemented in C++.
// Tests in this file should crash or throw on failure; passing tests must produce no output.
function shouldBe(actual, expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected))
throw new Error("Expected " + JSON.stringify(expected) + " but got " + JSON.stringify(actual));
}
function shouldThrow(fn, ctor) {
let threw = false;
try { fn(); } catch (e) {
threw = true;
if (ctor && !(e instanceof ctor))
throw new Error("Expected " + ctor.name + ", got " + e);
}
if (!threw)
throw new Error("Expected throw");
}
const splitFn = RegExp.prototype[Symbol.split];
shouldBe(splitFn.length, 2);
shouldBe(splitFn.name, "[Symbol.split]");
shouldBe(Object.getOwnPropertyDescriptor(RegExp.prototype, Symbol.split).enumerable, false);
// Step 1-2: receiver must be Object.
shouldThrow(() => splitFn.call(undefined, "a"), TypeError);
shouldThrow(() => splitFn.call(null, "a"), TypeError);
shouldThrow(() => splitFn.call(42, "a"), TypeError);
shouldThrow(() => splitFn.call("not an object", "a"), TypeError);
// Step 3: ToString called on the input.
{
let calls = 0;
const arg = { toString() { calls++; return "x,y,z"; } };
shouldBe(splitFn.call(/,/, arg), ["x", "y", "z"]);
shouldBe(calls, 1);
}
// Step 4: SpeciesConstructor — null/undefined fall back to %RegExp%, function species (non-constructor) throws,
// and a custom constructor is invoked with (rx, newFlags).
{
class WithNullSpecies extends RegExp {
static get [Symbol.species]() { return null; }
}
shouldBe(splitFn.call(new WithNullSpecies(/,/), "a,b"), ["a", "b"]);
class WithUndefinedSpecies extends RegExp {
static get [Symbol.species]() { return undefined; }
}
shouldBe(splitFn.call(new WithUndefinedSpecies(/,/), "p,q,r"), ["p", "q", "r"]);
const r = /,/;
r.constructor = { [Symbol.species]: () => null };
shouldThrow(() => splitFn.call(r, "a,b"), TypeError);
let species;
let speciesArgs;
class CustomSpecies extends RegExp {
static get [Symbol.species]() {
return species;
}
}
species = function CustomCtor(rx, newFlags) {
speciesArgs = [rx, newFlags];
// Forward to RegExp so we still get a usable splitter.
return new RegExp(rx, newFlags);
};
splitFn.call(new CustomSpecies(/,/, "g"), "a,b");
if (!(speciesArgs[0] instanceof RegExp))
throw new Error("species: rx not RegExp");
if (!speciesArgs[1].includes("y"))
throw new Error("species: newFlags must contain 'y', got " + speciesArgs[1]);
}
// Step 5: ToString called on regexp.flags.
{
let calls = 0;
const r = /,/;
Object.defineProperty(r, "flags", {
get() { calls++; return ""; }
});
splitFn.call(r, "a,b,c");
if (calls < 1)
throw new Error("flags getter not called");
}
// Step 5: an exception in flags propagates.
{
const r = /,/;
Object.defineProperty(r, "flags", {
get() { throw new Error("flags-bang"); }
});
try {
splitFn.call(r, "a,b,c");
throw new Error("should throw");
} catch (e) {
if (e.message !== "flags-bang")
throw new Error("expected flags-bang, got " + e);
}
}
// Step 6: 'u' or 'v' triggers unicode AdvanceStringIndex (surrogate pair stays together).
{
const surrogate = "a😀b";
shouldBe(splitFn.call(/(?:)/u, surrogate), ["a", "😀", "b"]);
shouldBe(splitFn.call(/(?:)/v, surrogate), ["a", "😀", "b"]);
// Without u/v the surrogate is split at the code-unit boundary.
shouldBe(splitFn.call(/(?:)/, surrogate), ["a", "\uD83D", "\uDE00", "b"]);
}
// Step 7: 'y' is added to flags if missing — triggers sticky exec on the splitter.
// We can't observe newFlags directly without species, but a sticky no-match means split returns the whole string.
{
// /a/ matches at any offset, but the splitter has /ay/, which from position 0 in "ba" doesn't match.
// (Our advanceStringIndex still moves past 'b', then the next attempt at 1 matches 'a'.)
shouldBe(splitFn.call(/a/, "ba"), ["b", ""]);
}
// Step 8 (Construct splitter) propagates errors.
{
class BadCtor extends RegExp {
constructor() { throw new Error("ctor-bang"); }
static get [Symbol.species]() { return BadCtor; }
}
try {
// Use a primordial RegExp that will go through species path because flags getter is overridden.
const r = /,/;
Object.setPrototypeOf(r, BadCtor.prototype);
r.constructor = BadCtor;
splitFn.call(r, "a,b");
throw new Error("should throw");
} catch (e) {
if (e.message !== "ctor-bang")
throw new Error("expected ctor-bang, got " + e);
}
}
// Step 11 / 12: limit handling.
shouldBe(splitFn.call(/,/, "a,b,c", 0), []);
shouldBe(splitFn.call(/,/, "a,b,c", 1), ["a"]);
shouldBe(splitFn.call(/,/, "a,b,c", 2), ["a", "b"]);
shouldBe(splitFn.call(/,/, "a,b,c", undefined), ["a", "b", "c"]);
// ToUint32(-1) wraps to 0xFFFFFFFF, so the limit is effectively unlimited.
shouldBe(splitFn.call(/,/, "a,b,c", -1), ["a", "b", "c"]);
{
let calls = 0;
const limit = { valueOf() { calls++; return 2; } };
shouldBe(splitFn.call(/,/, "a,b,c", limit), ["a", "b"]);
shouldBe(calls, 1);
}
// limit value coerced to UInt32 — ToUint32(2.7) = 2.
shouldBe(splitFn.call(/,/, "a,b,c,d", 2.7), ["a", "b"]);
// Limit equal to total parts.
shouldBe(splitFn.call(/,/, "a,b,c", 3), ["a", "b", "c"]);
// Limit larger than total parts.
shouldBe(splitFn.call(/,/, "a,b,c", 100), ["a", "b", "c"]);
// Step 13: empty string.
shouldBe(splitFn.call(/x/, ""), [""]); // exec returns null.
shouldBe(splitFn.call(/(?:)/, ""), []); // exec returns a match.
// Step 17 capture-group cases.
shouldBe(splitFn.call(/(b)/, "abc"), ["a", "b", "c"]);
shouldBe(splitFn.call(/(d)?b/, "abc"), ["a", undefined, "c"]);
shouldBe(splitFn.call(/(?<x>b)/, "abc"), ["a", "b", "c"]);
shouldBe(splitFn.call(/(b)(c)/, "abc"), ["a", "b", "c", ""]);
// Step 17.d.iii: e == p case — match at p with zero-length consumes nothing, advance by one.
shouldBe(splitFn.call(/(?=,)/, "a,b,c"), ["a", ",b", ",c"]);
shouldBe(splitFn.call(/(?:)/, "abc"), ["a", "b", "c"]);
// Custom exec via subclass: must round-trip results through user-facing array protocol.
{
let execCalls = 0;
class TraceExec extends RegExp {
exec(s) {
execCalls++;
return RegExp.prototype.exec.call(this, s);
}
}
const re = new TraceExec(/,/);
shouldBe(splitFn.call(re, "a,b,c"), ["a", "b", "c"]);
if (execCalls < 3)
throw new Error("exec must be invoked at least once per non-final segment, got " + execCalls);
}
// exec returning non-null, non-Object throws TypeError.
{
class BadExec extends RegExp {
exec() { return 42; }
}
shouldThrow(() => splitFn.call(new BadExec(/,/), "a,b"), TypeError);
}
// exec returning null forwards to AdvanceStringIndex; should consume the whole string into a single segment.
{
class NullExec extends RegExp {
exec() { return null; }
}
shouldBe(splitFn.call(new NullExec(/,/), "a,b,c"), ["a,b,c"]);
}
// Step 17.d.i: ToLength(splitter.lastIndex) — receiver-controlled lastIndex, clamped to size.
{
class LastIndexHack extends RegExp {
exec(s) {
const r = RegExp.prototype.exec.call(this, s);
if (r) this.lastIndex = 1000; // out of bounds, clamps to size.
return r;
}
}
// After the first match, e clamps to size, so p jumps to size and the trailing "" is pushed.
shouldBe(splitFn.call(new LastIndexHack(/,/), "a,b,c"), ["a", ""]);
}
// Step 17.d.ii: large captures should all be returned, capped at limit.
{
class FakeArray extends RegExp {
exec(s) {
const r = RegExp.prototype.exec.call(this, s);
if (!r) return null;
r.length = 4;
r[1] = "X";
r[2] = "Y";
r[3] = "Z";
return r;
}
}
shouldBe(splitFn.call(new FakeArray(/,/), "a,b"), ["a", "X", "Y", "Z", "b"]);
shouldBe(splitFn.call(new FakeArray(/,/), "a,b", 3), ["a", "X", "Y"]);
shouldBe(splitFn.call(new FakeArray(/,/), "a,b", 4), ["a", "X", "Y", "Z"]);
}
// Sticky source: 'y' already present is preserved (no double 'y').
{
let observedFlags;
class SeeFlags extends RegExp {
get flags() {
observedFlags = super.flags;
return observedFlags;
}
}
splitFn.call(new SeeFlags(/a/y), "ba");
if (observedFlags !== "y")
throw new Error("Expected y flag preserved, got " + observedFlags);
}
// String.prototype.split(regexp) — sanity check that the C++ split is reachable through the
// String.prototype.split bridge (already tested elsewhere, but include a smoke check here).
shouldBe("a,b,c".split(/,/), ["a", "b", "c"]);
shouldBe("a,b,c".split(/,/, 1), ["a"]);
// Also check that the fast path triggers (instanceof RegExp + plain limit).
{
const re = /,/;
for (let i = 0; i < 10000; i++)
re[Symbol.split]("a,b,c,d,e,f,g");
}