Skip to content
Open
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 = "549170099226f816a4b204ea1d8fa102fb79eefa";
export const WEBKIT_VERSION = "autobuild-preview-pr-360-52f23e4f";
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
85 changes: 85 additions & 0 deletions test/js/web/intl/intl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,91 @@
});
});

// ---------------------------------------------------------------------------
// String.prototype.localeCompare — collator caching for (string locale, no options)
// ---------------------------------------------------------------------------

describe("String.prototype.localeCompare with a string locale", () => {
// JSC caches a per-global IntlCollator for a.localeCompare(b, "<locale>") with undefined
// options so that arr.sort((a, b) => a.localeCompare(b, "en")) does not rebuild a UCollator
// on every comparison. These tests guard against the cache returning stale results.

test("matches new Intl.Collator(locale).compare", () => {
for (const loc of ["en", "sv", "de", "ja", "fr", "de-u-co-phonebk"]) {
const cmp = new Intl.Collator(loc).compare;
for (const [a, b] of [
["ä", "z"],
["ä", "ae"],
["a", "A"],
["abc", "abd"],
] as const) {
expect(a.localeCompare(b, loc)).toBe(cmp(a, b));
}
}
});

test("changing the locale string returns the new locale's order", () => {
// Swedish puts ä after z, English before.
expect("ä".localeCompare("z", "en")).toBeLessThan(0);
expect("ä".localeCompare("z", "sv")).toBeGreaterThan(0);
expect("ä".localeCompare("z", "en")).toBeLessThan(0);
// German phonebook sorts ä with ae.
expect("ä".localeCompare("ae", "de")).toBeLessThan(0);
expect("ä".localeCompare("ae", "de-u-co-phonebk")).toBeGreaterThan(0);
expect("ä".localeCompare("ae", "de")).toBeLessThan(0);
});

test("invalid locale still throws; cache is not poisoned", () => {
expect("ä".localeCompare("z", "en")).toBeLessThan(0);
expect(() => "a".localeCompare("b", "bad locale!!")).toThrow(RangeError);
expect("ä".localeCompare("z", "en")).toBeLessThan(0);
});

Check warning on line 181 in test/js/web/intl/intl.test.ts

View check run for this annotation

Claude / Claude Code Review

'cache is not poisoned' test cannot detect the poisoning it names

This test can't detect the most plausible "cache poisoned" bug it names: if the cache key were written before `initializeCollator` throws, the slot would become `{key: "bad locale!!", collator: <stale en>}`, and line 180's follow-up `"en"` call would miss that key, rebuild a fresh collator, and pass regardless. Add a second `expect(() => "a".localeCompare("b", "bad locale!!")).toThrow(RangeError)` — a repeat call with the same invalid string is what would hit the poisoned slot and return a numbe
Comment thread
robobun marked this conversation as resolved.

test("does not leak into calls that pass options", () => {
// Seed the cache first so an options call that wrongly reads it would be caught below.
expect("a".localeCompare("A", "en")).not.toBe(0);
expect("a".localeCompare("A", "en", { sensitivity: "base" })).toBe(0);
// And the cached collator must not have been overwritten by the options call.
expect("a".localeCompare("A", "en")).not.toBe(0);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

test("non-string locales and no-arg form still work", () => {
expect("ä".localeCompare("z", ["sv"])).toBeGreaterThan(0);
expect("ä".localeCompare("z", ["en"])).toBeLessThan(0);
expect(typeof "a".localeCompare("b")).toBe("number");
Comment thread
robobun marked this conversation as resolved.
Outdated
});

test("cached collator survives GC", () => {
"a".localeCompare("b", "fr");
Bun.gc(true);
expect("ä".localeCompare("z", "fr")).toBeLessThan(0);
Bun.gc(true);
expect("ä".localeCompare("z", "fr")).toBeLessThan(0);
});
Comment thread
robobun marked this conversation as resolved.

test("reuses the collator: sort by localeCompare(b,'en') is as fast as a hoisted Intl.Collator", () => {
// A 3000-element sort does ~33k comparisons. With the cache this costs one
// ucol_open instead of ~33k, so the two sorts should take about the same time.
// Without the cache the ratio is >5 on a debug build and >20 on a release build.
const N = 3000;
const arr = Array.from({ length: N }, (_, i) => ((i * 2654435761) >>> 0).toString(36) + "aä"[i % 2]);

const warm = arr.slice(0, 50);
[...warm].sort((a, b) => a.localeCompare(b, "en"));
[...warm].sort(new Intl.Collator("en").compare);

let t = performance.now();
[...arr].sort((a, b) => a.localeCompare(b, "en"));
const withLocale = performance.now() - t;

t = performance.now();
[...arr].sort(new Intl.Collator("en").compare);
const hoisted = performance.now() - t;

expect(withLocale / hoisted).toBeLessThan(3);
Comment thread
robobun marked this conversation as resolved.
Outdated
});
});

// ---------------------------------------------------------------------------
// Segmenter — brkitr/* raw (incl. cjdict)
// ---------------------------------------------------------------------------
Expand Down
Loading