Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "eeab04040fa61fd595695980f9d054b7fc0ed855";
export const WEBKIT_VERSION = "autobuild-preview-pr-456-f867433c";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
132 changes: 132 additions & 0 deletions test/js/bun/jsc/regexp-ignore-case.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";

// Non-unicode /i matching uses a table that is committed to JavaScriptCore
// (yarr/YarrCanonicalizeUCS2.cpp) and regenerated by hand, unlike the /iu table,
// which is generated from CaseFolding.txt at build time. The pairs below were
// added in Unicode 16 and 17 and were missing from the committed table
// (oven-sh/WebKit#456). Only RegExp behaviour is asserted here: the regexp
// tables are the same on every platform, while String.prototype.toUpperCase
// follows the system ICU on macOS.

// name -> [small letter, capital letter]
const unicode16And17Pairs: Record<string, [string, string]> = {
// Unicode 16
"LATIN LETTER LAMBDA WITH STROKE": ["\u019b", "\ua7dc"],
"LATIN LETTER RAMS HORN": ["\u0264", "\ua7cb"],
"LATIN LETTER S WITH DIAGONAL STROKE": ["\ua7cd", "\ua7cc"],
"LATIN LETTER LAMBDA": ["\ua7db", "\ua7da"],
"CYRILLIC LETTER TJE": ["\u1c8a", "\u1c89"],
// Unicode 17 (the small letters of the last two date from Unicode 14, the capitals are new)
"LATIN LETTER PHARYNGEAL VOICED FRICATIVE": ["\ua7cf", "\ua7ce"],
"LATIN LETTER DOUBLE THORN": ["\ua7d3", "\ua7d2"],
"LATIN LETTER DOUBLE WYNN": ["\ua7d5", "\ua7d4"],
};

// Pairs next to the new entries. The table stores runs of consecutive pairs as
// ranges, and adding the entries above merged or split the runs these live in.
const neighbouringPairs: Record<string, [string, string]> = {
"LATIN LETTER L WITH BAR": ["\u019a", "\u023d"],
"LATIN LETTER GAMMA": ["\u0263", "\u0194"],
"LATIN LETTER TURNED H": ["\u0265", "\ua78d"],
"LATIN LETTER S WITH SHORT STROKE OVERLAY": ["\ua7ca", "\ua7c9"],
"LATIN LETTER CLOSED INSULAR G": ["\ua7d1", "\ua7d0"],
"LATIN LETTER MIDDLE SCOTS S": ["\ua7d7", "\ua7d6"],
"LATIN LETTER SIGMOID S": ["\ua7d9", "\ua7d8"],
"LATIN LETTER REVERSED HALF H": ["\ua7f6", "\ua7f5"],
"CYRILLIC LETTER MONOGRAPH UK": ["\ua64b", "\ua64a"],
"CYRILLIC SMALL LETTER UNBLENDED UK (same set as MONOGRAPH UK)": ["\u1c88", "\ua64a"],
};

function escape(ch: string): string {
return "\\u" + ch.charCodeAt(0).toString(16).padStart(4, "0");
}

function matches(pattern: string, input: string, flags: string) {
const atom = escape(pattern);
return {
atom: new RegExp(atom, flags).test(input),
anchoredAtom: new RegExp(`^${atom}$`, flags).test(input),
insideLongerAtom: new RegExp(`x${atom}y`, flags).test(`x${input}y`),
class: new RegExp(`[${atom}]`, flags).test(input),
negatedClass: new RegExp(`[^${atom}]`, flags).test(input),
backreference: new RegExp(`(${atom})\\1`, flags).test(pattern + input),
replace: `a${input}b`.replace(new RegExp(atom, flags), "-"),
};
}

const folded = {
atom: true,
anchoredAtom: true,
insideLongerAtom: true,
class: true,
negatedClass: false,
backreference: true,
replace: "a-b",
};

describe.each(["i", "iu"])("/%s folds the case pairs added in Unicode 16 and 17", flags => {
test.each(Object.entries(unicode16And17Pairs))("%s", (_name, [lower, upper]) => {
expect(matches(lower, upper, flags)).toEqual(folded);
expect(matches(upper, lower, flags)).toEqual(folded);
});
});

describe.each(["i", "iu"])("/%s still folds the pairs next to the new entries", flags => {
test.each(Object.entries(neighbouringPairs))("%s", (_name, [lower, upper]) => {
expect(matches(lower, upper, flags)).toEqual(folded);
expect(matches(upper, lower, flags)).toEqual(folded);
});
});

test("/i class ranges pick up the new partners", () => {
// Each tested character lies outside the range and can only match through its partner.
expect([
/[\ua7cc-\ua7da]/i.test("\ua7db"), // through U+A7DA, the last character of the range
/[\u0190-\u01a0]/i.test("\ua7dc"), // through U+019B
/[\ua7c0-\ua7ff]/i.test("\u019b"), // through U+A7DC
/[\ua7c0-\ua7ff]/i.test("\u0264"), // through U+A7CB
/[\u1c80-\u1c89]/i.test("\u1c8a"), // through U+1C89, the last character of the range
]).toEqual([true, true, true, true, true]);
});

test("/i does not fold the code units around the new pairs", () => {
const unrelated: [string, string][] = [
["\u1c8a", "\u1c8b"], // U+1C8B is unassigned
["\u1c89", "\u1c88"],
["\ua7cb", "\ua7ca"],
["\ua7dc", "\ua7dd"], // U+A7DD is unassigned
["\ua7dc", "\ua7db"],
["\u019b", "\u019a"],
["\u0264", "\u0263"],
];
const results = unrelated.map(([a, b]) => [
new RegExp(escape(a), "i").test(b),
new RegExp(`[${escape(a)}]`, "i").test(b),
]);
expect(results).toEqual(unrelated.map(() => [false, false]));
});

test("the Yarr interpreter (--useRegExpJIT=false) folds the new pairs under /i too", async () => {
const script = `
const pairs = ${JSON.stringify(Object.values(unicode16And17Pairs))};
const failures = [];
for (const [lower, upper] of pairs) {
for (const [pattern, input] of [[lower, upper], [upper, lower]]) {
if (!new RegExp(pattern, "i").test(input) || !new RegExp("[" + pattern + "]", "i").test(input))
failures.push(pattern.charCodeAt(0).toString(16) + " vs " + input.charCodeAt(0).toString(16));
}
}
console.log(JSON.stringify(failures));
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", script],
env: { ...bunEnv, BUN_JSC_useRegExpJIT: "false" },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(JSON.parse(stdout)).toEqual([]);
expect(exitCode).toBe(0);
});
Loading