diff --git a/.typos.toml b/.typos.toml index d5a202eab6..19af521df3 100644 --- a/.typos.toml +++ b/.typos.toml @@ -10,6 +10,7 @@ extend-exclude = [ "apps/oxlint/src-js/package/config.generated.ts", "apps/oxfmt/test/**/fixtures/**", "apps/oxfmt/conformance/snapshots/diffs/**/", + "apps/oxfmt/conformance/snapshots/prettier.snap.md", "crates/oxc_formatter_tests/prettier", "crates/oxc_formatter/tests/jsdoc", "crates/oxc_linter/fixtures", diff --git a/apps/oxfmt/AGENTS.md b/apps/oxfmt/AGENTS.md index 5fe44da028..95380078a8 100644 --- a/apps/oxfmt/AGENTS.md +++ b/apps/oxfmt/AGENTS.md @@ -79,9 +79,12 @@ NOTE: Rust written formatters never fall back to Prettier, since they exist to r Known divergences live in DIVERGENCES.md (embedding / dispatch layer only); single-language ones live in the owning crate's. - The pin is a minimal repro in `conformance/fixtures/edge-cases/` -- `conformance/run.ts` compares a category's `projects` (real-world repositories) on Prettier's output of each file, the code a Prettier user hands over, - so the divergences that keep what was written do not fire in bulk; its `fixtures` (Prettier's own, the plugins', `edge-cases/`) stay as written, raw shapes are their job -- `conformance/run.ts` derives the file-to-entry map from every `DIVERGENCES.md` (this one's `Pin:` / `Conformance:`, the crates' `Oxfmt:`), prints the ref next to each failure, +- `conformance/run_prettier.ts` compares Prettier's own test suite with its committed jest snapshots, so Prettier never runs + - Its `PARSERS` table maps a Prettier parser to the filename oxfmt routes to that language, parsers not listed are not run +- `conformance/run_live.ts` compares with Prettier run on the spot + - A category's `projects` (real-world repositories) are compared on Prettier's output of each file, the code a Prettier user hands over, + so the divergences that keep what was written do not fire in bulk; its `fixtures` (Prettier's own, the plugins', `edge-cases/`) stay as written, raw shapes are their job +- `conformance/run_live.ts` derives the file-to-entry map from every `DIVERGENCES.md` (this one's `Pin:` / `Conformance:`, the crates' `Oxfmt:`), prints the ref next to each failure, and warns on a failure no entry lists (unclassified) and on a listed file that passes (stale) - All current entries are embedding-layer decisions that outlive the Prettier delegation; if an entry about the Prettier-fallback boundary itself is ever added, group it under a separate heading in DIVERGENCES.md so the removal scope stays obvious when the fallback goes away @@ -162,8 +165,10 @@ pnpm build-dev && pnpm t # Update snapshots pnpm t -u -# Run conformance test for xxx-in-js and js-in-xxx +# Run conformance tests (`conformance:prettier` and `conformance:live`) pnpm build-dev && pnpm download-fixtures && pnpm conformance +# Print the diffs of Prettier's test suite matching a path substring, without writing the report +pnpm conformance:prettier --filter ``` To manually verify the CLI behavior after building: diff --git a/apps/oxfmt/conformance/run.ts b/apps/oxfmt/conformance/run_live.ts similarity index 99% rename from apps/oxfmt/conformance/run.ts rename to apps/oxfmt/conformance/run_live.ts index a3f328f737..17f2a7e380 100644 --- a/apps/oxfmt/conformance/run.ts +++ b/apps/oxfmt/conformance/run_live.ts @@ -544,7 +544,7 @@ function writeReport(results: CategoryResult[], divergences: Map` runs only the matching paths and prints their diffs instead of writing the report. + +const SUITE_DIR = join(import.meta.dirname, "fixtures", "externals", "prettier"); +const SNAPSHOTS_DIR = join(import.meta.dirname, "snapshots"); + +/** Extension of the filename oxfmt is given → its language (the `associations` name), in the report's order */ +const LANGUAGES: Record = { + ".js": "javascript", + ".mjs": "javascript", + ".cjs": "javascript", + ".jsx": "jsx", + ".ts": "typescript", + ".mts": "typescript", + ".cts": "typescript", + ".tsx": "tsx", + ".json": "json", + ".importmap": "json", + ".json5": "json5", + ".jsonc": "jsonc", + ".css": "css", + ".scss": "scss", + ".less": "less", + ".graphql": "graphql", + ".yaml": "yaml", + ".md": "markdown", + ".html": "html", +}; + +const withExt = (ext: string) => (name: string) => name.replace(/\.[^. ]*$/, "") + ext; +// A JS/TS file keeps its own extension, as oxfmt parses it by that (e.g. a `.js` listing `typescript` stays JS); +// only snippets, which have none, get `ext` +const jsLike = (ext: string) => (name: string) => + /\.[cm]?[jt]sx?$/.test(name) ? name : name + ext; + +/** + * Prettier parser → the filename oxfmt routes to that language. + * A section runs once per distinct filename of its listed `parsers`, unlisted parsers are not run. + */ +const PARSERS: Record string> = { + babel: jsLike(".js"), + typescript: jsLike(".ts"), + // A spec may list only these (e.g. plain JSX under `flow`), Flow syntax itself fails to parse as JS + flow: jsLike(".js"), + "babel-ts": jsLike(".ts"), + json: withExt(".json"), + json5: withExt(".json5"), + jsonc: withExt(".jsonc"), + // Not `package.json`, which oxfmt also sorts + "json-stringify": withExt(".importmap"), + css: withExt(".css"), + scss: withExt(".scss"), + less: withExt(".less"), + graphql: withExt(".graphql"), + yaml: withExt(".yaml"), + markdown: withExt(".md"), + html: withExt(".html"), +}; + +/** + * Suite-relative path prefixes not run, + * mostly carried over from the crates' conformance minus the ones needing the dispatcher. + * "Experimental syntax" is a proposal before stage 3, which oxc does not parse. + */ +const IGNORE = [ + // Bogus nodes + "typescript/conformance/classes/constructorDeclarations/constructorParameters/readonlyReadonly.ts", + // Expression syntax: `a?.b = c` + "js/optional-chaining-assignment/", + "js/babel-plugins/optional-chaining-assignment.js", + // Experimental syntax: `do {}` + "js/async-do-expressions/", + "js/do", + "jsx/do/", + "js/babel-plugins/async-do-expressions.js", + "js/babel-plugins/do-expressions.js", + // Experimental syntax: `export X from "mod"` + "js/export-default/export-default-from", + "js/export-default/escaped", + "js/babel-plugins/export-default-from.js", + // Experimental syntax: `module {}` + "js/module-blocks", + "js/babel-plugins/module-blocks.js", + "js/explicit-resource-management/valid-module-block-", + // Experimental syntax: pipeline operator `|>` + "js/comments-pipeline-own-line", + "js/partial-application", + "js/pipeline-operator", + "js/babel-plugins/partial-application.js", + "js/babel-plugins/pipeline-operator-", + // Experimental syntax: `::` + "js/arrows-bind", + "js/bind-expressions", + "js/objects/expression.js", + "js/no-semi-babylon-extensions/no-semi.js", + "js/babel-plugins/function-bind.js", + // Experimental syntax: `let { #x: x } = ...` + "js/destructuring-private-fields", + "js/babel-plugins/destructuring-private.js", + // Experimental syntax: `throw` expressions + "js/throw_expressions", + "js/babel-plugins/throw-expressions.js", + // Experimental syntax: discard bindings `void` + "js/discard-binding/", + "js/babel-plugins/discard-binding.js", + // Experimental syntax: `function.sent` + "js/babel-plugins/function-sent.js", + // Syntax recovery + "typescript/error-recovery/", + // Trailing comma after rest element + "typescript/trailing-comma/invalid.ts", + // `<<` right after a heritage clause's or JSX element's type argument `<` + "typescript/type-arguments-bit-shift-left-like/3.ts", + "typescript/type-arguments-bit-shift-left-like/5.tsx", + // Invalid modifier combos (`readonly accessor`, `private abstract`, ambient initializer, ...) + "typescript/decorator-auto-accessors/decorator-auto-accessors-abstract-class.ts", + "typescript/decorator-auto-accessors/decorator-auto-accessors-declare-class.ts", + "typescript/decorator-auto-accessors/decorator-auto-accessors-mixed-modifiers.ts", + "typescript/class/declare-readonly-field-initializer-w-annotation.ts", + "typescript/conformance/classes/classDeclarations/classAbstractKeyword/classAbstractMixedWithModifiers.ts", + "typescript/conformance/classes/classDeclarations/classAbstractKeyword/classAbstractProperties.ts", + "typescript/conformance/classes/constructorDeclarations/constructorParameters/readonlyInConstructorParameters.ts", + "typescript/custom/abstract/abstractProperties.ts", + "typescript/declare/declare-class-fields.ts", + "typescript/declare/declare-module.ts", + "typescript/declare/declare-namespace.ts", + "typescript/interface2/module.ts", + "typescript/type-parameters-arguments/const.ts", + // Other early errors (duplicate import attributes, `new.target` outside functions, bad escapes, ...) + "js/import-assertions/multi-types.js", + "js/import-attributes/multi-types.js", + "js/multiparser-invalid/text.js", + "js/new-target/outside-functions.js", + "typescript/conformance/types/functions/functionOverloadErrorsSyntax.ts", + "typescript/module/keyword.ts", + "typescript/namespace/invalid-await.ts", + "typescript/definite/definite.ts", + "typescript/definite/without-annotation.ts", + // TS syntax in a JS file, which Prettier parses with `typescript` but oxfmt as JS + "jsx/expression-with-types/expression.js", + "typescript/tsx/comma/snippet: test.js", + "typescript/tsx/comma/snippet: test.cjs", + "typescript/tsx/comma/snippet: test.mjs", + "js/babel-plugins/typescript.js", + // Ambiguous await + "js/top-level-await", + "jsx/top-level-await", + "typescript/top-level-await", + "js/ternaries/parenthesis/await-expression.js", + "js/explicit-resource-management/for-await-using-of-comments.js", + "js/explicit-resource-management/valid-using-as-identifier-for-await-of.js", + // Top-level `await (1)` with no import/export: Prettier always parses `.js` as ESM (await expression), + // while our unambiguous detection leans to script (call expression), whose output is valid under both + "js/await/like-call.js", + // ES5 vs ES6+ identifier: Prettier uses ES5 validation, OXC uses ES6+ + // Characters outside BMP (like U+102A7) are valid ES6+ identifiers but not ES5 + "js/quotes/objects.js", + // Flow is not supported (`typescript/**/consistent-with-flow/` are TS) + "flow/", + "misc/babel-redirect-to-babel-flow/", + "misc/filename-matters/flow-ambient/", + "js/babel-plugins/flow.js", + + // postcss-conditionals (archived: https://github.com/andyjansson/postcss-conditionals) + "css/atrule/if-else.css", + + // Depends on Prettier's test-only plugins (`tests/config/prettier-plugins/`) + "misc/front-matter/with-plugins/", + "misc/plugins/", +]; + +/** Sections not run, by their options */ +const skipSection = (options: Options) => + // Prettier's runner replaces the `<|>` placeholder in the input (range sections are not even parsed) + "cursorOffset" in options + // Rejected by oxfmt's config, not supported yet + || options.experimentalTernaries === true + // Beyond what oxfmt's config holds (a `u8`), like `printWidth: Infinity` in `parseSection()` + || (typeof options.tabWidth === "number" && options.tabWidth > 255) + // No pragma options in oxfmt + || options.insertPragma === true + || options.requirePragma === true + || options.checkIgnorePragma === true; + +// Same as `printSeparator()` in Prettier's `tests/config/format-test/create-snapshot.js` +const separator = (label = "") => { + const left = Math.floor((80 - label.length) / 2); + return "=".repeat(left) + label + "=".repeat(80 - left - label.length); +}; +const OPTIONS_HEADER = `${separator("options")}\n`; +const INPUT_SEPARATOR = `\n${separator("input")}\n`; +const OUTPUT_SEPARATOR = `\n${separator("output")}\n`; +const END_SEPARATOR = `\n${separator()}`; + +const BOM = "\uFEFF"; +const EOL_BY_NAME: Record = { LF: "\n", CRLF: "\r\n", CR: "\r" }; +const NAME_BY_EOL: Record = { "\n": "LF", "\r\n": "CRLF", "\r": "CR" }; + +type Options = Record; +type Row = { + total: number; + passed: number; + failed: number; + /** Line similarity summed over the runs, a passed one counts as 1 */ + ratio: number; + /** The first error oxfmt reported */ + error: string | null; + /** `null` when idempotent, else whether the second pass errored */ + nonIdempotent: boolean | null; + variants: Set; +}; + +const { filter } = parseArgs({ options: { filter: { type: "string" } } }).values; +const rowsByLanguage = new Map>(); + +for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", { + cwd: SUITE_DIR, +}).sort()) { + const dir = dirname(dirname(snapPath)); + const entries = [ + ...readFileSync(join(SUITE_DIR, snapPath), "utf8").matchAll( + /^exports\[`((?:[^`\\]|\\.)*)`\] = `((?:[^`\\]|\\.)*)`;$/gms, + ), + ].map(([, key, body]) => [unescape(key), body] as const); + // Keys without the jest counter, ` format[<parser>]` holds the error of a parser expected to throw + const keys = new Set(entries.map(([key]) => key.replace(/ \d+$/, ""))); + + for (const [key, body] of entries) { + const title = /^(.*) format \d+$/s.exec(key)?.[1]; + if (title === undefined) continue; + const section = parseSection(unescape(body)); + if (section === null || skipSection(section.options)) continue; + + const name = title.replace(/ - \{.*\}$/, ""); + const path = `${dir}/${name}`; + if (IGNORE.some((ignore) => path.startsWith(ignore))) continue; + if (filter && !path.includes(filter)) continue; + + const { parsers, options, expected } = section; + const input = name.startsWith("snippet: ") + ? "endOfLine" in options + ? section.input.replace(/<(CRLF|CR|LF)>\n/g, (_, eol: string) => EOL_BY_NAME[eol]) + : section.input + : readFileSync(join(SUITE_DIR, path), "utf8"); + + // JS/TS runs once by the first parser listed, as Prettier asserts they all print the same: + // a JSX snippet is not also run as `.ts`, and one parser expected to throw still leaves another's output + const ran = new Set<string>(); + for (const parser of parsers) { + const filename = PARSERS[parser]?.(name); + if (filename === undefined) continue; + const key = /\.[cm]?[jt]sx?$/.test(filename) ? "js" : filename; + if (ran.has(key) || (key !== "js" && keys.has(`${title} format[${parser}]`))) continue; + ran.add(key); + + const result = await runCase(`${dir}/${filename}`, input, options, expected); + if (filter) printResult(`${path} [${parser}]`, options, expected, result); + + const language = LANGUAGES[extname(filename)]; + const rows = rowsByLanguage.get(language) ?? new Map<string, Row>(); + rowsByLanguage.set(language, rows); + const row = rows.get(path) ?? { + total: 0, + passed: 0, + failed: 0, + ratio: 0, + error: null, + nonIdempotent: null, + variants: new Set(), + }; + rows.set(path, row); + + row.total++; + if (result.error !== undefined) { + row.error ??= result.error; + continue; + } + if (result.passed) row.passed++; + else row.failed++; + row.ratio += result.ratio; + if (result.nonIdempotent !== null) row.nonIdempotent ||= result.nonIdempotent; + for (const variant of result.variants) row.variants.add(variant); + } + } +} + +if (!filter) writeReport(); + +// --- + +/** Inverse of the template-literal escaping jest applies (`` ` ``, `\` and `${` get a leading `\`). */ +function unescape(text: string) { + return text.replace(/\\([\s\S])/g, "$1"); +} + +/** `null` for a section without an options block (error snapshots, range-visualized inputs) or with a non-JSON option. */ +function parseSection(body: string) { + // jest's raw serializer wraps the body in a leading and a trailing newline + const content = body.slice(1, -1); + if (!content.startsWith(OPTIONS_HEADER) || !content.endsWith(END_SEPARATOR)) return null; + const inputStart = content.indexOf(INPUT_SEPARATOR); + const outputStart = content.indexOf(OUTPUT_SEPARATOR, inputStart); + if (inputStart === -1 || outputStart === -1) return null; + + const block: Options = {}; + for (const line of content.slice(OPTIONS_HEADER.length, inputStart).split("\n")) { + // The indented line is the `printWidth` indicator + if (line === "" || line.startsWith(" ")) continue; + const [key, value] = line.split(/: (.*)/s); + try { + block[key] = JSON.parse(value); + } catch { + // Non-JSON values (`printWidth: Infinity`) cannot be given to oxfmt + return null; + } + } + const { parsers = [], ...options } = block as Options & { parsers?: string[] }; + + return { + parsers, + options, + input: content.slice(inputStart + INPUT_SEPARATOR.length, outputStart), + expected: content.slice(outputStart + OUTPUT_SEPARATOR.length, -END_SEPARATOR.length), + }; +} + +/** `code` is `null` when oxfmt reports an error (parse, unsupported file, invalid option). */ +async function formatWithOxfmt(filename: string, input: string, options: Options) { + // Prettier's default, oxfmt's is 100 + const res = await format(filename, input, { printWidth: 80, ...options }); + return res.errors.length === 0 + ? { code: res.code, error: "" } + : { code: null, error: res.errors[0].message.split("\n")[0] }; +} + +async function runCase(filename: string, input: string, options: Options, expected: string) { + const first = await formatWithOxfmt(filename, input, options); + if (first.code === null) return { error: first.error }; + const actual = first.code; + + // Prettier visualizes the line endings of both input and output when `endOfLine` is set + const compared = + "endOfLine" in options + ? actual.replace(/\r\n?|\n/g, (eol) => `<${NAME_BY_EOL[eol]}>\n`) + : actual; + const passed = compared === expected; + + // Line endings are normalized and a BOM is kept, so these must produce the same output. + // Inputs already containing `\r` or a BOM are skipped, their variants would not be equivalent. + const variants: [name: string, input: string, output: string][] = []; + if (!input.includes("\r")) { + variants.push( + ["CRLF", input.replaceAll("\n", "\r\n"), actual], + ["CR", input.replaceAll("\n", "\r"), actual], + ); + } + if (!input.startsWith(BOM)) variants.push(["BOM", BOM + input, BOM + actual]); + + // The second pass (idempotency) and the variants are independent of each other + const [second, ...outputs] = await Promise.all( + [actual, ...variants.map(([, text]) => text)].map((text) => + formatWithOxfmt(filename, text, options), + ), + ); + + return { + compared, + passed, + ratio: passed ? 1 : similarity(expected, compared), + nonIdempotent: second.code === actual ? null : second.code === null, + variants: variants.filter(([, , output], i) => outputs[i].code !== output).map(([n]) => n), + }; +} + +/** Line similarity in `0..=1`, like `similar`'s `TextDiff::ratio()` the crates' conformance used. */ +function similarity(a: string, b: string) { + let common = 0; + let total = 0; + for (const change of diffLines(a, b)) { + const count = change.count ?? 0; + if (change.added || change.removed) { + total += count; + } else { + common += count; + total += count * 2; + } + } + return total === 0 ? 1 : (common * 2) / total; +} + +function printResult( + label: string, + options: Options, + expected: string, + result: Awaited<ReturnType<typeof runCase>>, +) { + console.log(`${label} ${JSON.stringify(options)}`); + if (result.error !== undefined) { + console.log(` => Error: ${result.error}\n`); + return; + } + if (result.passed) console.log(" => Passed"); + else console.log(createTwoFilesPatch("prettier", "oxfmt", expected, result.compared)); + if (result.nonIdempotent !== null) { + console.log(` => Not idempotent${result.nonIdempotent ? " (second pass errored)" : ""}`); + } + if (result.variants.length > 0) { + console.log(` => Input variants differ: ${result.variants.join(", ")}`); + } + console.log(); +} + +function writeReport() { + const lines: string[] = []; + for (const language of new Set(Object.values(LANGUAGES))) { + const rows = rowsByLanguage.get(language); + if (!rows) continue; + const sorted = [...rows].sort(([a], [b]) => a.localeCompare(b)); + let total = 0; + let passed = 0; + for (const [, row] of sorted) { + total += row.total; + passed += row.passed; + } + const summary = `${passed}/${total} (${((passed / total) * 100).toFixed(2)}%)`; + console.log(`[${language}] ${summary}`); + lines.push(`## ${language}`, "", summary, ""); + + const failed = sorted.filter(([, row]) => row.failed > 0); + if (failed.length > 0) { + lines.push("### Failed", ""); + lines.push("| Spec path | Failed or Passed | Match ratio |"); + lines.push("| :-------- | :--------------: | :---------: |"); + for (const [path, row] of failed) { + const marks = "💥".repeat(row.failed) + "✨".repeat(row.passed); + lines.push(`| ${path} | ${marks} | ${((row.ratio / row.total) * 100).toFixed(2)}% |`); + } + lines.push(""); + } + + const lists: [string, (row: Row) => string | null][] = [ + [ + "Not idempotent", + (r) => (r.nonIdempotent === null ? null : r.nonIdempotent ? " (second pass errored)" : ""), + ], + [ + "Input variants differ", + (r) => (r.variants.size > 0 ? ` (${[...r.variants].join(", ")})` : null), + ], + ["Error", (r) => (r.error === null ? null : `: ${r.error}`)], + ]; + for (const [heading, note] of lists) { + const entries = sorted.flatMap(([path, row]) => { + const text = note(row); + return text === null ? [] : [`- ${path}${text}`]; + }); + if (entries.length > 0) lines.push(`### ${heading}`, "", ...entries, ""); + } + } + + mkdirSync(SNAPSHOTS_DIR, { recursive: true }); + const outPath = join(SNAPSHOTS_DIR, "prettier.snap.md"); + writeFileSync(outPath, lines.join("\n")); + console.log(`Report written to ${relative(process.cwd(), outPath)}`); +} diff --git a/apps/oxfmt/conformance/snapshots/conformance.snap.md b/apps/oxfmt/conformance/snapshots/live.snap.md similarity index 100% rename from apps/oxfmt/conformance/snapshots/conformance.snap.md rename to apps/oxfmt/conformance/snapshots/live.snap.md diff --git a/apps/oxfmt/conformance/snapshots/prettier.snap.md b/apps/oxfmt/conformance/snapshots/prettier.snap.md new file mode 100644 index 0000000000..51611105a1 --- /dev/null +++ b/apps/oxfmt/conformance/snapshots/prettier.snap.md @@ -0,0 +1,421 @@ +## javascript + +1225/1307 (93.73%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| js/arrays/numbers-with-holes.js | 💥 | 92.86% | +| js/arrows/arrow-chain-with-trailing-comments.js | 💥💥 | 93.75% | +| js/arrows/issue-17421.js | 💥💥 | 88.70% | +| js/assignment-comments/string.js | 💥 | 96.59% | +| js/babel-plugins/v8intrinsic.js | 💥 | 95.83% | +| js/binary-expressions/mutiple-comments/17192.js | 💥✨ | 95.83% | +| js/call/no-argument/no-arguments.js | 💥 | 79.49% | +| js/class-comment/misc.js | 💥 | 66.67% | +| js/class-comment/superclass.js | 💥 | 70.33% | +| js/comments-closure-typecast/iife.js | 💥 | 76.92% | +| js/comments/array-and-object.js | 💥💥 | 62.50% | +| js/comments/assignment/variable-declarator.js | 💥 | 84.00% | +| js/comments/between-head-and-body/between-head-and-body.js | 💥 | 90.85% | +| js/comments/between-head-and-body/empty-statement.js | 💥 | 86.05% | +| js/comments/empty-statements.js | 💥💥 | 90.91% | +| js/comments/first-argument/first-argument.js | 💥 | 92.98% | +| js/comments/function-declaration.js | 💥💥 | 92.80% | +| js/comments/function/18146.js | 💥 | 51.80% | +| js/comments/function/between-parentheses-and-function-body.js | 💥 | 47.62% | +| js/comments/in-list/dangling-comment-in-list.js | 💥 | 95.91% | +| js/comments/return-statement-2.js | 💥💥 | 87.32% | +| js/comments/return-statement.js | 💥💥 | 97.25% | +| js/comments/tagged-template-literal.js | 💥💥 | 89.66% | +| js/comments/try.js | 💥💥 | 66.67% | +| js/comments/variable-declarator.js | 💥💥 | 82.35% | +| js/embeded/indention/19518.js | 💥 | 92.31% | +| js/explicit-resource-management/valid-await-using-comments.js | 💥 | 88.24% | +| js/export-default/class_instance.js | 💥 | 0.00% | +| js/export-default/function_tostring.js | 💥 | 0.00% | +| js/export-default/parentheses/snippet: #11 | 💥 | 0.00% | +| js/export-default/parentheses/snippet: #12 | 💥 | 0.00% | +| js/export-default/parentheses/snippet: #9 | 💥 | 0.00% | +| js/for-of/comments.js | 💥 | 44.83% | +| js/for/9812-2.js | 💥 | 88.89% | +| js/for/continue-and-break-comment-without-blocks.js | 💥 | 67.59% | +| js/function/iife.js | 💥 | 14.43% | +| js/function/issue-12967.js | 💥 | 0.00% | +| js/identifier/parentheses/let.js | 💥💥 | 92.73% | +| js/if/condition-break/boolean-expression.js | 💥 | 97.01% | +| js/if/condition-break/unary-expression.js | 💥 | 61.38% | +| js/if/expr_and_same_line_comments.js | 💥 | 81.93% | +| js/if/issue-15168.js | 💥 | 91.43% | +| js/if/non-block.js | 💥 | 82.35% | +| js/ignore/issue-10661.js | 💥 | 75.47% | +| js/ignore/issue-11077.js | 💥 | 93.33% | +| js/ignore/semi/class-expression-decorator.js | 💥💥 | 25.00% | +| js/ignore/semi/directive.js | 💥✨ | 88.89% | +| js/label/comment.js | 💥 | 58.82% | +| js/last-argument-expansion/dangling-comment-in-arrow-function.js | 💥 | 0.00% | +| js/last-argument-expansion/edge_case.js | 💥 | 74.07% | +| js/logical-expressions/in-unary-expression.js | 💥 | 79.49% | +| js/multiparser-css/styled-components.js | 💥 | 98.49% | +| js/multiparser-markdown/codeblock.js | 💥 | 77.78% | +| js/sequence-expression/parenthesized-trailing-comment-unstable.js | 💥 | 66.67% | +| js/sequence-expression/parenthesized.js | 💥 | 85.71% | +| js/switch/comments.js | 💥 | 94.74% | +| js/template-literals/expressions.js | 💥 | 97.64% | +| js/ternaries/indent-after-paren.js | 💥💥💥💥 | 99.14% | +| js/test-declarations/jest-each.js | 💥💥 | 95.71% | +| js/trailing-comma/trailing_whitespace.js | 💥💥💥 | 96.55% | +| js/try/try.js | 💥 | 44.44% | +| jsx/deprecated-jsx-bracket-same-line-option/jsx.js | 💥💥✨✨ | 92.11% | +| misc/embedded-language-formatting/in-javascript/test.js | 💥✨ | 98.00% | + +### Not idempotent + +- js/comments/first-argument/first-argument.js +- js/comments/return-statement.js +- js/for/continue-and-break-comment-without-blocks.js +- js/function/issue-12967.js +- js/ignore/semi/class-expression-decorator.js +- js/ignore/semi/head-ignored.js +- jsx/comments/in-attributes.js + +### Input variants differ + +- js/comments/trailing_space.js (BOM) +- js/shebang/shebang-newline.js (BOM) +- js/shebang/shebang.js (BOM) +- js/shebang/snippet: Empty file with shebang (BOM) + +## jsx + +39/39 (100.00%) + +## typescript + +632/681 (92.80%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| typescript/as/as-const/as-const.ts | 💥 | 66.67% | +| typescript/as/as.ts | 💥 | 97.74% | +| typescript/as/break-after-keyword/18148.ts | 💥 | 93.33% | +| typescript/as/comments/17407.ts | 💥 | 47.06% | +| typescript/as/comments/18160.ts | 💥 | 54.21% | +| typescript/as/export_default_as.ts | 💥 | 0.00% | +| typescript/call/callee-comments.ts | 💥 | 64.10% | +| typescript/cast/18406.ts | 💥 | 84.21% | +| typescript/class-comment/class-implements.ts | 💥 | 93.33% | +| typescript/class-comment/declare.ts | 💥 | 72.00% | +| typescript/comments/first-argument/first-argument.ts | 💥 | 81.16% | +| typescript/comments/method_types.ts | 💥 | 82.05% | +| typescript/comments/type-literals.ts | 💥 | 89.66% | +| typescript/conformance/types/union/unionTypeCallSignatures.ts | 💥 | 85.25% | +| typescript/conformance/types/union/unionTypeConstructSignatures.ts | 💥 | 91.62% | +| typescript/conformance/types/union/unionTypeIndexSignature.ts | 💥 | 83.64% | +| typescript/export-default/function_as.ts | 💥 | 0.00% | +| typescript/interface/comments-generic.ts | 💥💥 | 91.80% | +| typescript/interface2/comments-declare.ts | 💥 | 60.00% | +| typescript/interface2/comments.ts | 💥 | 65.79% | +| typescript/intersection/consistent-with-flow/intersection-parens.ts | 💥 | 97.67% | +| typescript/intersection/consistent-with-flow/single-member.ts | 💥 | 88.24% | +| typescript/intersection/intersection-parens.ts | 💥💥✨ | 99.29% | +| typescript/prettier-ignore/mapped-types.ts | 💥 | 92.19% | +| typescript/property-signature/consistent-with-flow/comments.ts | 💥 | 80.00% | +| typescript/property-signature/consistent-with-flow/union.ts | 💥 | 80.00% | +| typescript/satisfies-operators/comments-unstable.ts | 💥💥 | 61.54% | +| typescript/satisfies-operators/export-default-as.ts | 💥💥 | 0.00% | +| typescript/template-literals/member-expression.ts | 💥 | 65.12% | +| typescript/tsx/comma/snippet: test.unknown | 💥 | 0.00% | +| typescript/tsx/comma/snippet: unnamed | 💥 | 0.00% | +| typescript/type-parameters-arguments/18041.ts | 💥 | 91.43% | +| typescript/type-parameters-arguments/18604.ts | 💥 | 60.00% | +| typescript/type-parameters-arguments/19505.ts | 💥 | 81.48% | +| typescript/type-parameters-arguments/constraints-and-default.ts | 💥 | 91.67% | +| typescript/union/5849.ts | 💥 | 95.38% | +| typescript/union/comments/18106.ts | 💥 | 86.36% | +| typescript/union/comments/18379.ts | 💥 | 92.31% | +| typescript/union/consistent-with-flow/81-characters.ts | 💥 | 53.85% | +| typescript/union/consistent-with-flow/comment.ts | 💥 | 87.50% | +| typescript/union/consistent-with-flow/conditional.ts | 💥 | 54.55% | +| typescript/union/consistent-with-flow/leading-comments.ts | 💥 | 90.14% | +| typescript/union/consistent-with-flow/prettier-ignore.ts | 💥 | 88.00% | +| typescript/union/consistent-with-flow/single-type.ts | 💥 | 97.50% | +| typescript/union/union-parens.ts | 💥 | 97.67% | + +### Not idempotent + +- typescript/as/comments/17407.ts +- typescript/method-chain/object/issue-17239.ts +- typescript/prettier-ignore/issue-14238.ts +- typescript/prettier-ignore/mapped-types.ts + +## tsx + +20/20 (100.00%) + +## json + +367/367 (100.00%) + +## json5 + +193/193 (100.00%) + +## jsonc + +169/169 (100.00%) + +## css + +137/161 (85.09%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| css/comments/declaration.css | 💥 | 98.88% | +| css/fill-value/fill.css | 💥 | 93.33% | +| css/parens/empty-lines.css | 💥 | 31.58% | +| css/parens/parens.css | 💥 | 93.13% | +| css/postcss-8-improment/test.css | 💥 | 88.24% | +| css/postcss-plugins/postcss-nesting.css | 💥 | 97.66% | +| css/stylefmt-repo/at-media/at-media.css | 💥 | 95.24% | +| css/stylefmt-repo/cssnext-example/cssnext-example.css | 💥 | 98.31% | +| css/stylefmt-repo/media-queries-ranges/media-queries-ranges.css | 💥 | 90.91% | + +### Error + +- css/atrule/charset.css: Syntax error: expect token `<string>`, but found `<ident>` +- css/atrule/import.css: Syntax error: expect token `<ident>`, but found `(` +- css/atrule/supports.css: Syntax error: expect token `)`, but found `<` +- css/attribute/quotes.css: Syntax error: expect token `]`, but found `(` +- css/combinator/combinator.css: Syntax error: expect token `{`, but found `/` +- css/comments/selectors.css: Syntax error: simple selector is expected +- css/custom-properties/emoji.css: Syntax error: declaration at top level is disallowed +- css/no-semicolon/url.css: Syntax error: URL is expected +- css/postcss-plugins/postcss-mixins.css: Syntax error: expect token `{`, but found `<unknown>` +- css/postcss-plugins/postcss-simple-vars.css: Syntax error: declaration at top level is disallowed +- css/quotes/quotes.css: Syntax error: expect token `{`, but found `<string>` +- css/selector-list/selectors.css: Syntax error: simple selector is expected +- css/selector-string/string.css: Syntax error: simple selector is expected + +## scss + +112/143 (78.32%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| css/font/font.css | 💥 | 94.12% | +| scss/comments/4594.scss | 💥 | 83.78% | +| scss/comments/4878.scss | 💥 | 89.39% | +| scss/comments/lists.scss | 💥 | 76.92% | +| scss/comments/maps.scss | 💥 | 66.67% | +| scss/comments/variable-declaration.scss | 💥 | 81.25% | +| scss/function/arbitrary-arguments-comment.scss | 💥💥 | 72.73% | +| scss/map/15193.scss | 💥✨ | 92.86% | +| scss/map/comment.scss | 💥💥 | 73.91% | +| scss/map/function-argument/functional-argument.scss | 💥 | 96.00% | +| scss/parens/2.scss | 💥 | 75.00% | +| scss/parens/issue-16594.scss | 💥 | 71.43% | +| scss/trailing-comma/comments.scss | 💥💥 | 87.18% | +| scss/trailing-comma/issue-6920.scss | 💥💥 | 66.67% | +| scss/trailing-comma/list.scss | 💥💥 | 95.56% | +| scss/trailing-comma/variable.scss | 💥💥 | 50.00% | +| scss/variables/postcss-8-improment.scss | 💥 | 88.24% | + +### Error + +- css/custom-properties/emoji.css: Syntax error: declaration at top level is disallowed +- scss/atrule/for.scss: Syntax error: Sass keyword `to` is expected +- scss/case/case.scss: Syntax error: expect token `<string>`, but found `<ident>` +- scss/comments/CRLF.scss: Syntax error: component value is expected +- scss/math/3945.scss: Syntax error: declaration at top level is disallowed +- scss/no-semicolon/url.scss: Syntax error: expect token `<string>`, but found `<ident>` +- scss/parens/parens.scss: Syntax error: component value is expected + +## less + +222/245 (90.61%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| less/comments/value-lists.less | 💥 | 76.92% | +| less/less-test-suite/snippet: tests-config/globalVars/extended.less | 💥 | 78.26% | +| less/less-test-suite/snippet: tests-config/namespacing/namespacing-functions.less | 💥 | 97.30% | +| less/less-test-suite/snippet: tests-config/namespacing/namespacing-media.less | 💥 | 96.15% | +| less/less-test-suite/snippet: tests-unit/color-functions/rgba.less | 💥 | 82.93% | +| less/less-test-suite/snippet: tests-unit/comments/comments2.less | 💥 | 87.50% | +| less/less-test-suite/snippet: tests-unit/extend-chaining/extend-chaining.less | 💥 | 98.25% | +| less/less-test-suite/snippet: tests-unit/extend-selector/extend-selector.less | 💥 | 98.67% | +| less/less-test-suite/snippet: tests-unit/javascript/javascript.less | 💥 | 81.01% | +| less/less-test-suite/snippet: tests-unit/mixins-guards-default-func/mixins-guards-default-func.less | 💥 | 99.21% | +| less/less-test-suite/snippet: tests-unit/mixins-guards/mixins-guards.less | 💥 | 99.48% | +| less/less-test-suite/snippet: tests-unit/namespace-targeted/namespace-targeted.less | 💥 | 96.88% | +| less/less-test-suite/snippet: tests-unit/property-name-interp/property-name-interp.less | 💥 | 98.28% | +| less/less-test-suite/snippet: tests-unit/strings/strings.less | 💥 | 98.68% | +| less/less-test-suite/snippet: tests-unit/variables/variables.less | 💥 | 98.95% | +| less/postcss-8-improment/test.less | 💥 | 88.24% | + +### Error + +- less/case/case.less: Syntax error: expect token `(`, but found `<ident>` +- less/comments/in-value.less: Syntax error: expect token `;`, but found `{` +- less/interpolation/interpolation.less: Syntax error: expect token `<ident>`, but found `@{` +- less/less/less.less: Syntax error: expect one of `<ident>` or `@{`, but found `(` +- less/lookup/lookup-1.less: Syntax error: expect token `;`, but found `<ident>` +- less/no-semicolon/url.less: Syntax error: expect token `(`, but found `<ident>` +- less/parens/parens.less: Syntax error: expect token `;`, but found `{` + +## graphql + +105/105 (100.00%) + +## yaml + +1520/1619 (93.89%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| yaml/block-folded/block-folded-strip.yml | 💥💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >-\n x\n \t\n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: >\n x\n \t\n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: >+\n" | 💥💥💥 | 0.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |-\n x\n \t\n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n\n\n" | 💥 | 80.00% | +| yaml/block-value/snippet: "foo: |\n x\n \t\n\n\n\n\n..." | 💥 | 85.71% | +| yaml/block-value/snippet: "foo: |+\n" | 💥💥💥 | 0.00% | +| yaml/flow-mapping/comments/key.yml | 💥 | 43.75% | +| yaml/mapping/duplicated-keys/flow-mapping.yml | 💥 | 85.71% | +| yaml/spec/anchors-and-tags.yml | 💥💥 | 75.00% | +| yaml/spec/node-anchor-and-tag-on-seperate-lines.yml | 💥💥 | 50.00% | +| yaml/spec/spec-example-6-1-indentation-spaces.yml | 💥💥 | 84.62% | +| yaml/spec/spec-example-7-20-single-pair-explicit-entry.yml | 💥✨ | 62.50% | +| yaml/spec/spec-example-9-4-explicit-documents.yml | 💥✨ | 83.33% | +| yaml/spec/various-combinations-of-tags-and-anchors.yml | 💥💥 | 75.00% | +| yaml/yaml-test-suite/snippet: 2G84-4.yaml - Literal modifers | 💥 | 50.00% | +| yaml/yaml-test-suite/snippet: 6HB6.yaml - Spec Example 6.1. Indentation Spaces | 💥 | 84.62% | +| yaml/yaml-test-suite/snippet: 9KAX.yaml - Various combinations of tags and anchors | 💥 | 75.00% | +| yaml/yaml-test-suite/snippet: 9SA2.yaml - Multiline double quoted flow mapping key | 💥 | 50.00% | +| yaml/yaml-test-suite/snippet: BU8L.yaml - Node Anchor and Tag on Seperate Lines | 💥 | 50.00% | +| yaml/yaml-test-suite/snippet: CT4Q.yaml - Spec Example 7.20. Single Pair Explicit Entry | 💥 | 25.00% | +| yaml/yaml-test-suite/snippet: F2C7.yaml - Anchors and Tags | 💥 | 75.00% | +| yaml/yaml-test-suite/snippet: HWV9.yaml - Document-end marker | 💥 | 66.67% | +| yaml/yaml-test-suite/snippet: JEF9-3.yaml - Trailing whitespace in streams | 💥 | 66.67% | +| yaml/yaml-test-suite/snippet: K3WX.yaml - Colon and adjacent value after comment on next line | 💥 | 25.00% | +| yaml/yaml-test-suite/snippet: L24T-2.yaml - Trailing line of spaces | 💥 | 80.00% | +| yaml/yaml-test-suite/snippet: L24T.yaml - Trailing line of spaces | 💥 | 80.00% | +| yaml/yaml-test-suite/snippet: NJ66.yaml - Multiline plain flow mapping key | 💥 | 50.00% | +| yaml/yaml-test-suite/snippet: UT92.yaml - Spec Example 9.4. Explicit Documents | 💥 | 66.67% | +| yaml/yaml-test-suite/snippet: Y79Y-2.yaml - Tabs in various contexts | 💥 | 66.67% | + +## markdown + +1529/1548 (98.77%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| markdown/blockquote/ignore-code.md | 💥✨✨ | 99.67% | +| markdown/blockquote/notext-end.md | 💥💥✨ | 94.07% | +| markdown/code/lwc/lwc.md | 💥✨ | 90.00% | +| markdown/commonmark-test-suite/snippet: example-11.md (Tabs) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-2.md (Tabs) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-412.md (Emphasis and strong emphasis) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-50.md (Thematic breaks) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-51.md (Thematic breaks) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-52.md (Thematic breaks) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-53.md (Thematic breaks) | 💥 | 0.00% | +| markdown/commonmark-test-suite/snippet: example-54.md (Thematic breaks) | 💥 | 0.00% | +| markdown/link/encodedLink.md | 💥 | 86.67% | +| markdown/markdown/real-world-case.md | 💥💥 | 99.81% | +| markdown/paragraph/cjk.md | 💥💥✨ | 93.33% | +| markdown/splitCjkText/symbolSpaceNewLine.md | 💥 | 20.25% | +| markdown/thematicBreak/simple.md | 💥 | 0.00% | + +## html + +221/229 (96.51%) + +### Failed + +| Spec path | Failed or Passed | Match ratio | +| :-------- | :--------------: | :---------: | +| html/basics/broken-html.html | 💥 | 88.89% | +| html/basics/with-colon.html | 💥 | 98.37% | +| html/prettier_ignore/unclosed2.html | 💥 | 80.00% | +| html/tags/tags.html | 💥✨✨✨ | 99.82% | +| html/tags/tags2.html | 💥💥✨✨ | 97.17% | +| misc/filename-matters/html-doctype/snippet: #0 | 💥 | 0.00% | + +### Error + +- misc/filename-matters/html-doctype/xhtml-doctype.xhtml: Syntax error: missing end tag of raw text element at 16..74 diff --git a/apps/oxfmt/package.json b/apps/oxfmt/package.json index 1fc8722816..5f71ecfa71 100644 --- a/apps/oxfmt/package.json +++ b/apps/oxfmt/package.json @@ -15,7 +15,9 @@ "build-napi-release": "pnpm run build-napi --release --features allocator", "build-js": "node scripts/build.js", "test": "vitest run", - "conformance": "node conformance/run.ts", + "conformance": "pnpm run conformance:prettier && pnpm run conformance:live", + "conformance:live": "node conformance/run_live.ts", + "conformance:prettier": "node conformance/run_prettier.ts", "download-fixtures": "node conformance/download-fixtures.js", "generate-config-types": "node scripts/generate-config-types.ts" }, diff --git a/crates/oxc_formatter_core/FORMATTER_POLICY.md b/crates/oxc_formatter_core/FORMATTER_POLICY.md index dc76a352e0..6133b8245a 100644 --- a/crates/oxc_formatter_core/FORMATTER_POLICY.md +++ b/crates/oxc_formatter_core/FORMATTER_POLICY.md @@ -18,7 +18,7 @@ A conflict between the two is a bug in one of them: until it is fixed, the crate - The goal is CONSISTENT, PREDICTABLE, EXPLAINABLE behavior, not 100% parity with Prettier - Consistency and a sound rationale win over parity: the `DIVERGENCES.md` files record where they already did - Conformance is a measure, not a target: a deliberate divergence lowers it and is still correct -- The single oracle is Prettier's OUTPUT at the pin, the version in `apps/oxfmt/package.json`: the bundle, the conformance suite (`oxc_formatter_tests`) and fixture verification all use it +- The single oracle is Prettier's OUTPUT at the pin, the version in `apps/oxfmt/package.json`: the bundle, the conformance suites (`oxc_formatter_tests`, `apps/oxfmt`) and fixture verification all use it - Its source code is an analysis aid, never a porting target: pin behavior with fixtures, do not mirror its logic - A mismatch is fixed to match the pin, unless an admission reason decides otherwise (see "Known divergences") - Prettier `main` and its issues are evidence, not an oracle: they show whether the pin's output is Prettier's intent, a known bug, or a bug already fixed there @@ -189,8 +189,15 @@ It is gitignored, so use `rg --no-ignore` (or `-u`) when searching it. ### E2E conformance (`apps/oxfmt`) -The embedded-language features (e.g. xxx-in-js / js-in-xxx) are validated end-to-end through Oxfmt. +The embedded-language features (e.g. xxx-in-js / js-in-xxx) are validated end-to-end through Oxfmt, and so is Prettier's suite as a whole, embedded parts included. Requires a dev build first. A language crate's fixtures cover only its own side (e.g. `format_to_ir` on a fragment): they run without a dispatcher, so an embedded body stays verbatim. +```sh +pnpm --dir apps/oxfmt build-dev && pnpm --dir apps/oxfmt download-fixtures +pnpm --dir apps/oxfmt conformance +# Debug Prettier's suite for a path +pnpm --dir apps/oxfmt conformance:prettier --filter <path> +``` + Some categories also run on real-world repositories; see `apps/oxfmt`'s `AGENTS.md`.