diff --git a/docs/guides/format/migrate-from-oxfmt.mdx b/docs/guides/format/migrate-from-oxfmt.mdx
index b6ff21ef5340..4e06b04626fa 100644
--- a/docs/guides/format/migrate-from-oxfmt.mdx
+++ b/docs/guides/format/migrate-from-oxfmt.mdx
@@ -111,7 +111,7 @@ If `bun fmt` and oxfmt 0.72 print different code, that's a bug. [Open an issue](
- **An invalid TOML file is left alone**, with a warning. oxfmt rewrites the parts it can read.
- **A file that isn't valid UTF-8 is still formatted**, and the invalid bytes are kept. oxfmt exits with code `2`.
- **A function's only parameter doesn't move on the second run.** When an object type in the return type doesn't fit on the line, oxfmt gives the parameter its own line, then joins `m(a: string): {` again the next time it runs. Bun prints the joined form the first time.
-- **Where oxfmt's output changes what a file means, Bun's differs.** That's rare. In TOML oxfmt joins the lines of an inline table with a comment, which comments out the rest of the table. Bun keeps one entry per line. In Svelte oxfmt drops the comment in `{#if a /* b */}`, the space in `
some words`, and an empty `{:catch}`, and it empties the `\n`,
+ `\n \n\n\n\n`,
+ avoid,
+ ],
+ "a.svelte": [
+ `\n\n ({ q }) : (deps) => (id): T => y} />\n`,
+ `\n\n ({ q })\n : (deps) =>\n (id): T =>\n y}\n/>\n`,
+ avoid,
+ ],
+ // The plugin reads a `\n`, `\n`, avoid],
+ // The start of a block is on one line.
+ "c.svelte": [
+ `\n\n{#if a ? (q) => ({ q }) : (d) => (e): T => f}x
{/if}\n{#if a ? b ? c : (p, q) : (d) => (e): T => f}y
{/if}\n`,
+ `\n\n{#if ${kept}}x
{/if}\n{#if ${inParentheses}}y
{/if}\n`,
+ avoid,
+ ],
+ // So is a cell of a table.
+ "a.test.ts": [
+ 'test.each`\n a | b\n ${a ? (q) => ({ q }) : (d) => (e): T => f} | ${1}\n ${a ? b ? c : (p, q) : (d) => (e): T => f} | ${2}\n ${p ? (a, b) : (y) => t ? f() : (d) => (e): T => z} | ${3}\n ${c6 ? (a, b) : (y) => ({ z }) => t ? u : (e): T => f} | ${4}\n`("x", () => {});\n',
+ `test.each\`\n ${cell("a", afterChain)} | b\n ${cell(`\${${kept}}`, afterChain)} | \${1}\n ${cell(`\${${inParentheses}}`, afterChain)} | \${2}\n ${cell(`\${${inBody}}`, afterChain)} | \${3}\n ${cell(`\${${afterChain}}`, afterChain)} | \${4}\n\`("x", () => {});\n`,
+ avoid,
+ ],
+ "b.test.tsx": [
+ 'test.each`\n a | b\n ${a ? : b ? (q) => ({ q }) : (d) => (e): T => f} | ${1}\n ${z ? : a ? (p, r) : (y) => c ? () => ({}) : (d) => (e): T => f} | ${2}\n ${z ? : a ? (p, r) : (y) => c} | ${3}\n`("x", () => {});\n',
+ `test.each\`\n ${cell("a", inFamily)} | b\n ${cell(`\${${afterElement}}`, inFamily)} | \${1}\n ${cell(`\${${inFamily}}`, inFamily)} | \${2}\n ${cell(`\${${asPrettier}}`, inFamily)} | \${3}\n\`("x", () => {});\n`,
+ avoid,
+ ],
+ "a.html": [`\n`, `\n`, avoid],
+ "a.md": [`\`\`\`ts\n${code}\`\`\`\n`, `\`\`\`ts\n${printed}\`\`\`\n`, avoid],
+ "a.js": [code, printed, [...avoid, "--parser", "typescript"]],
+ "b.js": [code, printed, [...avoid, "--parser", "babel-ts"]],
+ "a.mts": [code, printed, avoid],
+ "a.ts": [code, printed, [], oxfmt],
+ // oxfmt prints no parentheses of a JSDoc type cast around an arrow function that another one returns.
+ "b.ts": [
+ "x = cond ? (q) => /** @type {F} */ ((r) => ({ q, r })) : (deps) => /** @type {F} */ ((id): T => y);\n",
+ "x = cond\n ? q => /** @type {F} */ r => ({ q, r })\n : (deps) =>\n /** @type {F} */ (id): T =>\n y;\n",
+ [],
+ oxfmt,
+ ],
+ // So the conditional that such an arrow function returns is in the family of the one around it.
+ "c.ts": [
+ "x = a ? (c ? (q) => ({ q }) : (d) => (e): T => f) : (y) => /** @type {F} */ ((z) => u ? (m, n) : (w) => g);\n",
+ "x = a\n ? c\n ? q => ({ q })\n : (d) =>\n (e): T =>\n f\n : y => /** @type {F} */ z => (u ? (m, n) : (w) => g);\n",
+ [],
+ oxfmt,
+ ],
+ };
+ const results = await Promise.all(
+ Object.entries(hosts).map(async ([file, [text, printed, flags, config]]) => {
+ const [first, second] = await Promise.all([
+ format({ ...config, [file]: text }, [...flags, file], { reads: [file] }),
+ format({ ...config, [file]: printed }, [...flags, "--check", file]),
+ ]);
+ return [file, { printed: first.files[file], exitCodes: [first.exitCode, second.exitCode] }];
+ }),
+ );
+ expect(Object.fromEntries(results)).toEqual(
+ Object.fromEntries(Object.entries(hosts).map(([file, [, printed]]) => [file, { printed, exitCodes: [0, 0] }])),
+ );
+ });
+
+ test.each([[[]], [avoid]])(
+ "known issue: in Flow, Prettier's text for the same conditional is a syntax error, with %j",
+ async flags => {
+ // `@babel/parser` reports `AmbiguousConditionalArrow` for it, and `flow-parser` expects a `:`. `(deps) =>` does
+ // not help: Flow needs the parentheses of the input around the arrow function before the `:`.
+ const text = "// @flow\nx = cond ? ((q) => ({ q })) : (deps) => (id): T => y;\n";
+ const [before, after] = flags.length ? ["q", "deps"] : ["(q)", "(deps)"];
+ const [result, checked] = await Promise.all([
+ format({ "a.js": text }, flags, { reads: ["a.js"] }),
+ format({ "a.js": text }, [...flags, "--debug-check"]),
+ ]);
+ expect(result.files).toEqual({
+ "a.js": `// @flow\nx = cond\n ? ${before} => ({ q })\n : ${after} =>\n (id): T =>\n y;\n`,
+ });
+ expect(result.exitCode).toBe(0);
+ expect(checked.stderr).toContain("Not formatted, because formatting would change what the code does");
+ expect(checked.exitCode).toBe(2);
+ },
+ );
+
+ test("decides once for each family, with 64 arrow functions or conditionals in each other", async () => {
+ const after = "(deps) => (id): T => y";
+ const nested = (make: (inner: string) => string, innermost: string) =>
+ Array.from({ length: 64 }).reduce(inner => make(inner), innermost);
+ const files = {
+ // 64 arrow functions come before the `:`, and the last one returns the object.
+ "a.ts": `x = a ? ${nested(inner => `(q) => ${inner}`, "({ q })")} : ${after};\n`,
+ // 64 names come before the return type.
+ "b.ts": `x = a ? (q) => ({ q }) : ${nested(inner => `(deps) => ${inner}`, "(id): T => y")};\n`,
+ // Each arrow function returns the conditional that the next one is in. Only the innermost has `( .. ) :`.
+ "c.ts": `x = a ? ${nested(inner => `(q) => (a ? ${inner} : ${after})`, "(q) => ({ q })")} : ${after};\n`,
+ // Each conditional ends an arrow function after the `:` of the one that it is in, which has `(b, c) :`.
+ "d.ts": `x = ${nested(inner => `a ? (b, c) : (y) => ${inner}`, `a ? (q) => ({ q }) : ${after}`)};\n`,
+ // Each conditional is before the `:` of the one that it is in, and the innermost one ends with `(m, n) : d`.
+ "e.ts": `x = ${nested(inner => `a ? ${inner} : z`, `a ? (q) => ({ q }) : (deps) => (id): T => t ? (m, n) : (d) => f`)};\n`,
+ // As in `d.ts`, and only the outermost one has `(b, c) :`. tsc reads the body of its `y =>` on to the return type.
+ "f.ts": `x = a ? (b, c) : (y) => ${nested(inner => `a ? f(b) : (y) => ${inner}`, `a ? g(q) : ${after}`)};\n`,
+ };
+ const result = await format(files, avoid, { reads: Object.keys(files) });
+ expect(result.exitCode).toBe(0);
+ const printed = result.files as Record;
+ const kept = (name: string) =>
+ Object.values(printed).map(text => text.match(new RegExp(`\\(\\s*${name},?\\s*\\) =>`, "g"))?.length ?? 0);
+ expect({ deps: kept("deps"), y: kept("y"), d: kept("d") }).toEqual({
+ deps: [1, 64, 1, 1, 1, 0],
+ y: [0, 0, 0, 64, 0, 1],
+ d: [0, 0, 0, 0, 1, 0],
+ });
+ // The parser of `bun fmt` reads each text as tsc does: a second run changes nothing.
+ const checked = await format(printed, [...avoid, "--check"]);
+ expect(checked.exitCode).toBe(0);
+ });
+
+ test("takes linear time for conditionals in conditionals", async () => {
+ // A balanced tree keeps the lines short. Each leaf is a conditional that keeps `(d) =>`.
+ const tree = (leaf: string, leaves: number): string =>
+ leaves === 1 ? leaf : `c ? ${tree(leaf, leaves / 2)} : ${tree(leaf, leaves / 2)}`;
+ const leaves = isDebug || isASAN ? 2_048 : 16_384;
+ const run = (parameter: string) =>
+ format({ "a.ts": `x = ${tree(`a ? (q) => ({ q }) : (d) => (${parameter}): T => f`, leaves)};\n` }, [
+ ...avoid,
+ "--check",
+ ]);
+ // Before `(e: U): T`, tsc is sure of an arrow function, and nothing is kept.
+ const [odd, plain] = await Promise.all([run("e"), run("e: U")]);
+ // A run that refuses the file would be fast. Exit code 1: the file is read, and is not as `bun fmt` prints it.
+ expect([odd.exitCode, plain.exitCode]).toEqual([1, 1]);
+ expect(slowdown(odd, plain)).toBeLessThan(4);
+ });
+ });
+
test("treats a vertical tab next to a comment as whitespace", async () => {
const text = "function a(b\v/* c */) {}\nfunction d(/* e */\vf) {}\nconst g = /* h */\vi;\n";
const result = await format({ "a.js": text }, [], { reads: ["a.js"] });
diff --git a/test/cli/format/oracle/arrow-parens/judge.mjs b/test/cli/format/oracle/arrow-parens/judge.mjs
new file mode 100644
index 000000000000..f60b3824756c
--- /dev/null
+++ b/test/cli/format/oracle/arrow-parens/judge.mjs
@@ -0,0 +1,1053 @@
+// What `bun fmt` prints for conditionals of arrow functions in TypeScript, judged by tsc's own parser: it has to read
+// the text as it reads the source. See `read_by_tsc` and `AfterColon` in src/format/js/utils/conditional.rs.
+//
+// node judge.mjs --modules= --bun= [--main=] [--matrix [--jsx] | --values | --sites | --paths |
+// --families | --comments | --file=] [--bare] [--each] [--always] [--count=5000] [--seed=1]
+// [--width=80,40] [--experimental-ternaries] [--trailing-comma=none] [--oxfmt] [--babel-ts]
+// [--reader=] [--show=5]
+//
+// - `--modules`: a directory with node_modules/prettier and node_modules/typescript, and node_modules/esbuild for the
+// counts about esbuild and Node. The rule was checked with Prettier 3.9.9, TypeScript 6.0.2, esbuild 0.18.6 and
+// Node 26.3.0.
+// - `--main`: a build of main. A text that it prints as `--bun` does is not counted as `broken`, `pairs`, `silenced`
+// or `unstable`: there Bun differs from Prettier for another reason. Without it, random sources with
+// `function () {}` in them count as `pairs`.
+// - `--matrix`: instead of random sources, every pair of an operand before the `:` and one after it, in ten
+// positions, with every parameter in parentheses.
+// - `--jsx`: the matrix is of conditionals with an element in them, in a `.tsx` file. Prettier puts parentheses
+// around their operands if they break.
+// - `--bare`: the parameters of the matrix have no parentheses, as `arrowParens: "avoid"` leaves them.
+// - `--values`: instead of the matrix, arrow functions without parentheses around their parameter as values in an
+// object, an array or a class after the `:`. With parentheses, tsc reads on in some of them.
+// - `--sites`: what follows a `( .. )` and a `:` of another kind, with a conditional of its own and without one:
+// `case (a, b):`, a `yield`, `() => async(q)`, `(a, b)`, a conditional under `// prettier-ignore`. Parentheses
+// that tsc does not need can change how it reads the text after such a `:`.
+// - `--paths`: the conditional of the rule at the end of what follows the `:` of another conditional, through arrow
+// functions, conditionals, an assignment, brackets and a block. With `(d) =>` kept in it alone, tsc can read on
+// from a `( .. )` before that other `:`: the rule keeps `(y) =>` after that `:` too. With `f(q, r)` before the `:`
+// of the inner one, only the outer one is misread: tsc reads the body of `y =>` on after that `:`.
+// - `--families`: the conditional of the rule before the `:` of another conditional, with a conditional of its own
+// at the end of the arrow function that has the return type. With `(d) =>` kept in the first one alone, that one
+// can take the `:` of the other: the rule keeps the names after each `:` of such a family.
+// - `--comments`: every pair of the matrix with a comment in one place: before and after each operand, in the
+// parentheses that end the first one, before a `=>`, as `// prettier-ignore` and as a JSDoc type cast.
+// - `--file`: instead of generated sources, the sources in a file: a JSON array of texts.
+// - `--each`: each source of the form `x = ..;` is a cell of a `test.each` table, which Bun prints on one line:
+// there a conditional with an element in it has no parentheses around its operands. Prettier breaks such a cell,
+// so `pairs` is no error with this flag: the `pairs` of `againstMain` are the parentheses that tsc does not need.
+// - `--trailing-comma`: Prettier's option. With `all`, the default, a parameter list that breaks ends in `,`.
+// - `--always`: with the default `arrowParens: "always"` instead of "avoid".
+// - `--width`: one print width or several. The counts are summed.
+// - `--oxfmt`: Bun formats in the flavor of oxfmt. The counts are still about what Prettier prints, and oxfmt puts
+// some comments in other places: use `--main` with `--comments`.
+// - `--babel-ts`: the files are `.js`, read with the parser `babel-ts`, which keeps JSDoc type casts. The parser of
+// `bun fmt` reads no return type before the `:` of a conditional in a `.js` file, so it reads some sources as
+// another program and refuses others, on main too: use `--main`, and compare `refused` with a run of main.
+// - `--reader`: a program that reads the texts in place of the `typescript` package. It gets the directory of
+// `--modules` as its argument and a JSON array of `{ text, ext }` on standard input, and prints one
+// `{ diags, shape }` for each: the messages of the syntax errors, and the tree without parentheses.
+// typescript7.mjs in this directory reads with the native TypeScript. The rule was also checked with TypeScript's
+// parser in Go at the head of microsoft/TypeScript#64233, which gives up a return type if the reading ends in a
+// syntax error. Such a tree has the white space between the children of an element: not for `--jsx`.
+//
+// A source that tsc reports a syntax error for is left out, and with `--reader` one that either reader reports one
+// for. Of the others it counts, and shows a few of each:
+//
+// - `broken`: tsc reads what Prettier prints as the source, and what Bun prints as another program or as a syntax
+// error. There must be none.
+// - `pairs`: it reads both as the source, and Bun prints something else than Prettier: parentheses that tsc does not
+// need. There must be none. `broken` can only come from such parentheses.
+// - `repairedOfMain`: the same, where tsc does not read what `--main` prints as the source. With `--oxfmt`, main
+// prints the parentheses of a JSDoc type cast, which Prettier does not print in a `.ts` file: `(d) =>` after
+// `q => /** @type {F} */ ([q])` is needed there.
+// - `pairsBeforeBrokenList`: the same, where the parameters before the return type are on lines of their own and end
+// in `,`. tsc reports a syntax error for Prettier's text there, and the parser of microsoft/TypeScript#64233 reads
+// it as the source: `(e,)` is no expression. A source counts here only if the reader misreads what Prettier prints
+// for it with `trailingComma: "none"`. If not, it is one of `pairs`.
+// - `pairsAfterBrokenAssertion`: the same, where Prettier breaks a type assertion before the `:` after its `>`. The
+// operand is in parentheses of its own there, `(⏎(q, r)⏎)`, and tsc tries no parameters. Bun keeps `(d) =>`
+// in both layouts. A source counts here only if the reader misreads what Prettier prints for it where the
+// assertion fits on its line. If not, it is one of `pairs`.
+// - `silenced`: tsc reports a syntax error for what Prettier prints, and reads what Bun prints as another program
+// with no error. With `--main`, only where main prints what Prettier prints: in a `.js` file, main prints
+// `async(q)` as `async < U > q`, and that is the other program. There must be none.
+// - `repaired`: it reads what Bun prints as the source, and what Prettier prints as something else.
+// - `kept`: Bun prints something else than Prettier. With `--main`: and than main.
+// - `left`: it reads neither as the source. The rule keeps parentheses only in a family of conditionals with the
+// shape of `a ? b => ({ c }) : (d) => (e): T => f` in it, before a return type that is one name. It leaves
+// `a ? () => ({}) : c => c` and `a ? b => ({ c }) : d => (e): Promise => f`, for which tsc reports a syntax
+// error, and which the parser of microsoft/TypeScript#64233 reads as the source. `silent` are the ones that the
+// reader reads as another program with no error. `refusedAgain` and `changedAgain` are what a second run of Bun
+// does with them: it reports the syntax error, or it prints the program that tsc reads. With `--main`, `anew` are
+// the ones that main prints in another way: tsc still reports a syntax error for another part of the text.
+// `anewChangedAgain` are the ones of them that a second run changes: tsc reads another program with no error
+// there, from main's text too. `againNotAsOnMain` are the ones that main prints in the same way and formats again
+// in another way. There must be none.
+// - `asOnMain`: with `--main`, the sources that main prints as `--bun` does and that would count as `broken` or
+// `pairs`. They are differences from Prettier that this rule does not make.
+// - `againstMain`: with `--main`, the sources that main prints in another way than `--bun`. `broken`: tsc reads
+// main's text as the source, and not Bun's. `pairs`: it reads both as the source, and main's text is none of
+// `pairsBeforeBrokenList`. `silenced`: it reports a syntax error for main's text, and reads Bun's as another
+// program with no error. There must be none of the three. `repaired`: it reads Bun's as the source, and not
+// main's. These count in a cell of a table too, where Prettier prints other lines than main. With `--babel-ts`,
+// `silenced` is no error: main prints `async(q)` as `async < U > q`, which is the other program, and a name
+// that Bun keeps elsewhere in that text takes away the syntax error.
+// - `unstable`: tsc reads what Bun prints as the source, and formatting that changes it. Where Bun prints what
+// Prettier prints, and Prettier changes that on a second run, it is not counted. There must be none.
+// - `refused`: `bun fmt` reports an error for a source, or for what it or Prettier prints for one where tsc reads
+// that as the source. There must be none. A source that it refuses is not in the other counts. With `--main`, a
+// source that main refuses too is in `refusedOnMainToo`: with `--oxfmt`, `(this, id): T => 1` is an error. With
+// `--reader` and with `--babel-ts` the count is no error: the parser of `bun fmt` reports the syntax errors of
+// tsc, which another reader may not report. `refusedAnew` is an error with every flag.
+// - `refusedAnew`: with `--main`, `bun fmt` reports an error for what it prints, and main reports none for what
+// main prints. There must be none.
+// - `leftToEsbuild`: esbuild reads the source, and reports a syntax error for what Bun prints.
+// - `node`: for the sources where Bun prints something else than Prettier, how Node's type stripping reads each text:
+// as the `source`, as some `other` program, or as an `error`. Its `broken` is the sources where it reads what
+// Prettier prints as the source, and not what Bun prints. There must be none.
+import { spawnSync } from "node:child_process";
+import fs from "node:fs";
+import module, { createRequire } from "node:module";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+
+const named = Object.fromEntries(process.argv.slice(2).map(it => it.replace(/^--/, "").split("=")));
+const require = createRequire(join(resolve(named.modules), "noop.js"));
+const prettier = require("prettier");
+const ts = require("typescript");
+const esbuild = (() => {
+ try {
+ return require("esbuild");
+ } catch {
+ return null;
+ }
+})();
+const options = {
+ arrowParens: "always" in named ? "always" : "avoid",
+ ...(named["trailing-comma"] ? { trailingComma: named["trailing-comma"] } : {}),
+ ...("experimental-ternaries" in named ? { experimentalTernaries: true } : {}),
+ ...("babel-ts" in named ? { parser: "babel-ts" } : {}),
+};
+const widths = String(named.width ?? 80)
+ .split(",")
+ .map(Number);
+
+const extension = "jsx" in named ? "tsx" : "babel-ts" in named ? "js" : "ts";
+const oneLine = text => text.trim().replace(/\s+/g, " ");
+
+/** The tree without parentheses, or the first syntax error, as the `typescript` package reads `text`. */
+function readWithTypeScript(text) {
+ const file = ts.createSourceFile(extension === "tsx" ? "a.tsx" : "a.ts", text, ts.ScriptTarget.Latest, true);
+ if (file.parseDiagnostics.length)
+ return `error: ${ts.flattenDiagnosticMessageText(file.parseDiagnostics[0].messageText, " ")}`;
+ const unwrapped = node => (ts.isParenthesizedExpression(node) ? unwrapped(node.expression) : node);
+ const shape = parenthesized => {
+ const node = unwrapped(parenthesized);
+ // Prettier prints `a ?? b ?? c` for `a ?? (b ?? c)`, which means the same.
+ if (ts.isBinaryExpression(node) && ["||", "&&", "??"].includes(node.operatorToken.getText())) {
+ const operands = [];
+ const flatten = operand => {
+ const inner = unwrapped(operand);
+ if (ts.isBinaryExpression(inner) && inner.operatorToken.kind === node.operatorToken.kind) {
+ flatten(inner.left);
+ flatten(inner.right);
+ } else operands.push(shape(inner));
+ };
+ flatten(node);
+ return `(${operands.join(` ${node.operatorToken.getText()} `)})`;
+ }
+ // The line breaks and the indentation that Prettier puts between the children of an element are no text.
+ if (ts.isJsxText(node)) return node.containsOnlyTriviaWhiteSpaces ? "" : oneLine(node.getText());
+ // Prettier removes the `;` after `case a: { k: b };`.
+ if (ts.isEmptyStatement(node)) return "";
+ // Prettier prints `T` for `| T`, which tsc reads as a union of one type.
+ if ((ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) && node.types.length === 1)
+ return shape(node.types[0]);
+ const children = [];
+ node.forEachChild(child => {
+ const text = shape(child);
+ if (text !== "") children.push(text);
+ });
+ return children.length ? `${ts.SyntaxKind[node.kind]}(${children.join(" ")})` : node.getText();
+ };
+ return file.statements.map(shape).join(" ");
+}
+
+const readings = new Map();
+/** Reads the texts that have no reading yet, with `--reader` if there is one. */
+function readAll(texts) {
+ const fresh = Array.from(new Set(texts)).filter(text => typeof text === "string" && !readings.has(text));
+ if (!named.reader) {
+ for (const text of fresh) readings.set(text, readWithTypeScript(text));
+ return;
+ }
+ // One run for every 20,000 texts keeps its input and output below what a string can hold.
+ for (let start = 0; start < fresh.length; start += 20000) {
+ const texts = fresh.slice(start, start + 20000);
+ // A script runs with this Node.
+ const reader = resolve(named.reader);
+ const command = reader.endsWith(".mjs") ? [process.execPath, reader] : [reader];
+ const run = spawnSync(command[0], [...command.slice(1), resolve(named.modules)], {
+ input: JSON.stringify(texts.map(text => ({ text, ext: extension === "tsx" ? "tsx" : "ts" }))),
+ encoding: "utf8",
+ maxBuffer: 2 ** 30,
+ });
+ if (run.error || run.status !== 0)
+ throw run.error ?? new Error(`the reader ended with ${run.status}: ${run.stderr}`);
+ JSON.parse(run.stdout).forEach(({ diags, shape }, index) => {
+ // As above, an empty statement is nothing.
+ const tree = shape.replace(/ (?:Kind)?EmptyStatement ;/g, "");
+ readings.set(texts[index], diags.length ? `error: ${diags[0]}` : tree);
+ });
+ }
+}
+/** The tree without parentheses, or the first syntax error. */
+function read(text) {
+ if (!readings.has(text)) readAll([text]);
+ return readings.get(text);
+}
+
+let state = Number(named.seed ?? 1) >>> 0;
+function random() {
+ state = (state + 0x6d2b79f5) >>> 0;
+ let t = state;
+ t = Math.imul(t ^ (t >>> 15), t | 1);
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
+}
+const pick = list => list[Math.floor(random() * list.length)];
+let count = 0;
+const name = () => pick(["a", "b", "c", "d", "e"]) + (count++ % 7);
+const leaf = () => pick([name, name, name, () => "1", () => '"s"', () => "null", () => "this", () => "true"])();
+const inParentheses = e => `(${e})`;
+/** A single name as a parameter, with parentheses or without. */
+const parameter = x => (random() < 0.5 ? `(${x})` : x);
+
+function arrow(e) {
+ const x = name();
+ return pick([
+ () => `${parameter(x)} => ${e()}`,
+ () => `${parameter(x)} => ${e()}`,
+ () => `${parameter(x)} => ${e()}`,
+ () => `${parameter(x)} => ({ k: ${e()} })`,
+ () => `${parameter(x)} => ({})`,
+ () => `${parameter(x)} => {}`,
+ () => `async ${parameter(x)} => ${e()}`,
+ () => `(${x}, ${name()}) => ${e()}`,
+ () => `() => ${e()}`,
+ () => `() => ({ k: ${e()} })`,
+ () => `() => ({})`,
+ () => `(${x}): T => ${e()}`,
+ () => `(${x}: T) => ${e()}`,
+ () => `(${x}: T) => ({ k: ${e()} })`,
+ () => `({ ${x} }) => ${e()}`,
+ () => `${parameter(x)} => (${e()}, ${e()})`,
+ () => `${parameter(x)} => (${name()} = ${e()})`,
+ ])();
+}
+
+function expression(depth) {
+ if (depth <= 0) return leaf();
+ const e = () => expression(depth - 1);
+ const chance = random();
+ if (chance < 0.35) return arrow(e);
+ if (chance < 0.5) return `${pick([name, () => inParentheses(e())])()} ? ${e()} : ${e()}`;
+ return pick([
+ leaf,
+ () => `[${e()}]`,
+ () => `[${e()}, ${e()}]`,
+ () => `[...${inParentheses(e())}, ${e()}]`,
+ () => `({ k: ${e()} })`,
+ () => `({ k: ${e()}, m: ${e()} })`,
+ () => `({ [${e()}]: ${e()} })`,
+ () => `({ ...${inParentheses(e())} })`,
+ () => `({ k() { return ${e()}; } })`,
+ () => `${inParentheses(e())}.p`,
+ () => `${inParentheses(e())}[${e()}]`,
+ () => `${name()}[${e()}]`,
+ () => `${name()}.p[${e()}]`,
+ () => `${inParentheses(e())}(${e()})`,
+ () => `f(${e()})`,
+ () => `f(${e()}, ${e()})`,
+ () => `new F(${e()})`,
+ () => `keyof(${e()})`,
+ () => `o.m(${e()})`,
+ () => `f(${e()})`,
+ () => `async(${e()})`,
+ () => `${inParentheses(e())}`,
+ () => `${inParentheses(e())} | ${inParentheses(e())}`,
+ () => `${inParentheses(e())} || ${inParentheses(e())}`,
+ () => `${inParentheses(e())} ?? ${inParentheses(e())}`,
+ () => `${inParentheses(e())} + ${inParentheses(e())}`,
+ () => `${inParentheses(e())} < ${inParentheses(e())}`,
+ () => `${name()} < ${e()}`,
+ () => `${name()} << ${e()}`,
+ () => `(${e()}, ${e()})`,
+ () => `(${name()} = ${e()})`,
+ () => `${name()} = ${e()}`,
+ () => `!${inParentheses(e())}`,
+ () => `typeof ${inParentheses(e())}`,
+ () => "`${" + e() + "}`",
+ () => "t`${" + e() + "}`",
+ () => `${inParentheses(e())} as T`,
+ () => `${inParentheses(e())}!`,
+ () => `function () { return ${e()}; }`,
+ () => `class { m = ${e()}; }`,
+ ])();
+}
+
+const conditional = depth =>
+ `${pick([name, name, () => inParentheses(expression(depth - 1))])()} ? ${expression(depth)} : ${expression(depth)}`;
+const positions = [
+ c => c,
+ c => c,
+ c => c,
+ c => `p ? ${c} : z`,
+ c => `p ? (${c}) : z`,
+ c => `p ? (r) => (${c}) : z`,
+ c => `p ? w ? v : ${c} : z`,
+ c => `f(${c}, ${arrow(leaf)})`,
+ c => `[${c}, ${arrow(leaf)}]`,
+ c => `f(${c}, ${name()}, [${arrow(() => arrow(leaf))}])`,
+ c => `f(${name()} => ${c}, ${arrow(leaf)})`,
+ c => `(${c}, ${arrow(leaf)})`,
+];
+const statements = [
+ e => `const v0 = ${e};`,
+ e => `x0 = ${e};`,
+ e => `const f0 = async () => { x0 = ${e}; };`,
+ e => `function* g() { yield ${e}; }`,
+ e => `switch (v) { case ${e}: break; }`,
+ e => `export default ${e};`,
+ e => `f(${e});`,
+ e => `class C { p = ${e}; }`,
+];
+
+function matrix() {
+ const before = [
+ ..."(q) => ({})|(q) => ({ c: q })|(q) => ({ c: f() })|(q) => (q, r)|(q) => (q = r)|(q) => ([q] = r)|(q) => ({ q } = r)".split(
+ "|",
+ ),
+ ..."(a, b)|(b = 1)|([a] = b)|({ a } = b)|async(q)|async(q)|(q, r)|(q) => async(q)|(q, r) => ({})|({ q }) => ({})".split(
+ "|",
+ ),
+ ..."(q = 1) => ({})|async (q) => ({})|(q) => (r) => ({})|(q) => (r += q)|(q) => ({ q }).r|(q) => (q.r, 2)".split(
+ "|",
+ ),
+ ..."(q) => (b ? c : d)|(q) => ([b] ? c : d)|(q) => [q]|(q) => q|(q) => { return {}; }|({} as T)|(a as T)|([a] ?? b)".split(
+ "|",
+ ),
+ ..."(q) => (r = (s) => ({}))|(q) => (r, (s) => ({}))|b ? c : (q) => ({})|(b ? c : (q) => ({}))|([b].c ? d : () => ({}))".split(
+ "|",
+ ),
+ ..."() => ({})|() => ({ c: f() })|() => ({ c: a.b })|() => ({ c: [b] })|() => ({ ...a })|() => ({ c: (d) => e })".split(
+ "|",
+ ),
+ ..."(a: T) => ({ c: q })|(a?) => ({})|(...a) => ({})|async () => ({})|() => (a, b)|() => (a = b)|() => ([a], b)".split(
+ "|",
+ ),
+ ..."() => ([b] ? c : d)|() => (a ? b : c)|() => []|() => (q) => ({})|(): T => ({})|(a) => ({})|() => async(q)".split(
+ "|",
+ ),
+ ..."async(1)|async(q.r)|(1, r)|(q.r, s)|() => ({ c: (d) => e, f: [(g) => h] })|(a = 1, b)".split("|"),
+ ..."(a as T, b)|(a satisfies T)|(q) => (a as T, b)|(q) => (a = 1, b)|(q) => (a! = b)|(q) => ((a as T) = b)".split(
+ "|",
+ ),
+ ..."(q) => (a, b)|(q) => (a, b)|(q) => ((a = 1) as T, b)|(q) => (a.b as T, c)".split("|"),
+ ..."((d) => e)|(q = r)|async((d) => e)|async(q = r)|async(async (d) => e)|async(q = r)".split("|"),
+ ..."async((q, r))|async(q = r, s)|async(q as T)|async(q.r = s)".split("|"),
+ ..."(() => ({}))|((q) => ({ c: q }))|() => (({}))".split("|"),
+ ..."b ? c : () => ({})|b ? c : d ? e : () => ({})|b ? (q) => ({}) : () => ({ c: q })".split("|"),
+ // What tsc reads as parameters, and what it does not: a pattern after `...` in an object, a default in a target.
+ ..."(q) => ({ ...[...a] })|(q) => ({ ...{ a } })|(q) => ({ a: [b, , ...c], ...d })|(q) => ({ a: b = 1 })".split(
+ "|",
+ ),
+ ..."({ a = 1 } = r)|([a = 1] = r)|(q) => ({ a = 1 } = r)|(q) => ([a = 1, { b: c = 2 }] = r)".split("|"),
+ // A body that loses its parentheses when it breaks, and conditionals with a `( .. ) :` of their own.
+ ..."(q) => (b ? c : (x, y))|b ? (x, y) : (inner) => ({ q })|b ? (inner) => ({ q }) : (x, y)".split("|"),
+ ..."b ? (q) => ({ q }) : (s) => f(s)|(this, r)".split("|"),
+ // A type assertion to a keyword: `any` can be the name of a type parameter, and `void` cannot.
+ ..."(q, r)|(q, r)".split("|"),
+ ];
+ const after = [
+ ..."(x) => x|(x) => (y) => 1|(x) => (id): T => 1|(x) => (y) => (id): T => 1|() => (y) => (id): T => 1".split("|"),
+ ..."(x, z) => (y) => 1|(x) => (b: X) => (y) => (id): T => 1|(x) => async (y): T => 1|(x) => (y): T => 1".split(
+ "|",
+ ),
+ ..."(x) => (y: X): T => 1|(x) => { return (id): T => 1; }|(x) => [(id): T => 1]|async (x) => x|(x, y) => x".split(
+ "|",
+ ),
+ ..."(x): T => (y) => 1|((x) => y)()|((x) => (id): T => y)()|((x) => y, z)|(x) => (b ? c : (id): T => y)".split("|"),
+ ..."(x) => (y = (id): T => 1)|x|f((x) => y)|items.map((x) => x.id)|[(x) => y]|{ a: (x) => y }|a[(x) => y]".split(
+ "|",
+ ),
+ ..."a < ((x) => y)|a << [(x) => y]|keyof((x) => y)|t ? (x) => y : w|(t ? (x) => y : w)(1)".split("|"),
+ ...["z || ((x) => y)", "a < b", "a << b", "(x) => a < b", "z | a < b", "(t ? u : w) ? (x) => y : w"],
+ ...["null", '"s".concat((x) => y)', "1 + ((x) => y)", '"s"[(x) => y]', "1 | ((x) => y)", "{ m() {} }", "class {}"],
+ "() => { if (a) { return 1; } }",
+ "`${(x) => y}`",
+ // With parentheses around the parameter, TypeScript reads on: the bare parameter is the one that it reads right.
+ "{ m: async (x) => { if (x) { return 1; } } }",
+ "{ m: (x) => (y) => { if (y) { return 1; } } }",
+ "{ m: async (x) => function () {} }",
+ "{ m: (x) => class {} }",
+ "() => ({ m: async (x) => { if (x) { return 1; } } })",
+ "((x) => (a, b)) | function () {}",
+ "{ m: (x) => x.d, n() { return 1; } }",
+ "{ fetch: async (x) => load(x), reset() { return 1; } }",
+ "(s: S) => ({ on: async (x) => { if (!x) { return; } await s.save(x); } })",
+ // Return types that are no single name: tsc reports a syntax error for what Prettier prints.
+ ..."(x) => (id): Promise => 1|(x) => (id): void => 1|(x) => (id): T[] => 1|(x) => (id): A.B => 1".split("|"),
+ ...["(x) => (id): T | U => 1", "(x) => (id): id is T => true", "(x) => (id): string => 1"],
+ // Other heads before and with the return type.
+ ..."(a, b) => (d) => (id): T => 1|(x) => async (g) => (e): T => 1|(x) => (e = 1): T => 1".split("|"),
+ ..."(x) => (id, key: U): T => 1|(x) => (id, ...rest): T => 1|(x) => (id, key?): T => 1".split("|"),
+ ..."(x) => ({ id }): T => 1|(x) => (id, key): T => 1".split("|"),
+ `${Array.from({ length: 20 }, (_, index) => `(n${index}) => `).join("")}(id): T => 1`,
+ // A return type right after the `:`, and conditionals that end an arrow function after it: tsc can read on.
+ ..."(id): T => 1|(x) => t ? () => ({}) : (y) => (id): T => 1|(x) => t ? (q) => ({ q }) : (y) => (id): T => 1".split(
+ "|",
+ ),
+ ..."(x) => t ? u : (y) => (id): T => 1|(a: U) => t ? () => ({}) : (y) => (id): T => 1".split("|"),
+ ..."(x) => () => t ? () => ({}) : (y) => (id): T => 1|() => (y) => t ? (q) => ({ q }) : (z) => (id): T => 1".split(
+ "|",
+ ),
+ // Names that start a type, and return types that are reserved words: tsc reports a syntax error for them.
+ ..."(keyof) => (id): T => 1|(readonly) => (id): T => 1|(unique) => (id): T => 1|(infer) => (id): T => 1".split("|"),
+ ..."(x) => (keyof) => (id): T => 1|(a, b) => (keyof) => (id): T => 1|(asserts) => (id): T => 1".split("|"),
+ ..."(x) => (id): enum => 1|(x) => (id): class => 1|(x) => (id): await => 1|(x) => (id): yield => 1".split("|"),
+ // A conditional of its own at the end of the arrow function with the return type: it can take a `:` that follows.
+ ..."(x) => (id): T => t ? (m, n) : (d) => 1|(x) => (id): T => t ? (q) => ({ q }) : (d) => 1".split("|"),
+ ..."(x) => (id): T => (w) => t ? (m, n) : (d) => 1|(x) => (id): T => t ? u : (d) => 1".split("|"),
+ // Parameters before the return type that are expressions too, and ones that are not.
+ ..."(x) => (id): T => 1|(x) => ({ id = 1 }): T => 1|(x) => ([id, key]): T => 1|(x) => (this, id): T => 1".split(
+ "|",
+ ),
+ // Two return types, and a return type before parameters that tsc is sure of: it gives up the last one that it
+ // can read on from.
+ ..."(x) => (p): Promise => (g) => (id): T => 1|(x) => (p): R => (g) => (id): Promise => 1".split("|"),
+ ..."(x) => (id): T => (s: S) => 1|(x) => (p, ...r): R => (g) => (id): T => 1|(x) => (this): T => 1".split("|"),
+ // Names long enough for the parameters to be on lines of their own, where they end in `,`.
+ `(x) => (${Array.from({ length: 4 }, (_, index) => `aLongNameOfAParameter${index}`).join(", ")}): T => 1`,
+ ];
+ const around = [
+ (b, a) => `cond ? ${b} : ${a}`,
+ (b, a) => `p ? (cond ? ${b} : ${a}) : z`,
+ (b, a) => `p ? (r) => (cond ? ${b} : ${a}) : z`,
+ (b, a) => `p ? (w ? v : cond ? ${b} : ${a}) : z`,
+ (b, a) => `f(cond ? ${b} : ${a}, (s) => (t) => s)`,
+ (b, a) => `[cond ? ${b} : ${a}, d, [(s) => s]]`,
+ (b, a) => `f((r) => cond ? ${b} : ${a}, (s) => s)`,
+ ];
+ const sources = [];
+ for (const make of around) {
+ for (const b of before) {
+ for (const a of after) sources.push(`const v0 = ${make(b, a)};\n`, `x0 = ${make(b, a)};\n`);
+ }
+ }
+ for (const b of before) {
+ for (const a of after) {
+ sources.push(
+ `switch (v) { case cond ? ${b} : ${a}: break; }\n`,
+ `x0 = { k: cond ? ${b} : ${a}, [(s) => s]: 1 };\n`,
+ `x0 = import(cond ? ${b} : ${a}, { with: (s) => s });\n`,
+ );
+ }
+ }
+ return sources;
+}
+
+function jsxMatrix() {
+ const sources = [
+ ..."x = a ? (b ? (q) => ({ x }) : (deps) => y) : ;|x = a ? (b ? () => ({}) : (deps) => y) : ;".split("|"),
+ ..."x = a ? : b ? (q) => ({ x }) : (deps) => (id): T => y;|x = a ? : b ? () => ({}) : (deps) => y;".split(
+ "|",
+ ),
+ ..."x = a ? (b ? (q) => ({ x }) : ) : (deps) => (id): T => y;|x = a ? () => ({}) : (deps) => ;".split(
+ "|",
+ ),
+ ..."x = a ? (b ? : (q) => ({ x })) : (deps) => (id): T => y;|x = a ? ((q) => ({ x })) : ;".split("|"),
+ ..."x = a ? (b ? : () => ({})) : (deps) => y;|x = a ? (q, r) : b ? : (deps) => (id): T => y;".split(
+ "|",
+ ),
+ "x = a ? (b ? (q) => ({ x }) : (deps) => y) : c ? : (z) => z;",
+ "x = a ? : (b ? (q) => ({ x }) : (deps) => y) ? (u) => u : (w) => w;",
+ "x = a ? (v) => (b ? (q) => ({ x }) : (deps) => y) : ;",
+ "x = a ? (v) => (b ? () => ({}) : (deps) => ) : z;",
+ "x = a ? (b ? c : (q) => ({ x })) : (deps) => (id): T => ;",
+ "x = {a ? (b ? (q) => ({ x }) : (deps) => y) : z};",
+ "x = {a ? () => ({}) : (deps) => y};",
+ "x = ({ x }) : (deps) => (id): T => y} />;",
+ "x = a ? (b ? (q) => ({ x }) : (deps) => y) : {c ? () => ({}) : (z) => z};",
+ "x = a ? (q: T) => ({}) : (deps) => y;",
+ "x = a ? (q) => ({}) : (deps) => (id): U => y;",
+ "x = a ? (p) => (q) => ({}) : (deps) => (id): U => y;",
+ "x = a ? (q) => ({}) : (deps) => (r) => (id): U => y;",
+ ].map(source => `${source}\n`);
+ const before = [
+ ..."a|{ a: 1 }|[a]|(a = 1)|(a, b)|this|a.b|f(a)|(q) => ({ x })|() => ({})|(q) => q|null|b ? c : d".split("|"),
+ ..."b ? c : { d }|{ d } ? b : c||async (q) => q|() => ({ x: (q) => q })".split("|"),
+ ];
+ const after = [
+ ..."(c) => (d): T => e|(c) => e|(c) => (d) => e|(c) => |(c) => (d): T => ".split("|"),
+ ..."b ? (c) => (d): T => e : z|[(c) => e]|null|items.map((c) => c.id)".split("|"),
+ ..."{items.map((c) => c.id)}| e} />|<>{(c) => e}>|(d){(c) => (d): T => e}".split("|"),
+ " e}>(d){(f) => g}",
+ ];
+ for (const test of ["", "t"]) {
+ for (const b of before) {
+ for (const a of after) {
+ const conditional = `${test} ? ${b} : ${a}`;
+ sources.push(
+ `x = ${conditional};\n`,
+ `x = p ? (${conditional}) : ;\n`,
+ `x = p ? : ${conditional};\n`,
+ );
+ }
+ }
+ }
+ return sources;
+}
+
+function valuesMatrix() {
+ const before =
+ "() => ({})|(a, b)|q => ({ c: q })|(a: T) => ({ c: a })|async(q)|() => (a ? b : c)|(b = 1)|q => q".split("|");
+ const values = [
+ ..."async c => function () { return 1; }|async c => function* () {}|async c => class {}".split("|"),
+ ..."async c => { return 1; }|async c => ({})|async c => c|async c => async function () {}".split("|"),
+ ..."async c => () => { return 1; }|async c => d => { return 1; }|async c => new (class {})()".split("|"),
+ ..."async c => function () {}.call(c)|c => function () { return 1; }|c => class {}".split("|"),
+ ..."c => d => function () { return 1; }|async c => async d => function () { return 1; }".split("|"),
+ ..."async c => [function () {}]|async c => ({ m() {} })|async c => `${function () {}}`".split("|"),
+ "async c => typeof function () {}",
+ ];
+ const after = [
+ v => `{ m: ${v} }`,
+ v => `{ k: 1, m: ${v} }`,
+ v => `{ m() { return ${v}; } }`,
+ v => `[${v}]`,
+ v => `{ k: [${v}] }`,
+ v => `{ k: { m: ${v} } }`,
+ v => `class { m = ${v}; }`,
+ v => `(y) => ({ m: ${v} })`,
+ v => `{ ...o, m: ${v} }`,
+ v => `{ [k]: ${v} }`,
+ v => `{ m: ${v}, n: 1 }`,
+ ];
+ const statements = [c => `x = ${c};`, c => `const x = ${c};`, c => `function g() { return ${c}; }`, c => `f(${c});`];
+ const sources = [];
+ for (const statement of statements) {
+ for (const b of before) {
+ for (const a of after) for (const v of values) sources.push(`${statement(`q ? ${b} : ${a(v)}`)}\n`);
+ }
+ }
+ return sources;
+}
+
+function sitesMatrix() {
+ const sites = [
+ a => `x = c6 ? () => ({}) : ${a};`,
+ a => `x = c6 ? (a, b) : ${a};`,
+ a => `x = c6 ? (a = b) : ${a};`,
+ a => `x = c6 ? async(q) : ${a};`,
+ a => `x = c6 ? (a, b) : ${a};`,
+ a => `function* g() {\n x = c6 ? yield (a, b) : ${a};\n}`,
+ a => `function* g() {\n x = c6 ? yield () => ({}) : ${a};\n}`,
+ a => `x = c6 ? () => async(q) : ${a};`,
+ a => `x = c6 ? (q) => async(q) : ${a};`,
+ a => `x = c6 ? (p: T) => (a, b) : ${a};`,
+ a => `x = c6\n ? // prettier-ignore\n t ? u : () => ({})\n : ${a};`,
+ a => `switch (v) {\n case (a, b):\n ${a};\n}`,
+ a => `switch (v) {\n case () => ({}):\n ${a};\n}`,
+ a => `switch (v) {\n case async(q):\n ${a};\n}`,
+ ];
+ const tests = ["t", "({ k: d0 })", "[t]", "t.u", "t!"];
+ const before = [
+ ..."(c1) => ({ k: null })|(c1) => ({ k })|(c1) => (c2, c3)|() => ({})|(c1: T) => ({})|async (c1) => ({})".split(
+ "|",
+ ),
+ ..."(c1) => (c2 = c3)|(c1) => (c2) => ({})".split("|"),
+ ];
+ const after = [
+ ..."b2 < null|(y) => y|[v]|{ m() {} }|(y) => (id): T => y|(y) => (z) => (id, w): T => y|new A()".split("|"),
+ ...["await2", "v << w", "v | w", "(y) => (id): Promise => y"],
+ ];
+
+ const around = [
+ c => `(${c}) ?? z`,
+ c => `[${c}]`,
+ c => `{ k: ${c} }`,
+ c => `f(${c})`,
+ c => `(y) => ${c}`,
+ c => `(a1: T) => ${c}`,
+ c => c,
+ c => `z || (${c})`,
+ c => `{ [${c}]: 1 }`,
+ c => "`${" + c + "}`",
+ c => `new X(${c})`,
+ c => `(${c}) as T`,
+ c => `f(${c})`,
+ c => `(${c})(1)`,
+ c => `(${c}).k`,
+ c => `{ m: (a1: T) => ${c} }`,
+ c => `[(a1) => ${c}]`,
+ ];
+ const sources = [];
+ for (const site of sites) {
+ for (const test of tests) {
+ for (const b of before) {
+ for (const a of after) for (const make of around) sources.push(`${site(make(`${test} ? ${b} : ${a}`))}\n`);
+ }
+ }
+ // What follows the `:` of the site with no conditional of its own.
+ for (const a of after) sources.push(`${site(a)}\n`);
+ }
+ return sources;
+}
+
+/** The conditional of the rule at the end of what follows the `:` of another conditional, where tsc can read on. */
+function pathsMatrix() {
+ const outerBefore = [
+ ..."(a, b)|(q) => ({ q })|() => ({})|async(q)|z|(b = 1)|(a, b)|(q) => [q]|(p: T) => ({})".split("|"),
+ // A conditional before the `:`: where it breaks, it has no parentheses. One that an arrow function returns can.
+ ..."b2 ? c2 : (a, b)|b2 ? c2 : (q) => ({ q })|(r2) => b2 ? c2 : (a, b)".split("|"),
+ ];
+ const paths = [
+ ...[c => `(y) => ${c}`, c => `(y) => (z) => ${c}`, c => `(y) => t2 ? u : ${c}`, c => `t2 ? u : (y) => ${c}`],
+ ...[c => `(y) => t2 ? (k) => ({ k }) : ${c}`, c => `(y) => (a1: T) => ${c}`, c => `(a1: T) => (y) => ${c}`],
+ ...[c => `(y) => async (z) => ${c}`, c => `async (y) => ${c}`, c => `(y) => (z = ${c})`, c => `(y) => (${c})`],
+ ...[c => `(y) => [${c}]`, c => `(y, z) => ${c}`, c => `(y) => (z, w) => ${c}`, c => `() => (y) => ${c}`],
+ ...[c => `(y) => () => ${c}`, c => `(y) => ({ z }) => ${c}`, c => `(y) => (z): U => ${c}`, c => `(y) => z = ${c}`],
+ ...[c => `(y) => t2 ? () => ({}) : (z) => ${c}`, c => `(y) => t2 ? (x2, y2) : t3 ? u : ${c}`],
+ ...[c => `(y) => { return ${c}; }`, c => `(y) => f(${c})`],
+ // A return type ends what tsc can read as a type: the names after it stay as they are.
+ ...[c => `(y): U => (z) => ${c}`, c => `(a1: T): U => (z) => ${c}`, c => `(y): U => (z) => ${c}`],
+ ...[c => `(y) => (z) => ${c}`, c => `({ y }) => (z) => ${c}`, c => `(y = 1) => (z) => ${c}`],
+ ];
+ // With `f(q, r)` and `u` before the `:`, the inner conditional is not misread for itself: the outer one is.
+ const before = "() => ({})|(q) => ({ q })|(c: T) => ({})|(m, n)|async (q) => ({ q })|f(q, r)|u".split("|");
+ const after = [
+ ..."(d) => (e): T => f|(d) => (g) => (e): T => f|(d) => (e, h): T => { return 1; }".split("|"),
+ // A return type right after the `:`, with `async`, and two of them.
+ ..."(e): T => f|(e): T => (g) => f|async (d) => (e): T => f|(u): R => (d) => (e): T => f".split("|"),
+ ..."(u): Promise => (d) => (e): T => f|(d) => (e): T => (s: S) => f".split("|"),
+ ];
+ const statements = [
+ ...[c => `x = ${c};`, c => `x = p ? (${c}) : z;`, c => `f(${c}, (s) => s);`, c => `const v = p ? w : ${c};`],
+ ];
+ const sources = [];
+ for (const outer of outerBefore) {
+ for (const path of paths) {
+ for (const b of before) {
+ for (const a of after) {
+ for (const statement of statements)
+ sources.push(`${statement(`c6 ? ${outer} : ${path(`t ? ${b} : ${a}`)}`)}\n`);
+ }
+ }
+ }
+ }
+ return sources;
+}
+
+/** The conditional of the rule before the `:` of another conditional, or with a conditional at the end of its own. */
+function familiesMatrix() {
+ const sources = [];
+ // The rule's conditional where a `:` follows it, and a body after the return type that can take that `:`.
+ const contexts = [
+ ...[c => `x = p ? (${c}) : w;`, c => `x = p ? (y) => (${c}) : w;`, c => `x = p ? (t2 ? u2 : ${c}) : w;`],
+ ...[c => `x = p ? (y) => (z2) => (${c}) : w;`, c => `x = p ? () => (${c}) : w;`],
+ ...[
+ c => `x = p ? (a1: T) => (${c}) : w;`,
+ c => `x = p ? w : q2 ? (${c}) : w3;`,
+ c => `f(p ? (${c}) : w, (s) => s);`,
+ ],
+ ...[c => `x = p ? (${c}) : (w) => w;`, c => `x = p ? (${c}) : (w): T => w;`],
+ ...[c => `const v = p ? (y) => (t2 ? u2 : ${c}) : w;`, c => `x = p0 ? (p ? (${c}) : w) : w4;`],
+ ...[c => `x = ${c};`, c => `switch (v) { case ${c}: break; }`, c => `x = q ? p ? ${c} : z : w;`],
+ ...[
+ c => `x = p ? [${c}] : z;`,
+ c => `x = p ? f(${c}) : z;`,
+ c => `x = p ? z : ${c};`,
+ c => `x = { k: ${c}, l: 1 };`,
+ ],
+ ...[c => `x = p ? (w = ${c}) : z;`, c => `x = p ? (r) => ${c} : z;`],
+ ];
+ const before = ["(q) => ({ q })", "() => ({})", "(m, n1)", "(q) => (q = r)"];
+ const heads = ["(d) => (e): T", "(d) => (g) => (e): T", "(d) => (e, h): string"];
+ const bodies = [
+ ..."f|u ? (v, w2) : (z) => m|u ? (v, w2) : (z) => (n) => m|u ? (k) => ({ k }) : (z) => m".split("|"),
+ ..."u ? () => ({}) : (z) => m|u ? v : (z) => m|u ? (v, w2) : (z, n) => m|u ? (v, w2) : (z, n) => (o) => m".split(
+ "|",
+ ),
+ ..."u ? (v, w2) : async (z) => m|(y2) => u ? (v, w2) : (z) => m|u ? v : t3 ? (v, w2) : (z) => m".split("|"),
+ ..."u ? (v, w2) : (z) => (e2): U => m|u ? (k) => ({ k }) : (z) => (e2): U => m|[u ? (v, w2) : (z) => m]".split("|"),
+ ..."u ? (v, w2) : m|u ? (v = 1) : (z) => m|{ return u ? (v, w2) : (z) => m; }".split("|"),
+ ..."(z3 = u ? (v, w2) : (z) => m)|u ? (v, w2) : (z) => t4 ? (v3, w4) : (z5) => m".split("|"),
+ ..."u ? (k) => ({ k }) : (z) => (e2): U => t4 ? (v3, w4) : (z5) => m|u ? (v, w2) : (z: D) => m".split("|"),
+ ..."u ? (v, w2) : f(z)|u ? (k) => ({ k }) : (z) => (e2): Promise => m|g(u ? (v, w2) : (z) => m)".split("|"),
+ ..."u ? (k) => k : (z) => m|u ? [v] : (z) => m|u ? (k) => ({ c: g() }) : (z) => m".split("|"),
+ ];
+ for (const context of contexts) {
+ for (const b of before) {
+ for (const head of heads) {
+ for (const body of bodies) sources.push(`${context(`cond ? ${b} : ${head} => ${body}`)}\n`);
+ }
+ }
+ }
+ // A conditional before the `:` of another one that can take that `:`, next to the rule's conditional.
+ const others =
+ "t ? (m, n) : (d) => f|t ? (k) => ({ k }) : (d) => f|(r) => t ? (m, n) : (d) => f|t ? (m, n) : f".split("|");
+ const rules = "c ? (q) => ({ q }) : (d2) => (e): T => g|(y) => c ? (q) => ({ q }) : (d2) => (e): T => g".split("|");
+ for (const other of others) {
+ for (const rule of rules) {
+ sources.push(`x = o ? ${other} : ${rule};\n`, `x = o ? ${rule} : ${other};\n`);
+ sources.push(`x = o ? (y) => o2 ? ${other} : ${rule} : z;\n`, `x = o ? o2 ? ${rule} : z : ${other};\n`);
+ }
+ }
+ return sources;
+}
+
+/** The operands of the matrix that the rule is about, and some that it leaves, with a comment in one place. */
+function commentsMatrix() {
+ const before = [
+ ..."(q) => ({})|(q) => ({ c: q })|(q) => ({ q })|(q) => (q, r)|(q) => (q = r)|(q) => ({ q } = r)|() => ({})".split(
+ "|",
+ ),
+ ..."(q) => (r) => ({ q, r })|async (q) => ({ q })|(a: T) => ({ a })|(q) => ({ c: f() })|(q) => [q]|(a, b)".split(
+ "|",
+ ),
+ // What has its `( .. )` only in a type cast, or as it is written under `// prettier-ignore`.
+ ..."(q) => r|(q) => [q, r]|(q) => (a = 1, b)|(q) => async(r)|(q) => (r)|(q)".split("|"),
+ ];
+ const after = [
+ ..."(x) => (id): T => 1|(x) => (y) => (id): T => 1|(x) => (id, z): T => { return 1; }|(x) => x|(x) => (y) => 1".split(
+ "|",
+ ),
+ ..."(x, z) => (id): T => 1|() => (y) => (id): T => 1|(x) => async (id): T => 1|{ m() {} }|a < b".split("|"),
+ ];
+ const places = [
+ (b, a) => `cond ? /* c */ ${b} : ${a}`,
+ (b, a) => `cond ? ${b} /* c */ : ${a}`,
+ (b, a) => `cond ? ${b.replace(/\)$/, " /* c */)")} : ${a}`,
+ (b, a) => `cond ? ${b.replace(" => ", " => /* c */ ")} : ${a}`,
+ (b, a) => `cond ? ${b.replace(" => ", " /* c */ => ")} : ${a}`,
+ (b, a) => `cond ? ${b} : /* c */ ${a}`,
+ (b, a) => `cond ? ${b} : ${a.replace(" => ", " /* c */ => ")}`,
+ (b, a) => `cond ? ${b} : ${a.replace(" => ", " => /* c */ ")}`,
+ (b, a) => `cond ? ${b} : ${a.replace(/\): T/, " /* c */): T")}`,
+ (b, a) => `cond ? ${b} : ${a.replace(/: T => /, ": T => /* c */ ")}`,
+ (b, a) => `cond /* c */ ? ${b} : ${a}`,
+ (b, a) => `cond\n ? // prettier-ignore\n ${b}\n : ${a}`,
+ (b, a) => `cond\n ? ${b}\n : // prettier-ignore\n ${a}`,
+ (b, a) => `cond ? ${b.replace(" => ", " => // prettier-ignore\n ")} : ${a}`,
+ (b, a) => `cond ? ${b} : ${a.replace(" => ", " => // prettier-ignore\n ")}`,
+ (b, a) => `cond ? /** @type {F} */ (${b}) : ${a}`,
+ (b, a) => `cond ? ${b.replace(" => ", " => /** @type {F} */ (")}) : ${a}`,
+ (b, a) => `cond ? ${b} : /** @type {F} */ (${a})`,
+ (b, a) => `cond ? ${b} : ${a.replace(" => ", " => /** @type {F} */ (")})`,
+ (b, a) => `cond ? ${b} // c\n : ${a}`,
+ (b, a) => `cond ? // c\n ${b} : ${a}`,
+ ];
+ const sources = [];
+ for (const place of places) {
+ for (const b of before) {
+ for (const a of after) sources.push(`x0 = ${place(b, a)};\n`, `x0 = p ? (${place(b, a)}) : z;\n`);
+ }
+ }
+ return sources;
+}
+
+function random_sources() {
+ const sources = [];
+ while (sources.length < Number(named.count ?? 5000)) {
+ const source = `${pick(statements)(pick(positions)(conditional(1 + Math.floor(random() * 3))))}\n`;
+ if (source.includes("=>")) sources.push(source);
+ }
+ return sources;
+}
+
+/** What Bun prints for each text that isn't null: null for a text that it reports an error for and leaves as it is. */
+function format(texts, printWidth, bun = named.bun) {
+ const directory = fs.mkdtempSync(join(tmpdir(), "arrow-parens-"));
+ const config = "oxfmt" in named ? ".oxfmtrc.json" : ".prettierrc";
+ // A parser for every file would read the configuration file with it too.
+ const { parser, ...others } = options;
+ const overrides = parser ? { overrides: [{ files: `*.${extension}`, options: { parser } }] } : {};
+ fs.writeFileSync(join(directory, config), JSON.stringify({ ...others, printWidth, ...overrides }));
+ const path = index => join(directory, `${index}.${extension}`);
+ texts.forEach((text, index) => text != null && fs.writeFileSync(path(index), text));
+ const run = spawnSync(resolve(bun), ["fmt", "--log-level=error", "."], {
+ cwd: directory,
+ encoding: "utf8",
+ maxBuffer: 2 ** 30,
+ });
+ if (run.error) throw run.error;
+ // Under each error: ` at 12.ts:1:31`
+ const failed = new Set(Array.from(run.stderr.matchAll(/^\s+at (\d+)\.(?:tsx?|js)\b/gm), match => Number(match[1])));
+ if (run.status !== 0 && failed.size === 0)
+ throw new Error(`bun fmt ended with ${run.signal ?? run.status}:\n${run.stderr.slice(0, 2000)}`);
+ const printed = texts.map((text, index) => {
+ if (text == null) return false;
+ return failed.has(index) ? null : fs.readFileSync(path(index), "utf8");
+ });
+ fs.rmSync(directory, { recursive: true, force: true });
+ return printed;
+}
+
+const refusals = new Map();
+function refusedByEsbuild(text) {
+ if (!refusals.has(text)) {
+ try {
+ esbuild.transformSync(text, { loader: extension });
+ refusals.set(text, false);
+ } catch {
+ refusals.set(text, true);
+ }
+ }
+ return refusals.get(text);
+}
+
+/** The program that Node runs for `text`: what its type stripping leaves, as esbuild reads that. Null for an error. */
+function byNode(text) {
+ try {
+ return esbuild.transformSync(module.stripTypeScriptTypes(text), { loader: "js", minify: true }).code;
+ } catch {
+ return null;
+ }
+}
+const countsNode = esbuild != null && extension === "ts" && typeof module.stripTypeScriptTypes === "function";
+
+// `(x) =>` as `x =>`.
+const bared = source => source.replace(/\((\w+)\) =>/g, "$1 =>");
+const generators = { values: valuesMatrix, sites: sitesMatrix, paths: pathsMatrix, families: familiesMatrix };
+Object.assign(generators, { comments: commentsMatrix, jsx: jsxMatrix, matrix });
+const generator = Object.keys(generators).find(kind => kind in named);
+const generated =
+ "file" in named ? JSON.parse(fs.readFileSync(named.file, "utf8")) : (generators[generator] ?? random_sources)();
+// `x = ..;` as a cell of a table.
+const inCell = source => {
+ const value = /^(?:const \w+|\w+) = (.*);\n$/.exec(source)?.[1];
+ return value == null || value.includes("`") ? source : `test.each\`\n a\n \${${value}}\n\`("x", () => {});\n`;
+};
+const made = generated
+ .map(source => ("bare" in named ? bared(source) : source))
+ .map("each" in named ? inCell : it => it);
+readAll(made);
+// Prettier reads the source with the `typescript` package, so a source is one that both readers read.
+const isRead = source =>
+ !read(source).startsWith("error: ") && !(named.reader && readWithTypeScript(source).startsWith("error: "));
+const sources = made.filter(isRead);
+const sourceSet = new Set(sources);
+
+async function judge(printWidth) {
+ const byPrettier = text =>
+ prettier.format(text, { ...options, printWidth, filepath: `a.${extension}` }).catch(() => null);
+ const ours = format(sources, printWidth);
+ const byMain = named.main ? format(sources, printWidth, named.main) : [];
+ // Where main prints the same text, a difference from Prettier is not from this rule.
+ const isAsOnMain = index => byMain[index] != null && byMain[index] === ours[index];
+ const theirs = await Promise.all(sources.map(byPrettier));
+ readAll([...ours, ...theirs, ...byMain]);
+ // Parameters before a return type, on lines of their own: `(e,)` is no expression. Parentheses that Bun keeps in
+ // such a text are no error if the reader misreads what Prettier prints for the source without the `,`.
+ const hasListWithComma = text => typeof text === "string" && /,\n\s*\): /.test(text);
+ const withComma = sources
+ .map((_, index) => index)
+ .filter(index => ours[index] !== theirs[index] && [theirs[index], byMain[index]].some(hasListWithComma));
+ const withoutComma = await Promise.all(
+ withComma.map(index =>
+ prettier
+ .format(sources[index], { ...options, printWidth, trailingComma: "none", filepath: `a.${extension}` })
+ .catch(() => null),
+ ),
+ );
+ readAll(withoutComma);
+ const isMisreadWithoutComma = new Set(
+ withComma.filter((index, at) => withoutComma[at] == null || read(withoutComma[at]) !== read(sources[index])),
+ );
+ const endsListWithComma = (text, index) => hasListWithComma(text) && isMisreadWithoutComma.has(index);
+ // A type assertion that breaks after its `>` has its operand in parentheses of its own: `(⏎(q, r)⏎)`, and tsc
+ // tries no parameters there. Parentheses that Bun keeps after such a text are no error if the reader misreads what
+ // Prettier prints for the source where the assertion fits on its line.
+ const hasBrokenAssertion = text => typeof text === "string" && /(^|[^\w$\])])<[\w$.]+>\(\n\s*\(/.test(text);
+ const withAssertion = sources
+ .map((_, index) => index)
+ .filter(index => ours[index] !== theirs[index] && [theirs[index], byMain[index]].some(hasBrokenAssertion));
+ const onOneLine = await Promise.all(
+ withAssertion.map(index =>
+ prettier.format(sources[index], { ...options, printWidth: 1000, filepath: `a.${extension}` }).catch(() => null),
+ ),
+ );
+ readAll(onOneLine);
+ const isMisreadOnOneLine = new Set(
+ withAssertion.filter((index, at) => onOneLine[at] == null || read(onOneLine[at]) !== read(sources[index])),
+ );
+ const isAfterBrokenAssertion = (text, index) => hasBrokenAssertion(text) && isMisreadOnOneLine.has(index);
+ const again = format(ours, printWidth);
+ const againByMain = named.main ? format(byMain, printWidth, named.main) : [];
+ // Bun reads what Prettier prints, where tsc reads that as the source.
+ const theirsAgain = format(
+ theirs.map((text, index) => (text != null && read(text) === read(sources[index]) ? text : null)),
+ printWidth,
+ );
+
+ const counts = { sources: sources.length, broken: 0, pairs: 0, silenced: 0, repaired: 0, kept: 0, repairedOfMain: 0 };
+ Object.assign(counts, { pairsBeforeBrokenList: 0, pairsAfterBrokenAssertion: 0, asOnMain: { broken: 0, pairs: 0 } });
+ Object.assign(counts, {
+ left: { all: 0, silent: 0, anew: 0, refusedAgain: 0, changedAgain: 0, againNotAsOnMain: 0 },
+ });
+ counts.left.anewChangedAgain = 0;
+ const againstMain = { broken: 0, pairs: 0, silenced: 0, repaired: 0 };
+ if (named.main) Object.assign(counts, { againstMain, refusedOnMainToo: 0 });
+ Object.assign(counts, { unstable: 0, refusedAnew: 0, leftToEsbuild: 0 });
+ const node = { broken: 0, ours: { source: 0, other: 0, error: 0 }, theirs: { source: 0, other: 0, error: 0 } };
+ const shown = { broken: [], pairs: [], silenced: [], leftAnew: [], againNotAsOnMain: [], left: [], unstable: [] };
+ shown.leftSilent = [];
+ Object.assign(shown, { pairsBeforeBrokenList: [], pairsAfterBrokenAssertion: [], brokenAsOnMain: [] });
+ shown.anewChangedAgain = [];
+ Object.assign(shown, { brokenAgainstMain: [], silencedAgainstMain: [], pairsAgainstMain: [] });
+ Object.assign(shown, { refusedAnew: [], leftToEsbuild: [], brokenForNode: [] });
+ const refused = new Set();
+ const changed = [];
+ sources.forEach((source, index) => {
+ if (ours[index] == null) {
+ // The flavor of oxfmt reports the errors of oxc for some sources that tsc reads, on main too.
+ if (named.main && byMain[index] == null) counts.refusedOnMainToo++;
+ else refused.add(source);
+ return;
+ }
+ const tree = read(source);
+ const isRight = read(ours[index]) === tree;
+ const isRightByPrettier = theirs[index] != null && read(theirs[index]) === tree;
+ const both = `${oneLine(source)}\n ${oneLine(ours[index])}`;
+ if (typeof byMain[index] === "string" && byMain[index] !== ours[index]) {
+ const byTscOnMain = read(byMain[index]);
+ const isSilent = !isRight && !read(ours[index]).startsWith("error: ");
+ if (isRight && byTscOnMain === tree) {
+ if (!endsListWithComma(byMain[index], index) && !isAfterBrokenAssertion(byMain[index], index)) {
+ counts.againstMain.pairs++;
+ shown.pairsAgainstMain.push(`${oneLine(byMain[index])}\n ${oneLine(ours[index])}`);
+ }
+ } else if (isRight) counts.againstMain.repaired++;
+ else if (byTscOnMain === tree) {
+ counts.againstMain.broken++;
+ shown.brokenAgainstMain.push(`${both}\n ${oneLine(byMain[index])}`);
+ } else if (isSilent && byTscOnMain.startsWith("error: ")) {
+ counts.againstMain.silenced++;
+ shown.silencedAgainstMain.push(`${both}\n ${oneLine(byMain[index])}`);
+ }
+ }
+ if (!isRight && isRightByPrettier) {
+ if (!isAsOnMain(index)) {
+ counts.broken++;
+ shown.broken.push(`${both}\n ${read(ours[index]).slice(0, 80)}`);
+ } else {
+ counts.asOnMain.broken++;
+ shown.brokenAsOnMain.push(both);
+ }
+ } else if (!isRight) {
+ counts.left.all++;
+ shown.left.push(both);
+ if (!read(ours[index]).startsWith("error: ")) {
+ counts.left.silent++;
+ shown.leftSilent.push(both);
+ }
+ if (again[index] == null) counts.left.refusedAgain++;
+ else if (again[index] !== ours[index]) counts.left.changedAgain++;
+ if (isAsOnMain(index)) {
+ if (again[index] !== againByMain[index]) {
+ counts.left.againNotAsOnMain++;
+ shown.againNotAsOnMain.push(`${both}\n ${oneLine(again[index] ?? "an error")}`);
+ }
+ } else if (named.main) {
+ counts.left.anew++;
+ shown.leftAnew.push(both);
+ if (again[index] != null && again[index] !== ours[index]) {
+ counts.left.anewChangedAgain++;
+ shown.anewChangedAgain.push(`${both}\n ${oneLine(byMain[index] ?? "an error")}`);
+ }
+ }
+ // Where main prints another text than Prettier, the parentheses are not the only difference.
+ const isOnlyTheRule = !isAsOnMain(index) && (!named.main || byMain[index] === theirs[index]);
+ const isSilent = !read(ours[index]).startsWith("error: ");
+ if (isSilent && theirs[index] != null && read(theirs[index]).startsWith("error: ") && isOnlyTheRule) {
+ counts.silenced++;
+ shown.silenced.push(both);
+ }
+ } else if (!isRightByPrettier) counts.repaired++;
+ else if (ours[index] !== theirs[index]) {
+ const isBeforeList = endsListWithComma(theirs[index], index);
+ // tsc does not read what main prints as the source: the parentheses are a repair of that text.
+ const isRepairOfMain = typeof byMain[index] === "string" && !isAsOnMain(index) && read(byMain[index]) !== tree;
+ const isAfterAssertion = isAfterBrokenAssertion(theirs[index], index);
+ const exempt = isBeforeList ? "pairsBeforeBrokenList" : isAfterAssertion ? "pairsAfterBrokenAssertion" : "pairs";
+ const kind = isAsOnMain(index) || isRepairOfMain ? null : exempt;
+ if (isRepairOfMain) counts.repairedOfMain++;
+ else if (kind == null) counts.asOnMain.pairs++;
+ else {
+ counts[kind]++;
+ shown[kind].push(`${oneLine(theirs[index])}\n ${oneLine(ours[index])}`);
+ }
+ }
+ if (theirs[index] != null && ours[index] !== theirs[index] && !isAsOnMain(index)) counts.kept++;
+ if (again[index] == null) {
+ if (isRight) refused.add(ours[index]);
+ if (named.main && againByMain[index] !== null) {
+ counts.refusedAnew++;
+ shown.refusedAnew.push(both);
+ }
+ } else if (isRight && again[index] !== ours[index] && !isAsOnMain(index)) changed.push(index);
+ if (theirsAgain[index] === null) refused.add(theirs[index]);
+ if (esbuild && refusedByEsbuild(ours[index]) && !refusedByEsbuild(source)) {
+ counts.leftToEsbuild++;
+ shown.leftToEsbuild.push(both);
+ }
+ if (countsNode && theirs[index] != null && ours[index] !== theirs[index] && !isAsOnMain(index)) {
+ const program = byNode(source);
+ if (program == null) return;
+ const kind = text => {
+ const read = byNode(text);
+ return read == null ? "error" : read === program ? "source" : "other";
+ };
+ const [byUs, byThem] = [kind(ours[index]), kind(theirs[index])];
+ node.ours[byUs]++;
+ node.theirs[byThem]++;
+ if (byThem === "source" && byUs !== "source") {
+ node.broken++;
+ shown.brokenForNode.push(both);
+ }
+ }
+ });
+ // Prettier changes some of its own texts on a second run, and so does Bun then: `(((a: T) => ({})))`.
+ await Promise.all(
+ changed.map(async index => {
+ if (ours[index] === theirs[index] && (await byPrettier(theirs[index])) !== theirs[index]) return;
+ counts.unstable++;
+ shown.unstable.push(`${oneLine(ours[index])}\n ${oneLine(again[index])}`);
+ }),
+ );
+ shown.refused = Array.from(refused, oneLine);
+ for (const [kind, texts] of Object.entries(shown))
+ for (const text of texts.slice(0, Number(named.show ?? 5))) console.log(`${kind}: ${text}`);
+ const result = { width: printWidth, ...counts, refused: refused.size, ...(countsNode ? { node } : {}) };
+ console.log(JSON.stringify(result));
+ // The next width has other texts.
+ for (const text of [...ours, ...theirs, ...again, ...byMain, ...withoutComma, ...onOneLine])
+ if (!sourceSet.has(text)) readings.delete(text);
+ return result;
+}
+
+for (const printWidth of widths) {
+ const counts = await judge(printWidth);
+ const { broken, pairs, silenced, left, unstable, refused, refusedAnew, node, againstMain } = counts;
+ // The parser of `bun fmt` reports the syntax errors of tsc, which another reader may not report. In a `.js` file it
+ // reads no return type before the `:` of a conditional, on main too.
+ const isRefused = refusedAnew > 0 || (refused > 0 && !named.reader && !("babel-ts" in named));
+ // Bun prints a cell of a table on one line, and Prettier does not: there, `pairs` has that difference in it too.
+ const isPair = pairs > 0 && !("each" in named);
+ // In a `.js` file, main reads `async(q)` as `async < U > q`: tsc reads another program from its text already.
+ const isSilenced = againstMain?.silenced > 0 && !("babel-ts" in named);
+ const isWorseThanMain = againstMain?.broken > 0 || againstMain?.pairs > 0 || isSilenced;
+ if (broken || isPair || silenced || left.againNotAsOnMain || unstable || isRefused || node?.broken || isWorseThanMain)
+ process.exitCode = 1;
+}
diff --git a/test/cli/format/oracle/arrow-parens/known-issues.json b/test/cli/format/oracle/arrow-parens/known-issues.json
new file mode 100644
index 000000000000..713b3b55a4bc
--- /dev/null
+++ b/test/cli/format/oracle/arrow-parens/known-issues.json
@@ -0,0 +1,22 @@
+[
+ "x = cond ? (a: T) => ({ c: a }) : (y) => 1;\n",
+ "x = cond ? async () => ({}) : (y) => y;\n",
+ "x = f(cond ? () => ({}) : a < b, (s) => s);\n",
+ "const v = cond ? () => ({}) : [(y) => y];\n",
+ "x = cond ? (q) => ({ a: 1 }) : (deps) => (id): T => y;\n",
+ "x = cond ? (q) => ({ ...{ a } }) : (deps) => (id): T => y;\n",
+ "x = cond ? (q) => ({ q }) : (deps) => async (id): Promise => y;\n",
+ "x = cond ? (q) => ({ q }) : (deps) => (id): void => y;\n",
+ "function* g() {\n x = a ? yield (q, r) : (deps) => y;\n}\n",
+ "switch (v) {\n case (a, b):\n (c) => d;\n}\n",
+ "export const reducer = enabled ? useImmer ? (draft) => ({ draft }) : (state) => reduceWithPlainObjects(state, initialAction) : fallbackReducerForDisabled;\n",
+ "x = a ? b ? (q) => ({ q }) : (state) => f(state) : (fallback): Handler => fallback;\n",
+ "switch (v) {\n case cond ? (q) => ({ q }) : (d) => d:\n break;\n}\n",
+ "x = cond ? (q) => ({ q }) : (a, b) => (keyof) => (id): T => y;\n",
+ "x = a ? (q, r) : (deps) => (id): T => y;\n",
+ "x = cond ? (q) => ({ q }) : (deps) => (id): T[] => y;\n",
+ "x = cond ? (q) => ({ q }) : (deps) => (id): T | U => y;\n",
+ "x = cond ? (q) => ({ q }) : (deps) => (id): id is T => y;\n",
+ "x = a ? (q.r, s) : (deps) => (id): T => y;\n",
+ "x = cond ? (q) => ({ q }) : (d) => (p): Promise => (s: S) => y;\n"
+]
diff --git a/test/cli/format/oracle/arrow-parens/typescript7.mjs b/test/cli/format/oracle/arrow-parens/typescript7.mjs
new file mode 100644
index 000000000000..f663cdd5436e
--- /dev/null
+++ b/test/cli/format/oracle/arrow-parens/typescript7.mjs
@@ -0,0 +1,58 @@
+// A reader for `judge.mjs --reader`: the native TypeScript, which is the package `typescript7` in test/package.json.
+//
+// node judge.mjs --modules= --bun= --matrix --reader=typescript7.mjs
+//
+// It gets the directory of `--modules` as its argument, and a JSON array of `{ text, ext }` on standard input. It
+// prints one `{ diags, shape }` for each text: the messages of the syntax errors, and the tree without parentheses.
+// The API of the package reads the files of a project, so the texts go to a temporary directory.
+import fs from "node:fs";
+import { createRequire } from "node:module";
+import { tmpdir } from "node:os";
+import { dirname, join, resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+
+const require = createRequire(join(resolve(process.argv[2]), "noop.js"));
+const base = dirname(require.resolve("typescript7/package.json"));
+const { API } = await import(pathToFileURL(join(base, "dist/api/sync/api.js")).href);
+const { SyntaxKind } = await import(pathToFileURL(join(base, "dist/ast/index.js")).href);
+
+const texts = JSON.parse(fs.readFileSync(0, "utf8"));
+const directory = fs.mkdtempSync(join(tmpdir(), "arrow-parens-typescript7-"));
+const path = index => join(directory, "texts", `${index}.${texts[index].ext}`);
+fs.mkdirSync(join(directory, "texts"));
+texts.forEach(({ text }, index) => fs.writeFileSync(path(index), text));
+const compilerOptions = { noCheck: true, noLib: true, noResolve: true, noEmit: true, types: [] };
+fs.writeFileSync(
+ join(directory, "tsconfig.json"),
+ JSON.stringify({
+ compilerOptions: { ...compilerOptions, target: "esnext", module: "preserve", jsx: "preserve" },
+ include: ["texts"],
+ }),
+);
+
+const api = new API({ cwd: directory });
+try {
+ const project = { openProjects: [join(directory, "tsconfig.json")] };
+ // 7.0 has `updateSnapshot` for this, and 7.1 has `createSnapshot`.
+ const snapshot = (api.createSnapshot ?? api.updateSnapshot).call(api, project);
+ const program = snapshot.getProjects()[0].program;
+ const shape = node => {
+ if (node.kind === SyntaxKind.ParenthesizedExpression) return shape(node.expression);
+ // Prettier prints `T` for `| T`, which is a union of one type.
+ const isOneType = node.kind === SyntaxKind.UnionType || node.kind === SyntaxKind.IntersectionType;
+ if (isOneType && node.types.length === 1) return shape(node.types[0]);
+ const children = [];
+ node.forEachChild(child => void children.push(shape(child)));
+ const name = SyntaxKind[node.kind];
+ return children.length ? `${name}(${children.join(" ")})` : `${name} ${node.getText()}`;
+ };
+ const readings = texts.map((_, index) => {
+ const diags = Array.from(program.getSyntacticDiagnostics(path(index)), diagnostic => diagnostic.text);
+ const file = diags.length ? null : program.getSourceFile(path(index));
+ return { diags, shape: file ? Array.from(file.statements, shape).join(" ") : "" };
+ });
+ console.log(JSON.stringify(readings));
+} finally {
+ api.close();
+ fs.rmSync(directory, { recursive: true, force: true });
+}
diff --git a/test/cli/format/oracle/fuzz/README.md b/test/cli/format/oracle/fuzz/README.md
index e9ce0b801ac6..4904e59b5bf2 100644
--- a/test/cli/format/oracle/fuzz/README.md
+++ b/test/cli/format/oracle/fuzz/README.md
@@ -1,8 +1,9 @@
# Fuzzers
-They run `bun fmt` on texts that they make, and ask the npm package of Prettier about each. Neither runs in CI: they need Prettier.
+They run `bun fmt` on texts that they make, and ask the npm package of Prettier about each. None of them runs in CI: they need Prettier.
| file | what it makes |
| --- | --- |
| `handlebars/damage.ts` | Handlebars: the templates whose syntax tree, as Prettier compares trees in its own tests, is another one after formatting, and of which `bun fmt` does not say that it would damage them |
| `near.ts` | every language: Prettier's fixtures, or real files (`--files`), each a little changed: a token deleted, doubled, swapped with the next, or the text cut off: a file while it is typed. It runs `bun fmt --write` itself, with its checks, and asks Prettier, or oxfmt (`--oxfmt`), with the defaults or a set of options (`--options`). Two tables: how many have the same bytes, and which check refused what the other tool prints, rightly or not. `bun near.ts --command="bun fmt" --prettier= --language=js` (`css`, `yaml`, `graphql`, `json`, `handlebars`, `markdown`, `html`) |
+| `../arrow-parens/judge.mjs` | TypeScript: conditionals of arrow functions, as a matrix of operands, behind other `( .. ) :`, in families of conditionals that end each other's operands, with comments, or at random, formatted with `arrowParens: "avoid"` or, with `--always`, the default. The judge is `tsc`'s own parser, not Prettier, or with `--reader` another parser such as `typescript7.mjs` beside it: it has to read what `bun fmt` prints as it reads the source, and `bun fmt` must not differ from Prettier where `tsc` reads Prettier's text as the source too. `known-issues.json` beside it has sources that `bun fmt` prints as Prettier does although `tsc` does not read that as the source: with `--file`, all of them count as `left`. `node ../arrow-parens/judge.mjs --modules= --bun= --matrix` |