mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
test(oxfmt): unified prettier conformance (#28529)
Up until now, conformance with Prettier was handled separately by each formatter crate and oxfmt. This was because the individual formatters were unable to handle embedded formatting. While that approach worked fine for languages like JSON and CSS, I realized it would become inconvenient as we moved forward with supporting languages like Vue.
This commit is contained in:
1 parent
9b6b7bf98f
commit
60efdd3d0a
8 files changed
+918
-8
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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 <substring>
|
||||
```
|
||||
|
||||
To manually verify the CLI behavior after building:
|
||||
|
||||
@@ -544,7 +544,7 @@ function writeReport(results: CategoryResult[], divergences: Map<string, string[
|
||||
}
|
||||
|
||||
mkdirSync(SNAPSHOTS_DIR, { recursive: true });
|
||||
const outPath = join(SNAPSHOTS_DIR, "conformance.snap.md");
|
||||
const outPath = join(SNAPSHOTS_DIR, "live.snap.md");
|
||||
writeFileSync(outPath, lines.join("\n"));
|
||||
console.log("=".repeat(60));
|
||||
console.log(`Report written to ${relative(process.cwd(), outPath)}`);
|
||||
@@ -0,0 +1,474 @@
|
||||
// oxlint-disable no-console, no-await-in-loop
|
||||
|
||||
import { createTwoFilesPatch, diffLines } from "diff";
|
||||
import { globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, extname, join, relative } from "node:path";
|
||||
import { parseArgs } from "node:util";
|
||||
import { format, type Language } from "../dist/index.js";
|
||||
|
||||
// Compares oxfmt with the committed outputs of Prettier's own test suite (`tests/format/**/__snapshots__`),
|
||||
// so Prettier itself never runs here.
|
||||
// `--filter <substring>` 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<string, Language> = {
|
||||
".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, (name: string) => 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 <id> {}`
|
||||
"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<string, string> = { LF: "\n", CRLF: "\r\n", CR: "\r" };
|
||||
const NAME_BY_EOL: Record<string, string> = { "\n": "LF", "\r\n": "CRLF", "\r": "CR" };
|
||||
|
||||
type Options = Record<string, unknown>;
|
||||
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<string>;
|
||||
};
|
||||
|
||||
const { filter } = parseArgs({ options: { filter: { type: "string" } } }).values;
|
||||
const rowsByLanguage = new Map<Language, Map<string, Row>>();
|
||||
|
||||
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, `<title> 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)}`);
|
||||
}
|
||||
File renamed without changes.
@@ -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
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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`.
|
||||
Reference in new issue
Block a user