249 lines
6.3 KiB
TypeScript
249 lines
6.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { EcmaScriptSyntaxProvider } from "../../src/regex/syntax/providers/ecmascript/EcmaScriptSyntaxProvider";
|
|
import { executeEcmaScript } from "../../src/regex/execution/adapters/ecmascript/EcmaScriptEngineAdapter";
|
|
|
|
const provider = new EcmaScriptSyntaxProvider();
|
|
|
|
const cases = [
|
|
{
|
|
name: "named capture",
|
|
pattern: "(?<name>\\w+)",
|
|
flags: ["u"],
|
|
subject: "alice",
|
|
value: "alice",
|
|
},
|
|
{
|
|
name: "lookbehind",
|
|
pattern: "(?<=€)\\d+",
|
|
flags: ["u"],
|
|
subject: "€42",
|
|
value: "42",
|
|
},
|
|
{
|
|
name: "backreference",
|
|
pattern: "(?<word>\\w+)\\s+\\k<word>",
|
|
flags: ["u"],
|
|
subject: "echo echo",
|
|
value: "echo echo",
|
|
},
|
|
{
|
|
name: "Unicode property",
|
|
pattern: "\\p{Letter}+",
|
|
flags: ["u"],
|
|
subject: "Grüße 42",
|
|
value: "Grüße",
|
|
},
|
|
] as const;
|
|
|
|
describe("project-authored ECMAScript conformance cases", () => {
|
|
for (const testCase of cases) {
|
|
it(testCase.name, async () => {
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern: testCase.pattern,
|
|
flags: testCase.flags,
|
|
options: {},
|
|
});
|
|
expect(syntax.accepted).toBe(true);
|
|
const result = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern: testCase.pattern,
|
|
flags: testCase.flags,
|
|
subject: testCase.subject,
|
|
captureMetadata: syntax.captures,
|
|
scanAll: false,
|
|
maximumMatches: 100,
|
|
maximumCaptureRows: 1_000,
|
|
});
|
|
expect(result.accepted).toBe(true);
|
|
expect(result.matches[0]?.value).toBe(testCase.value);
|
|
});
|
|
}
|
|
|
|
it("keeps astral ranges in UTF-16 code units", async () => {
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern: "(?<emoji>😀)",
|
|
flags: ["u"],
|
|
options: {},
|
|
});
|
|
const result = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern: "(?<emoji>😀)",
|
|
flags: ["u"],
|
|
subject: "x😀y",
|
|
captureMetadata: syntax.captures,
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
});
|
|
expect(result.matches[0]?.range).toEqual({
|
|
startUtf16: 1,
|
|
endUtf16: 3,
|
|
});
|
|
expect(result.matches[0]?.captures[0]?.nativeRange).toEqual({
|
|
start: 1,
|
|
end: 3,
|
|
unit: "utf16",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "global iteration",
|
|
pattern: "\\d",
|
|
flags: ["g"],
|
|
subject: "1 2",
|
|
values: ["1", "2"],
|
|
},
|
|
{
|
|
name: "sticky iteration",
|
|
pattern: "\\w",
|
|
flags: ["y"],
|
|
subject: "ab",
|
|
values: ["a"],
|
|
},
|
|
{
|
|
name: "ignore case",
|
|
pattern: "hello",
|
|
flags: ["i"],
|
|
subject: "HELLO",
|
|
values: ["HELLO"],
|
|
},
|
|
{
|
|
name: "multiline anchors",
|
|
pattern: "^second$",
|
|
flags: ["m"],
|
|
subject: "first\nsecond\nthird",
|
|
values: ["second"],
|
|
},
|
|
{
|
|
name: "dot all",
|
|
pattern: "a.b",
|
|
flags: ["s"],
|
|
subject: "a\nb",
|
|
values: ["a\nb"],
|
|
},
|
|
{
|
|
name: "Unicode sets",
|
|
pattern: "[\\p{Letter}&&\\p{ASCII}]+",
|
|
flags: ["v"],
|
|
subject: "abcä",
|
|
values: ["abc"],
|
|
},
|
|
])("implements $name flag semantics", async (testCase) => {
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern: testCase.pattern,
|
|
flags: testCase.flags,
|
|
options: {},
|
|
});
|
|
expect(syntax.accepted).toBe(true);
|
|
const result = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern: testCase.pattern,
|
|
flags: testCase.flags,
|
|
subject: testCase.subject,
|
|
captureMetadata: syntax.captures,
|
|
scanAll: false,
|
|
maximumMatches: 100,
|
|
maximumCaptureRows: 1_000,
|
|
});
|
|
expect(result.accepted).toBe(true);
|
|
expect(result.matches.map((match) => match.value)).toEqual(testCase.values);
|
|
});
|
|
|
|
it("preserves an explicit user indices flag", async () => {
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern: "(a)",
|
|
flags: ["d"],
|
|
options: {},
|
|
});
|
|
const result = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern: "(a)",
|
|
flags: ["d"],
|
|
subject: "a",
|
|
captureMetadata: syntax.captures,
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
});
|
|
|
|
expect(result.accepted).toBe(true);
|
|
expect(result.flags.userFlags).toBe("d");
|
|
expect(result.flags.internallyAddedIndicesFlag).toBe(false);
|
|
expect(result.matches[0]?.captures[0]?.range).toEqual({
|
|
startUtf16: 0,
|
|
endUtf16: 1,
|
|
});
|
|
});
|
|
|
|
it("reports malformed syntax from both provider and engine", async () => {
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern: "(",
|
|
flags: [],
|
|
options: {},
|
|
});
|
|
expect(syntax.accepted).toBe(false);
|
|
expect(syntax.diagnostics[0]?.range).toEqual({
|
|
startUtf16: 1,
|
|
endUtf16: 1,
|
|
});
|
|
|
|
const execution = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern: "(",
|
|
flags: [],
|
|
subject: "",
|
|
captureMetadata: [],
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
});
|
|
expect(execution.accepted).toBe(false);
|
|
expect(execution.diagnostics[0]?.code).toBe("compile-error");
|
|
});
|
|
|
|
it("records the current runtime's duplicate-name behavior", async () => {
|
|
const pattern = "(?<value>a)|(?<value>b)";
|
|
const syntax = await provider.parsePattern({
|
|
flavour: "ecmascript",
|
|
pattern,
|
|
flags: [],
|
|
options: {},
|
|
});
|
|
const execution = executeEcmaScript({
|
|
flavour: "ecmascript",
|
|
pattern,
|
|
flags: [],
|
|
subject: "b",
|
|
captureMetadata: syntax.captures,
|
|
scanAll: false,
|
|
maximumMatches: 10,
|
|
maximumCaptureRows: 10,
|
|
});
|
|
let runtimeSupportsDuplicateNames = true;
|
|
try {
|
|
new RegExp(pattern);
|
|
} catch {
|
|
runtimeSupportsDuplicateNames = false;
|
|
}
|
|
|
|
expect(execution.accepted).toBe(runtimeSupportsDuplicateNames);
|
|
if (runtimeSupportsDuplicateNames) {
|
|
expect(
|
|
execution.matches[0]?.captures.find(
|
|
(capture) =>
|
|
capture.groupName === "value" &&
|
|
capture.status !== "did-not-participate",
|
|
)?.value,
|
|
).toBe("b");
|
|
} else {
|
|
expect(execution.diagnostics[0]?.code).toBe("compile-error");
|
|
}
|
|
});
|
|
});
|