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: "(?\\w+)", flags: ["u"], subject: "alice", value: "alice", }, { name: "lookbehind", pattern: "(?<=€)\\d+", flags: ["u"], subject: "€42", value: "42", }, { name: "backreference", pattern: "(?\\w+)\\s+\\k", 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: "(?😀)", flags: ["u"], options: {}, }); const result = executeEcmaScript({ flavour: "ecmascript", pattern: "(?😀)", 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 = "(?a)|(?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"); } }); });