Add LibRegex's new Rust ECMAScript regular expression engine. Replace the old parser's direct pattern-to-bytecode pipeline with a split architecture: parse patterns into a lossless AST first, then lower that AST into bytecode for a dedicated backtracking VM. Keep the syntax tree as the place for validation, analysis, and optimization instead of teaching every transformation to rewrite partially built bytecode. Specialize this backend for the job LibJS actually needs. The old C++ engine shared one generic parser and matcher stack across ECMA-262 and POSIX modes and supported both byte-string and UTF-16 inputs. The new engine focuses on ECMA-262 semantics on WTF-16 data, which lets it model lone surrogates and other JavaScript-specific behavior directly instead of carrying POSIX and multi-encoding constraints through the whole implementation. Fill in the ECMAScript features needed to replace the old engine for real web workloads: Unicode properties and sets, lookahead and lookbehind, named groups and backreferences, modifier groups, string properties, large quantifiers, lone surrogates, and the parser and VM corner cases those features exercise. Reshape the runtime around compile-time pattern hints and a hotter VM loop. Pre-resolve Unicode properties, derive first-character, character-class, and simple-scan filters, extract safe trailing literals for anchored patterns, add literal and literal-alternation fast paths, and keep reusable scratch storage for registers, backtracking state, and modifier stacks. Teach `find_all` to stay inside one VM so global searches stop paying setup costs on every match. Make those shortcuts semantics-aware instead of merely fast. In Unicode mode, do not use literal fast paths for lone surrogates, since ECMA-262 must not let `/\ud83d/u` match inside a surrogate pair. Likewise, only derive end-anchor suffix hints when the suffix lies on every path to `Match`, so lookarounds and disjunctions cannot skip into a shared tail and produce false negatives. This commit lands the Rust crate, the C++ wrapper, the build integration, and the initial LibJS-side plumbing needed to exercise the new engine under real RegExp callers before removing the legacy backend.
606 lines
24 KiB
JavaScript
606 lines
24 KiB
JavaScript
describe("errors", () => {
|
||
test("invalid pattern", () => {
|
||
expect(() => {
|
||
RegExp("[");
|
||
}).toThrowWithMessage(SyntaxError, "RegExp compile error: Error during parsing of regular expression:");
|
||
});
|
||
|
||
test("invalid flag", () => {
|
||
expect(() => {
|
||
RegExp("", "x");
|
||
}).toThrowWithMessage(SyntaxError, "Invalid RegExp flag 'x'");
|
||
});
|
||
|
||
test("repeated flag", () => {
|
||
expect(() => {
|
||
RegExp("", "gg");
|
||
}).toThrowWithMessage(SyntaxError, "Repeated RegExp flag 'g'");
|
||
});
|
||
});
|
||
|
||
test("basic functionality", () => {
|
||
expect(RegExp().toString()).toBe("/(?:)/");
|
||
expect(RegExp(undefined).toString()).toBe("/(?:)/");
|
||
expect(RegExp("foo").toString()).toBe("/foo/");
|
||
expect(RegExp("foo", undefined).toString()).toBe("/foo/");
|
||
expect(RegExp("foo", "g").toString()).toBe("/foo/g");
|
||
expect(RegExp(undefined, "g").toString()).toBe("/(?:)/g");
|
||
});
|
||
|
||
test("regexp object as pattern parameter", () => {
|
||
expect(RegExp(/foo/).toString()).toBe("/foo/");
|
||
expect(RegExp(/foo/g).toString()).toBe("/foo/g");
|
||
expect(RegExp(/foo/g, "").toString()).toBe("/foo/");
|
||
expect(RegExp(/foo/g, "y").toString()).toBe("/foo/y");
|
||
|
||
var regex_like_object_without_flags = {
|
||
source: "foo",
|
||
[Symbol.match]: function () {},
|
||
};
|
||
expect(RegExp(regex_like_object_without_flags).toString()).toBe("/foo/");
|
||
expect(RegExp(regex_like_object_without_flags, "y").toString()).toBe("/foo/y");
|
||
|
||
var regex_like_object_with_flags = {
|
||
source: "foo",
|
||
flags: "g",
|
||
[Symbol.match]: function () {},
|
||
};
|
||
expect(RegExp(regex_like_object_with_flags).toString()).toBe("/foo/g");
|
||
expect(RegExp(regex_like_object_with_flags, "").toString()).toBe("/foo/");
|
||
expect(RegExp(regex_like_object_with_flags, "y").toString()).toBe("/foo/y");
|
||
});
|
||
|
||
test("regexp literals are re-useable", () => {
|
||
for (var i = 0; i < 2; ++i) {
|
||
const re = /test/;
|
||
expect(re.test("te")).toBeFalse();
|
||
expect(re.test("test")).toBeTrue();
|
||
}
|
||
});
|
||
|
||
test("Incorrectly escaped code units not converted to invalid patterns", () => {
|
||
const re = /[\⪾-\⫀]/;
|
||
expect(re.test("⫀")).toBeTrue();
|
||
expect(re.test("\\u2abe")).toBeFalse(); // ⫀ is \u2abe
|
||
});
|
||
|
||
test("regexp that always matches stops matching if it's past the end of the string instead of infinitely looping", () => {
|
||
const re = new RegExp("[\u200E]*", "gu");
|
||
expect("whf".match(re)).toEqual(["", "", "", ""]);
|
||
expect(re.lastIndex).toBe(0);
|
||
});
|
||
|
||
test("v flag should enable unicode mode", () => {
|
||
const re = new RegExp("a\\u{10FFFF}", "v");
|
||
expect(re.test("a\u{10FFFF}")).toBe(true);
|
||
});
|
||
|
||
test("v flag empty character classes", () => {
|
||
expect(/[]/v.test("a")).toBeFalse();
|
||
expect("a".match(/[^]/v)).toEqual(["a"]);
|
||
expect("\n".match(/[^]/v)).toEqual(["\n"]);
|
||
expect("foo".match(/[^]+?/v)).toEqual(["f"]);
|
||
});
|
||
|
||
test("parsing a large bytestring shouldn't crash", () => {
|
||
RegExp(new Uint8Array(0x40000));
|
||
});
|
||
|
||
test("Unicode non-ASCII matching", () => {
|
||
const cases = [
|
||
{ pattern: /é/u, match: "é", expected: ["é"] },
|
||
{ pattern: /é/, match: "é", expected: ["é"] },
|
||
{ pattern: /\u{61}/u, match: "a", expected: ["a"] },
|
||
{ pattern: /\u{61}/, match: "a", expected: null },
|
||
{ pattern: /😄/u, match: "😄", expected: ["😄"] },
|
||
{ pattern: /😄/u, match: "\ud83d", expected: null },
|
||
{ pattern: /😄/, match: "\ud83d", expected: null },
|
||
];
|
||
for (const test of cases) {
|
||
const result = test.match.match(test.pattern);
|
||
expect(result).toEqual(test.expected);
|
||
}
|
||
});
|
||
|
||
// https://github.com/tc39/test262/tree/main/test/built-ins/RegExp/unicodeSets/generated
|
||
test("Unicode properties of strings", () => {
|
||
const regexes = [
|
||
/\p{Basic_Emoji}/v,
|
||
/\p{Emoji_Keycap_Sequence}/v,
|
||
/\p{RGI_Emoji_Modifier_Sequence}/v,
|
||
/\p{RGI_Emoji_Flag_Sequence}/v,
|
||
/\p{RGI_Emoji_Tag_Sequence}/v,
|
||
/\p{RGI_Emoji_ZWJ_Sequence}/v,
|
||
/\p{RGI_Emoji}/v,
|
||
];
|
||
|
||
for (const re of regexes) {
|
||
expect(() => {
|
||
re.test("test");
|
||
}).not.toThrow();
|
||
}
|
||
|
||
function testExtendedCharacterClass({ regExp, matchStrings, nonMatchStrings }) {
|
||
matchStrings.forEach(str => expect(regExp.test(str)).toBeTrue());
|
||
nonMatchStrings.forEach(str => expect(regExp.test(str)).toBeFalse());
|
||
}
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{ASCII_Hex_Digit}--\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["0", "1", "2", "3", "4", "5", "8", "A", "B", "D", "E", "F", "a", "b", "c", "d", "e", "f"],
|
||
nonMatchStrings: [
|
||
"6\uFE0F\u20E3",
|
||
"7\uFE0F\u20E3",
|
||
"9\uFE0F\u20E3",
|
||
"\u2603",
|
||
"\u{1D306}",
|
||
"\u{1F1E7}\u{1F1EA}",
|
||
],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\d\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "9", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[[0-9]\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "9", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[_--[0-9]]+$/v,
|
||
matchStrings: ["_"],
|
||
nonMatchStrings: ["6\uFE0F\u20E3", "7", "9\uFE0F\u20E3", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{ASCII_Hex_Digit}--[0-9]]+$/v,
|
||
matchStrings: ["a", "b"],
|
||
nonMatchStrings: ["0", "9", "9\uFE0F\u20E3", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{ASCII_Hex_Digit}\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "A", "B", "a", "b"],
|
||
nonMatchStrings: ["\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[_\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3", "_"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}--\d]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}--[0-9]]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}--\p{ASCII_Hex_Digit}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}--_]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}&&\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}\d]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "9", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}[0-9]]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "9", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}\p{ASCII_Hex_Digit}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0", "0\uFE0F\u20E3", "9", "9\uFE0F\u20E3", "A", "a"],
|
||
nonMatchStrings: ["\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}_]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3", "_"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}\p{Emoji_Keycap_Sequence}]+$/v,
|
||
matchStrings: ["#\uFE0F\u20E3", "*\uFE0F\u20E3", "0\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\d--\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\d--\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["1", "9"],
|
||
nonMatchStrings: ["0", "9\uFE0F\u20E3", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\d&&\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\d&&\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["0", "2", "4"],
|
||
nonMatchStrings: ["1", "9\uFE0F\u20E3", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\d\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\d\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["0", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["6\uFE0F\u20E3", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}--\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\p{Emoji_Keycap_Sequence}--\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["#\uFE0F\u20E3", "8\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "9\uFE0F\u20E3", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\p{Emoji_Keycap_Sequence}\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\p{Emoji_Keycap_Sequence}\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["#\uFE0F\u20E3", "0", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\q{0|2|4|9\uFE0F\u20E3}--\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\q{0|2|4|9\uFE0F\u20E3}--\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: [],
|
||
nonMatchStrings: ["0", "9\uFE0F\u20E3", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\q{0|2|4|9\uFE0F\u20E3}&&\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\q{0|2|4|9\uFE0F\u20E3}&&\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["0", "2", "4", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["6\uFE0F\u20E3", "7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\q{0|2|4|9\uFE0F\u20E3}\q{0|2|4|9\uFE0F\u20E3}]+$/v,
|
||
expression: "[\q{0|2|4|9\uFE0F\u20E3}\q{0|2|4|9\uFE0F\u20E3}]",
|
||
matchStrings: ["0", "2", "4", "9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["6\uFE0F\u20E3", "7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
|
||
testExtendedCharacterClass({
|
||
regExp: /^[\q{0|2|4|9\uFE0F\u20E3}&&\p{Emoji_Keycap_Sequence}]+$/v,
|
||
expression: "[\q{0|2|4|9\uFE0F\u20E3}&&\p{Emoji_Keycap_Sequence}]",
|
||
matchStrings: ["9\uFE0F\u20E3"],
|
||
nonMatchStrings: ["0", "2", "4", "6\uFE0F\u20E3", "7", "C", "\u2603", "\u{1D306}", "\u{1F1E7}\u{1F1EA}"],
|
||
});
|
||
});
|
||
|
||
test("Unicode matching with u and v flags", () => {
|
||
const text = "𠮷a𠮷b𠮷";
|
||
const complexText = "a\u{20BB7}b\u{10FFFF}c";
|
||
|
||
const cases = [
|
||
{ pattern: /𠮷/, match: text, expected: ["𠮷"] },
|
||
{ pattern: /𠮷/u, match: text, expected: ["𠮷"] },
|
||
{ pattern: /𠮷/v, match: text, expected: ["𠮷"] },
|
||
{ pattern: /\p{Script=Han}/u, match: text, expected: ["𠮷"] },
|
||
{ pattern: /\p{Script=Han}/v, match: text, expected: ["𠮷"] },
|
||
{ pattern: /./u, match: text, expected: ["𠮷"] },
|
||
{ pattern: /./v, match: text, expected: ["𠮷"] },
|
||
{ pattern: /\p{ASCII}/u, match: text, expected: ["a"] },
|
||
{ pattern: /\p{ASCII}/v, match: text, expected: ["a"] },
|
||
{ pattern: /x/u, match: text, expected: null },
|
||
{ pattern: /x/v, match: text, expected: null },
|
||
{ pattern: /\p{Script=Han}(.)/gu, match: text, expected: ["𠮷a", "𠮷b"] },
|
||
{ pattern: /\p{Script=Han}(.)/gv, match: text, expected: ["𠮷a", "𠮷b"] },
|
||
{ pattern: /\P{ASCII}/u, match: complexText, expected: ["\u{20BB7}"] },
|
||
{ pattern: /\P{ASCII}/v, match: complexText, expected: ["\u{20BB7}"] },
|
||
{ pattern: /\P{ASCII}/gu, match: complexText, expected: ["\u{20BB7}", "\u{10FFFF}"] },
|
||
{ pattern: /\P{ASCII}/gv, match: complexText, expected: ["\u{20BB7}", "\u{10FFFF}"] },
|
||
{ pattern: /./gu, match: text, expected: ["𠮷", "a", "𠮷", "b", "𠮷"] },
|
||
{ pattern: /./gv, match: text, expected: ["𠮷", "a", "𠮷", "b", "𠮷"] },
|
||
{ pattern: /(?:)/gu, match: text, expected: ["", "", "", "", "", ""] },
|
||
{ pattern: /(?:)/gv, match: text, expected: ["", "", "", "", "", ""] },
|
||
// Character class splits family emoji (👨👩👧👦) into individual components, so it should match only the first one (👨)
|
||
{ pattern: /[👨👩👧👦]/v, match: "𠮷a𠮷b𠮷c👨👩👧👦d", expected: ["👨"] },
|
||
];
|
||
|
||
for (const test of cases) {
|
||
const result = test.match.match(test.pattern);
|
||
expect(result).toEqual(test.expected);
|
||
}
|
||
});
|
||
|
||
test("RegExp string literal", () => {
|
||
[
|
||
{ pattern: /[\q{abc}]/v, match: "abc", expected: ["abc"] },
|
||
{ pattern: /[\q{abc}]/v, match: "a", expected: null },
|
||
{ pattern: /[\q{a|b}]/v, match: "b", expected: ["b"] },
|
||
{ pattern: /[\q{a\\b}]/v, match: "a\\b", expected: ["a\\b"] },
|
||
{ pattern: /[\q{}]/v, match: "", expected: [""] },
|
||
{ pattern: /[\q{😀|😁|😂}]/v, match: "😁", expected: ["😁"] },
|
||
{ pattern: /[\q{1|1\uFE0F\u20E3}]/v, match: "1️⃣", expected: ["1️⃣"] },
|
||
{ pattern: /[\q{1}]/v, match: "1️⃣", expected: ["1"] },
|
||
{ pattern: /[\d&&\q{2}]/v, match: "123", expected: ["2"] },
|
||
{ pattern: /[^\q{a|b}]/v, match: "abc", expected: ["c"] },
|
||
{ pattern: /[\q{\n}]/v, match: "\n", expected: ["\n"] },
|
||
{ pattern: /[\q{\b}]/v, match: "\b", expected: ["\b"] },
|
||
{ pattern: /[\q{\0}]/v, match: "\0", expected: ["\0"] },
|
||
{ pattern: /[\q{\|}]/v, match: "|", expected: ["|"] },
|
||
{ pattern: /[\q{\x41}]/v, match: "A", expected: ["A"] },
|
||
{
|
||
pattern: /[\q{\uD83D\uDC68\u200d\uD83D\uDC69\u200d\uD83D\uDC66\u200d\uD83D\uDC66}]/v,
|
||
match: "👨👩👦👦",
|
||
expected: ["👨👩👦👦"],
|
||
},
|
||
{ pattern: /[\q{\u{1F600}}]/v, match: "😀", expected: ["😀"] },
|
||
{ pattern: /[\q{\cZ}]/v, match: "\x1A", expected: ["\x1A"] },
|
||
{ pattern: /[\q{ }]/v, match: " ", expected: [" "] },
|
||
{ pattern: /[[\d+]--[\q{1}]]/gv, match: "12", expected: ["2"] },
|
||
{ pattern: /[[\d]&&[\q{1}]]/gv, match: "21", expected: ["1"] },
|
||
{ pattern: /[\d\q{a}]/gv, match: "a1", expected: ["a", "1"] },
|
||
].forEach(test => {
|
||
const result = test.match.match(test.pattern);
|
||
expect(result).toEqual(test.expected);
|
||
});
|
||
|
||
[
|
||
"[\\q{(a)}]",
|
||
"[\\q{[a]}]",
|
||
"[\\q{{a}}]",
|
||
"[^\\q{bad}]",
|
||
"[\\q{a-b}]",
|
||
"[^\\q{a|bc}]",
|
||
"[^\\q{\\b+}]",
|
||
"[\\q{\\d}]",
|
||
"[\\q{\\w}]",
|
||
"[\\q{\\q}]",
|
||
"[^\\q{\\(\\)}]",
|
||
].forEach(pattern => {
|
||
expect(() => new RegExp(pattern, "v")).toThrow(SyntaxError);
|
||
});
|
||
});
|
||
|
||
// https://github.com/tc39/test262/tree/main/test/built-ins/RegExp/regexp-modifiers
|
||
test("RegExp modifiers", () => {
|
||
const testModifiers = (pattern, flags, tests) => {
|
||
const re = new RegExp(pattern, flags);
|
||
tests.forEach(([input, expected]) => expect(re.test(input)).toBe(expected));
|
||
};
|
||
|
||
testModifiers("(^a$)|(?:^b$)|(?m:^c$)|(?:^d$)|(^e$)", "", [
|
||
["\na\n", false],
|
||
["\nb\n", false],
|
||
["\nc\n", true],
|
||
["\nd\n", false],
|
||
["\ne\n", false],
|
||
]);
|
||
|
||
testModifiers("(?m-:es$|(?-m:js$))", "", [
|
||
["es\ns", true],
|
||
["js", true],
|
||
["js\ns", false],
|
||
]);
|
||
|
||
testModifiers("(a)|(?:b)|(?-i:c)|(?:d)|(e)", "i", [
|
||
["A", true],
|
||
["B", true],
|
||
["C", false],
|
||
["D", true],
|
||
["E", true],
|
||
]);
|
||
|
||
testModifiers("(?m:es.$)", "", [
|
||
["esz\n", true],
|
||
["es\n\n", false],
|
||
]);
|
||
|
||
testModifiers("(?m-:es.$)", "s", [
|
||
["esz\n", true],
|
||
["es\n\n", true],
|
||
]);
|
||
|
||
testModifiers("(?-i:\\u{0061})b", "iu", [
|
||
["ab", true],
|
||
["aB", true],
|
||
["Ab", false],
|
||
]);
|
||
|
||
testModifiers("(?-i:\\p{Lu})", "iu", [
|
||
["A", true],
|
||
["a", false],
|
||
["Z", true],
|
||
["z", false],
|
||
]);
|
||
|
||
testModifiers("(?-m:^es)$", "m", [
|
||
["e\nes\n", false],
|
||
["es\n", true],
|
||
]);
|
||
});
|
||
|
||
test("Unicode case-insensitive matching", () => {
|
||
const testMatch = (pattern, string, expected) => {
|
||
const result = string.match(pattern);
|
||
expect(result).toEqual(expected);
|
||
};
|
||
|
||
// U+017F - Latin Small Letter Long S (ſ)
|
||
testMatch(/\w/iv, "\u017F", ["\u017F"]);
|
||
testMatch(/\W/iv, "\u017F", null);
|
||
|
||
// U+212A - Kelvin Sign (K)
|
||
testMatch(/\b/i, "\u017F", null);
|
||
testMatch(/\b/iv, "\u017F", [""]);
|
||
testMatch(/\b/i, "\u212A", null);
|
||
testMatch(/\b/iv, "\u212A", [""]);
|
||
|
||
// ß shouldn't expand to SS
|
||
testMatch(/ss/i, "ß", null);
|
||
testMatch(/ss/iv, "ß", null);
|
||
|
||
// Greek Sigma has three case forms (Σ, σ, ς)
|
||
testMatch(/ς/i, "Σ", ["Σ"]);
|
||
testMatch(/ς/i, "σ", ["σ"]);
|
||
testMatch(/ς/i, "ς", ["ς"]);
|
||
testMatch(/Σ/i, "Σ", ["Σ"]);
|
||
testMatch(/Σ/i, "σ", ["σ"]);
|
||
testMatch(/Σ/i, "ς", ["ς"]);
|
||
|
||
// Accented characters
|
||
testMatch(/Ï/, "ï", null);
|
||
testMatch(/Ï/i, "ï", ["ï"]);
|
||
testMatch(/á/i, "Á", ["Á"]);
|
||
testMatch(/[á]/i, "Á", ["Á"]);
|
||
testMatch(/[á-á]/i, "Á", ["Á"]);
|
||
testMatch(/être/i, "ÊTRE", ["ÊTRE"]);
|
||
testMatch(/[être]/i, "ÊTRE", ["Ê"]);
|
||
|
||
// Uppercase (Lu) and Lowercase (Ll)
|
||
testMatch(/\p{Lu}/v, "ẞ", ["ẞ"]);
|
||
testMatch(/\p{Lu}/iv, "ẞ", ["ẞ"]);
|
||
testMatch(/\p{Ll}/v, "ẞ", null);
|
||
testMatch(/\p{Ll}/iv, "ẞ", ["ẞ"]);
|
||
|
||
testMatch(/\p{Lu}/v, "ß", null);
|
||
testMatch(/\p{Lu}/iv, "ß", ["ß"]);
|
||
testMatch(/\p{Ll}/v, "ß", ["ß"]);
|
||
testMatch(/\p{Ll}/iv, "ß", ["ß"]);
|
||
|
||
testMatch(/\p{Lu}/v, "Σ", ["Σ"]);
|
||
testMatch(/\p{Lu}/iv, "Σ", ["Σ"]);
|
||
testMatch(/\p{Lu}/v, "σ", null);
|
||
testMatch(/\p{Lu}/iv, "σ", ["σ"]);
|
||
testMatch(/\p{Lu}/v, "ς", null);
|
||
testMatch(/\p{Lu}/iv, "ς", ["ς"]);
|
||
|
||
testMatch(/\p{Lu}/gv, "Áá", ["Á"]);
|
||
testMatch(/\p{Lu}/giv, "Áá", ["Á", "á"]);
|
||
testMatch(/\p{Ll}/gv, "Áá", ["á"]);
|
||
testMatch(/\p{Ll}/giv, "Áá", ["Á", "á"]);
|
||
testMatch(/\p{Lu}/gv, "i\u0307", null);
|
||
testMatch(/\p{Lu}/giv, "i\u0307", ["i"]);
|
||
|
||
testMatch(/\p{Ll}/giu, "Aa", ["A", "a"]);
|
||
testMatch(/[^\P{Ll}]/giu, "Aa", null);
|
||
|
||
testMatch(/[\p{Ll}]/giv, "Aa", ["A", "a"]);
|
||
testMatch(/[^\P{Ll}]/giv, "Aa", ["A", "a"]);
|
||
|
||
testMatch(/\P{Ll}/giu, "Aa", ["A", "a"]);
|
||
testMatch(/\P{Ll}/giv, "Aa", null);
|
||
testMatch(/\P{Lu}/giu, "Aa", ["A", "a"]);
|
||
testMatch(/\P{Lu}/giv, "Aa", null);
|
||
|
||
testMatch(/[[\p{Ll}&&\p{Lu}]á]/i, "Á", null);
|
||
testMatch(/[[\p{Ll}&&\p{Lu}]á]/iv, "Á", ["Á"]);
|
||
|
||
// Binary properties
|
||
testMatch(/\p{Uppercase}/gv, "Áá", ["Á"]);
|
||
testMatch(/\p{Uppercase}/giv, "Áá", ["Á", "á"]);
|
||
testMatch(/\p{Lowercase}/gv, "Áá", ["á"]);
|
||
testMatch(/\p{Lowercase}/giv, "Áá", ["Á", "á"]);
|
||
|
||
// String literals
|
||
testMatch(/[á\q{ábc}]/giv, "ÁÁBC", ["Á", "ÁBC"]);
|
||
testMatch(/[á\q{ábc}]/giv, "áBC", ["áBC"]);
|
||
|
||
// U+FB05 - Latin Small Ligature Long S T (ſt)
|
||
testMatch(/[\ufb05]/i, "\ufb06", null);
|
||
testMatch(/[\ufb05]/v, "\ufb06", null);
|
||
testMatch(/[\ufb05]/iv, "\ufb06", ["st"]);
|
||
|
||
// U+FB06 - Latin Small Ligature ST (st)
|
||
testMatch(/[\ufb06]/i, "\ufb05", null);
|
||
testMatch(/[\ufb06]/v, "\ufb05", null);
|
||
testMatch(/[\ufb06]/iv, "\ufb05", ["ſt"]);
|
||
|
||
// Greek lowercase letters
|
||
testMatch(/[\u0390]/iv, "\u1fd3", ["\u1fd3"]);
|
||
testMatch(/[\u1fd3]/iv, "\u0390", ["\u0390"]);
|
||
testMatch(/[\u03b0]/iv, "\u1fe3", ["\u1fe3"]);
|
||
testMatch(/[\u1fe3]/iv, "\u03b0", ["\u03b0"]);
|
||
|
||
// U+017F - Latin Small Letter Long S (ſ)
|
||
testMatch(/[a-z]/i, "\u017F", null);
|
||
testMatch(/[a-z]/iv, "\u017F", ["\u017F"]);
|
||
testMatch(/s/i, "\u017F", null);
|
||
testMatch(/s/iv, "\u017F", ["\u017F"]);
|
||
|
||
// U+212A - Kelvin Sign (K)
|
||
testMatch(/[a-z]/i, "\u212A", null);
|
||
testMatch(/[a-z]/iv, "\u212A", ["\u212A"]);
|
||
testMatch(/k/i, "\u212A", null);
|
||
testMatch(/k/iv, "\u212A", ["\u212A"]);
|
||
|
||
// U+2126 - Ohm Sign (Ω)
|
||
testMatch(/[ω]/i, "\u2126", null);
|
||
testMatch(/[ω]/iv, "\u2126", ["\u2126"]);
|
||
testMatch(/[\u03A9]/i, "\u2126", null);
|
||
testMatch(/[\u03A9]/iv, "\u2126", ["\u2126"]);
|
||
});
|
||
|
||
test("surrogate pairs", () => {
|
||
expect(eval(`/[\uD83D\uDC38]/u`).exec("\u{1F438}")?.[0]).toBe("\u{1F438}");
|
||
expect(eval(`/[\uD83D\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
|
||
expect(eval(`/[\\uD83D\uDC38]/u`).exec("\u{1F438}")).toBeNull();
|
||
expect(eval(`/[\\u{D83D}\uDC38]/u`).exec("\u{1F438}")).toBeNull();
|
||
expect(eval(`/[\uD83D\\uDC38]/u`).exec("\u{1F438}")).toBeNull();
|
||
expect(eval(`/[\uD83D\\u{DC38}]/u`).exec("\u{1F438}")).toBeNull();
|
||
expect(eval(`/[\\uD83D\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
|
||
expect(eval(`/[\uD83D\\uDC38]/`).exec("\u{1F438}")?.[0]).toBe("\uD83D");
|
||
});
|
||
|
||
test("incomplete \\u and \\x escapes", () => {
|
||
expect("u".match(/^\u$/)).toEqual(["u"]);
|
||
expect("\\u\u0000".match(/[\u]+/)).toEqual(["u"]);
|
||
expect("\\uy\u0000".match(/[\uy]+/)).toEqual(["uy"]);
|
||
expect("\\u0\u0000".match(/[\u0]+/)).toEqual(["u0"]);
|
||
expect("\\u0\u0000".match(/[\u00]+/)).toEqual(["u0"]);
|
||
expect("\\u0\u0000".match(/[\u000]+/)).toEqual(["u0"]);
|
||
expect("\\u0y\u0000".match(/[\u0y]+/)).toEqual(["u0y"]);
|
||
expect("\\u0y\u0000".match(/[\u00y]+/)).toEqual(["u0y"]);
|
||
expect("\\u0y\u0000".match(/[\u000y]+/)).toEqual(["u0y"]);
|
||
|
||
expect("uy".match(/^\uy$/)).toEqual(["uy"]);
|
||
expect("u0".match(/^\u0$/)).toEqual(["u0"]);
|
||
expect("u00".match(/^\u00$/)).toEqual(["u00"]);
|
||
expect("u000".match(/^\u000$/)).toEqual(["u000"]);
|
||
expect("u0y".match(/^\u0y$/)).toEqual(["u0y"]);
|
||
expect("u00y".match(/^\u00y$/)).toEqual(["u00y"]);
|
||
expect("u000y".match(/^\u000y$/)).toEqual(["u000y"]);
|
||
|
||
expect("x".match(/^\x$/)).toEqual(["x"]);
|
||
expect("xy".match(/^\xy$/)).toEqual(["xy"]);
|
||
expect("x0".match(/^\x0$/)).toEqual(["x0"]);
|
||
expect("x0y".match(/^\x0y$/)).toEqual(["x0y"]);
|
||
expect("\\x\u0000".match(/[\x]+/)).toEqual(["x"]);
|
||
expect("\\xy\u0000".match(/[\xy]+/)).toEqual(["xy"]);
|
||
expect("\\x0\u0000".match(/[\x0]+/)).toEqual(["x0"]);
|
||
expect("\\x0y\u0000".match(/[\x0y]+/)).toEqual(["x0y"]);
|
||
expect("\\x\u0000".match(/[\x00]+/)).toEqual(["\u0000"]);
|
||
expect("0\u0000".match(/[\x000]+/)).toEqual(["0\u0000"]);
|
||
});
|