test(oxfmt): track classified divergences (#28533)

Finalize on the stack of changes: classifying the divergences and
cleaning up.
This commit is contained in:
Yuji Sugiura authored and GitHub committed 2026-10-30 22:15:37 +09:00
1 parent 211126a771
commit e3f90b1f80
22 files changed
+1115 -582

No files matched your search

+1 -1
View File
@@ -346,7 +346,7 @@ jobs:
id: conformance-fixtures
if: steps.filter-conformance.outputs.changed == 'true'
with:
# Warm fixtures skip download entirely (`.version` stamps in download-fixtures.js)
# Warm fixtures skip download entirely (`.version` stamps in download-fixtures.ts)
path: ./apps/oxfmt/conformance/fixtures/externals
background: true
- if: steps.filter.outputs.changed == 'true' || steps.filter-conformance.outputs.changed == 'true'
+4 -11
View File
@@ -81,22 +81,15 @@ Known divergences live in DIVERGENCES.md (embedding / dispatch layer only); sing
- The pin is a minimal repro in `conformance/fixtures/edge-cases/`
- `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
- Each input runs under its spec's own options only
- Each input runs under its spec's own options only, and is also checked for idempotency and its CRLF / CR / BOM variants
- `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` stay as written, raw shapes are their job
- `conformance/run_live.ts` derives the file-to-entry map from every backticked `externals/` / `edge-cases/` path in every `DIVERGENCES.md`, prints the ref next to each failure,
and warns on a failure no entry lists (unclassified) and on a listed file that passes (stale)
- Both runners name a file as its report prints it (its path under `conformance/fixtures/`, `<dir>/snippet: <title>` for a snippet), as `DIVERGENCES.md` lists it in `Pin:` / `Conformance:` lines (`conformance/shared.ts`), and fail on a listed path that passes (stale)
- Both reports list the entries under each failing file, `❓unclassified` when none does (those files first); `run_live.ts` also warns on an unclassified one
- 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
Conformance failures (`conformance/snapshots/`) no entry accounts for, to fix rather than admit:
- `jsdoc`: `externals/svelte/compiler/print/index.js`
- `if (!(a && b))` hugs `!(` to the head paren since prettier/prettier#18401, not ported yet (Prettier conformance `js/if/condition-break/unary-expression.js`; unrelated to JSDoc)
- `astro`: `externals/plugin-astro/other/prettier-ignore-js/input.astro`
- A `// prettier-ignore`d expression statement without `;` gets `;` appended (`oxc_formatter` itself, reproducible in plain `.ts`; unrelated to Astro)
### Embedded language formatting
Embedded languages (e.g. css-in-js, CSS front matter YAML) go through the `FormatDispatcher` (defined in `oxc_formatter_core`) assembled by `src/core/embed/dispatcher.rs`.
@@ -115,7 +108,7 @@ The pure Rust build runs fallback-less, so non-native embeds (angular-in-js, cus
The roots install `SessionServices`, all via `embed/services.rs::for_root` (one definition per build; the napi one takes the `ExternalServices` transport, and adds the Prettier fallback / string embedder / Tailwind sorter to the registry dispatcher): the JS/TS, CSS, Markdown and HTML file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte/Astro `<script>` and Astro frontmatter root (`api/text_to_doc_api.rs`, `VirtualDocument` session, napi only).
Which languages may dispatch AT ALL from a given host is the host crate's own gate (e.g. `oxc_formatter_css` dispatches only `yaml`/`toml` front matter, `oxc_formatter_markdown` every fenced code block's language); the shared `route_embedded()` table then decides who serves the language.
Adding a dispatch call to a host crate is therefore a routing decision (check `route_embedded()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
Adding a dispatch call to a host crate is therefore a routing decision (check `route_embedded()` and the xxx-in-yyy categories of `conformance:live` when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
`embeddedLanguageFormatting: off` installs no dispatcher, every builder consults the same off-gate, `ResolvedDispatchConfig::is_embedded_formatting_enabled`.
Tracing span namespaces: `oxfmt::embed::` = pure Rust work, `oxfmt::external::` = napi-crossing calls.
+17 -7
View File
@@ -6,7 +6,8 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
- Why: uniform-rule (embedded script formats like its standalone file)
- Pin: `conformance/fixtures/edge-cases/js-in-vue/generic-trailing-comma.vue`
- Conformance: `externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue`
- Conformance:
- `externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue`
```vue
<!-- input -->
@@ -33,7 +34,8 @@ in ts-in-md and plain `.ts` — one rule over that internal inconsistency, the s
- Why: cost
- Pin: `conformance/fixtures/edge-cases/css-in-js/styled-extend-tag.js`
- Conformance: `js/multiparser-css/styled-components.js`
- Conformance:
- `externals/prettier/js/multiparser-css/styled-components.js`
```js
/* input */
@@ -124,7 +126,8 @@ Under `embeddedLanguageFormatting: off`, the source shape still decides, like Pr
- Why: semantics (prettier/prettier#19908)
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/nested-fence-length.md`
- Conformance: `js/multiparser-markdown/codeblock.js`
- Conformance:
- `externals/prettier/js/multiparser-markdown/codeblock.js`
`````markdown
<!-- input -->
@@ -193,7 +196,8 @@ Prettier formats it when a space separates the language, and keeps `js{1,3}` as
- Why: uniform-rule (code fence names follow Shiki's vocabulary)
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/lwc-code-block.md`
- Conformance: `markdown/code/lwc/lwc.md`
- Conformance:
- `externals/prettier/markdown/code/lwc/lwc.md`
````markdown
<!-- input -->
@@ -249,7 +253,10 @@ Prettier keeps `json` / `json5` blocks as written: their parser rejects a blank
- Why: cost
- Pin: `conformance/fixtures/edge-cases/astro/style-lang-sass.astro`
- Conformance: `externals/plugin-astro/styles/with-sass/input.astro`, `externals/plugin-astro/styles/with-indented-sass/input.astro`, `externals/plugin-astro/styles/format-nested-sass-style-tag-content/input.astro`
- Conformance:
- `externals/plugin-astro/styles/with-sass/input.astro`
- `externals/plugin-astro/styles/with-indented-sass/input.astro`
- `externals/plugin-astro/styles/format-nested-sass-style-tag-content/input.astro`
```astro
<!-- input -->
@@ -374,7 +381,9 @@ Prettier looks for the placeholder as it wrote it, so the lowercased one stays i
- Why: uniform-rule (same construct, same output: an expression outside `pre`-like content, whose lines indent from the line its `${` starts on; prettier/prettier#20355)
- Pin: `conformance/fixtures/edge-cases/html-in-js/pre-like-template-expression.js`
- Conformance: `edge-cases/html-in-js/template-expression-indent.js`, `js/embeded/indention/19518.js`
- Conformance:
- `edge-cases/html-in-js/template-expression-indent.js`
- `externals/prettier/js/embeded/indention/19518.js`
```js
// input
@@ -420,7 +429,8 @@ Reformatting the expression changes no content: its source is code, not text in
- Why: uniform-rule (same construct, same output: an element htm's `<//>` closes, which Prettier formats; prettier/prettier#17849)
- Pin: `conformance/fixtures/edge-cases/html-in-js/dynamic-tag-name.js`
- Conformance: `externals/webawesome/button/button.ts`
- Conformance:
- `externals/webawesome/button/button.ts`
```js
// input
@@ -14,7 +14,7 @@ const execAsync = promisify(exec);
const externalsDir = join(import.meta.dirname, "fixtures", "externals");
// `repo` is `<owner>/<name>` plus the directory to take, `version` is any ref (a tag or a commit)
const sources = [
export const sources = [
// Prettier's own suite (`run_prettier.ts`)
{
name: "prettier",
@@ -118,54 +118,71 @@ const sources = [
},
];
// Names given as arguments (e.g. `prettier`) narrow the download, none means every source
const names = process.argv.slice(2);
type Source = (typeof sources)[number];
// Group sources by archive, so an archive shared by several sources downloads once.
const sourcesByArchive = Map.groupBy(
names.length === 0 ? sources : sources.filter(({ name }) => names.includes(name)),
({ repo, version }) => `${repo.split("/").slice(0, 2).join("/")}#${version}`,
);
/** By its stamp, written last, so a half-downloaded tree never counts. */
const isDownloaded = ({ name, repo, version }: Source) => {
const stamp = join(externalsDir, name, ".version");
return existsSync(stamp) && readFileSync(stamp, "utf8").trim() === `${repo}#${version}`;
};
await Promise.all(
[...sourcesByArchive.values()].map(async (group) => {
// Stamp-based skip: the stamp is written last, so a half-downloaded tree is always re-done.
const stale = group.filter(({ name, repo, version }) => {
const stamp = join(externalsDir, name, ".version");
const upToDate =
existsSync(stamp) && readFileSync(stamp, "utf8").trim() === `${repo}#${version}`;
if (upToDate) console.log(`Up-to-date: ${name}@${version}`);
return !upToDate;
});
if (stale.length === 0) return;
/** Throws unless every source in `names` is downloaded at its pinned version, a runner reading an older one reports wrong. */
export function assertDownloaded(names: string[]) {
const stale = sources.filter((source) => names.includes(source.name) && !isDownloaded(source));
if (stale.length > 0) {
const list = stale.map(({ name, version }) => `${name}@${version}`).join(", ");
throw new Error(`Not downloaded: ${list}, run \`download-fixtures\``);
}
}
const [owner, repoName] = group[0].repo.split("/");
const { version } = group[0];
console.log(`Downloading ${owner}/${repoName}@${version}...`);
const tmp = mkdtempSync(join(tmpdir(), "oxfmt-fixtures-"));
const tarball = join(tmp, "archive.tar.gz");
await execAsync(
`curl -fsSL -o "${tarball}" https://codeload.github.com/${owner}/${repoName}/tar.gz/${version}`,
);
const top = await topDirectory(tarball);
// @ts-expect-error -- `import.meta.main` is newer than the `@types/node` (24.1) here
if (import.meta.main) {
// Names given as arguments (e.g. `prettier`) narrow the download, none means every source
const names = process.argv.slice(2);
for (const { name, repo } of stale) {
const dest = join(externalsDir, name);
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
const subdir = repo.split("/").slice(2);
// Group sources by archive, so an archive shared by several sources downloads once.
const sourcesByArchive = Map.groupBy(
names.length === 0 ? sources : sources.filter(({ name }) => names.includes(name)),
({ repo, version }) => `${repo.split("/").slice(0, 2).join("/")}#${version}`,
);
await Promise.all(
[...sourcesByArchive.values()].map(async (group) => {
const stale = group.filter((source) => {
const upToDate = isDownloaded(source);
if (upToDate) console.log(`Up-to-date: ${source.name}@${source.version}`);
return !upToDate;
});
if (stale.length === 0) return;
const [owner, repoName] = group[0].repo.split("/");
const { version } = group[0];
console.log(`Downloading ${owner}/${repoName}@${version}...`);
const tmp = mkdtempSync(join(tmpdir(), "oxfmt-fixtures-"));
const tarball = join(tmp, "archive.tar.gz");
await execAsync(
`tar -xzf "${tarball}" -C "${dest}" --strip-components=${subdir.length + 1} "${[top, ...subdir].join("/")}"`,
`curl -fsSL -o "${tarball}" https://codeload.github.com/${owner}/${repoName}/tar.gz/${version}`,
);
writeFileSync(join(dest, ".version"), `${repo}#${version}`);
console.log(`Done: ${name}@${version}`);
}
rmSync(tmp, { recursive: true });
}),
);
const top = await topDirectory(tarball);
for (const { name, repo } of stale) {
const dest = join(externalsDir, name);
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
const subdir = repo.split("/").slice(2);
await execAsync(
`tar -xzf "${tarball}" -C "${dest}" --strip-components=${subdir.length + 1} "${[top, ...subdir].join("/")}"`,
);
writeFileSync(join(dest, ".version"), `${repo}#${version}`);
console.log(`Done: ${name}@${version}`);
}
rmSync(tmp, { recursive: true });
}),
);
}
/** The archive's top directory, GitHub's `<name>-<ref>` with the ref normalized (e.g. no leading `v`). */
async function topDirectory(tarball) {
async function topDirectory(tarball: string) {
const tar = spawn("tar", ["-tzf", tarball]);
const [line] = await once(createInterface({ input: tar.stdout }), "line");
tar.kill();
+25 -51
View File
@@ -1,29 +1,24 @@
// oxlint-disable no-console, no-await-in-loop
import { createTwoFilesPatch } from "diff";
import {
existsSync,
globSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join, relative } from "node:path";
import prettier from "prettier";
import * as astroPlugin from "prettier-plugin-astro";
import * as sveltePlugin from "prettier-plugin-svelte";
import { format } from "../dist/index.js";
import { assertDownloaded, sources } from "./download-fixtures.ts";
import {
collectDivergences,
type Divergence,
divergencesOf,
listDivergences,
FIXTURES_DIR,
SNAPSHOTS_DIR,
checkStaleDivergences,
} from "./shared.ts";
const CONFORMANCE_DIR = import.meta.dirname;
const FIXTURES_DIR = join(CONFORMANCE_DIR, "fixtures");
const EXTERNALS_DIR = join(FIXTURES_DIR, "externals");
const SNAPSHOTS_DIR = join(CONFORMANCE_DIR, "snapshots");
const REPO_ROOT = join(CONFORMANCE_DIR, "..", "..", "..");
const DIVERGENCES_FILES = globSync("{apps/oxfmt,crates/oxc_formatter*}/DIVERGENCES.md", {
cwd: REPO_ROOT,
});
type Category = {
name: string;
@@ -229,6 +224,7 @@ const categories: Category[] = [
// ---
assertDownloaded(sources.map(({ name }) => name).filter((name) => name !== "prettier"));
const divergences = collectDivergences();
const results: CategoryResult[] = [];
@@ -255,39 +251,20 @@ for (const category of categories) {
writeReport(results, divergences);
const failedNames = new Set(
results.flatMap((r) => r.optionSetResults.flatMap((o) => o.failures.map((f) => f.name))),
);
const failedNames = [
...new Set(
results.flatMap((r) => r.optionSetResults.flatMap((o) => o.failures.map((f) => f.name))),
),
];
for (const name of failedNames) {
if (!divergences.has(name)) {
if (divergencesOf(divergences, name).length === 0) {
console.warn(`WARNING: "${name}" fails and no DIVERGENCES.md entry lists it (unclassified)`);
}
}
for (const [name, refs] of divergences) {
if (!failedNames.has(name)) {
console.warn(`WARNING: "${name}" passes but ${refs.join(", ")} lists it, remove it?`);
}
}
checkStaleDivergences(divergences, failedNames, { prettierSuite: false });
// ---
/** Fixture name -> `<file>#<slug>` for every backticked `externals/` / `edge-cases/` path in a DIVERGENCES.md entry. */
function collectDivergences(): Map<string, string[]> {
const map = new Map<string, string[]>();
for (const file of DIVERGENCES_FILES) {
let slug = "";
for (const line of readFileSync(join(REPO_ROOT, file), "utf8").split("\n")) {
if (line.startsWith("## ")) slug = line.slice(3).trim();
for (const [, name] of line.matchAll(
/`(?:conformance\/fixtures\/)?((?:externals|edge-cases)\/[^`]+)`/g,
)) {
map.set(name, [...(map.get(name) ?? []), `${file}#${slug}`]);
}
}
}
return map;
}
type Fixture = { name: string; fullPath: string; isProject: boolean };
type Failure = {
@@ -400,7 +377,7 @@ async function formatWithOxfmt(
return res.errors.length === 0 ? res.code : "ERROR";
}
function writeReport(results: CategoryResult[], divergences: Map<string, string[]>) {
function writeReport(results: CategoryResult[], divergences: Divergence[]) {
const lines: string[] = [];
const diffsDir = join(SNAPSHOTS_DIR, "diffs");
@@ -451,14 +428,11 @@ function writeReport(results: CategoryResult[], divergences: Map<string, string[
lines.push("");
if (r.failures.length > 0) {
for (const failure of r.failures) {
const safeName = failure.name.replaceAll("/", "__");
const diffRelPath = `diffs/${result.name}/${safeName}.md`;
const refs = divergences.get(failure.name)?.join(", ") ?? "unclassified";
lines.push(`- [${failure.name}](${diffRelPath})`);
lines.push(` - ${refs}`);
}
lines.push("");
const items = r.failures.map(({ name }): [string, string] => {
const diffRelPath = `diffs/${result.name}/${name.replaceAll("/", "__")}.md`;
return [name, `- [${name}](${diffRelPath})`];
});
lines.push(...listDivergences(divergences, items), "");
}
}
}
+54 -26
View File
@@ -5,13 +5,22 @@ 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";
import { assertDownloaded } from "./download-fixtures.ts";
import {
PRETTIER_SUITE,
FIXTURES_DIR,
SNAPSHOTS_DIR,
collectDivergences,
type Divergence,
listDivergences,
checkStaleDivergences,
} from "./shared.ts";
// 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");
const SUITE_DIR = join(FIXTURES_DIR, PRETTIER_SUITE);
/** Extension of the filename oxfmt is given → its language (the `associations` name), in the report's order */
const LANGUAGES: Record<string, Language> = {
@@ -46,6 +55,7 @@ const jsLike = (ext: string) => (name: string) =>
/**
* 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.
* A section listing none (Prettier infers it from the filepath, `misc/parser-inference/`) is not run either.
*/
const PARSERS: Record<string, (name: string) => string> = {
babel: jsLike(".js"),
@@ -155,6 +165,10 @@ const IGNORE = [
"typescript/tsx/comma/snippet: test.js",
"typescript/tsx/comma/snippet: test.cjs",
"typescript/tsx/comma/snippet: test.mjs",
// No known filepath for Prettier, which keeps `<T,>` and the doctype case, but run here as `.ts` / `.html`
"typescript/tsx/comma/snippet: test.unknown",
"typescript/tsx/comma/snippet: unnamed",
"misc/filename-matters/html-doctype/snippet: #0",
"js/babel-plugins/typescript.js",
// Ambiguous await
"js/top-level-await",
@@ -179,9 +193,9 @@ const IGNORE = [
"css/atrule/if-else.css",
// Depends on Prettier's test-only plugins (`tests/config/prettier-plugins/`)
"vue/with-plugins/",
"misc/front-matter/with-plugins/",
"misc/plugins/",
"vue/with-plugins/",
];
/** Sections not run, by their options */
@@ -190,6 +204,8 @@ const skipSection = (options: Options) =>
"cursorOffset" in options
// Rejected by oxfmt's config, not supported yet
|| options.experimentalTernaries === true
// Deprecated alias of `bracketSameLine`, which oxfmt's config does not take
|| "jsxBracketSameLine" in options
// 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
@@ -226,6 +242,8 @@ type Row = {
};
const { filter } = parseArgs({ options: { filter: { type: "string" } } }).values;
assertDownloaded(["prettier"]);
const rowsByLanguage = new Map<Language, Map<string, Row>>();
for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", {
@@ -249,7 +267,7 @@ for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", {
const name = title.replace(/ - \{.*\}$/, "");
const path = `${dir}/${name}`;
if (IGNORE.some((ignore) => path.startsWith(ignore))) continue;
if (filter && !path.includes(filter)) continue;
if (filter && !(PRETTIER_SUITE + path).includes(filter)) continue;
const { parsers, options, expected } = section;
const input = name.startsWith("snippet: ")
@@ -274,7 +292,9 @@ for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", {
const language = LANGUAGES[extname(filename)];
const rows = rowsByLanguage.get(language) ?? new Map<string, Row>();
rowsByLanguage.set(language, rows);
const row = rows.get(path) ?? {
// Named by its fixture path, as DIVERGENCES.md lists it
const fixture = PRETTIER_SUITE + path;
const row = rows.get(fixture) ?? {
total: 0,
passed: 0,
failed: 0,
@@ -283,7 +303,7 @@ for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", {
nonIdempotent: null,
variants: new Set(),
};
rows.set(path, row);
rows.set(fixture, row);
row.total++;
if (result.error !== undefined) {
@@ -299,7 +319,10 @@ for (const snapPath of globSync("**/__snapshots__/format.test.js.snap", {
}
}
if (!filter) writeReport();
if (!filter) {
const divergences = collectDivergences();
checkStaleDivergences(divergences, writeReport(divergences), { prettierSuite: true });
}
// ---
@@ -425,7 +448,9 @@ function printResult(
console.log();
}
function writeReport() {
/** Returns the rows given `listDivergences` lines, all a DIVERGENCES.md entry may list. */
function writeReport(divergences: Divergence[]) {
const listable: string[] = [];
const lines: string[] = [];
for (const language of new Set(Object.values(LANGUAGES))) {
const rows = rowsByLanguage.get(language);
@@ -441,19 +466,16 @@ function writeReport() {
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][] = [
const lists: [string, (row: Row) => string | null, withRefs?: boolean][] = [
[
"Failed",
(r) => {
if (r.failed === 0) return null;
const marks = "💥".repeat(r.failed) + "✨".repeat(r.passed);
return ` (${marks}, ${((r.ratio / r.total) * 100).toFixed(2)}%)`;
},
true,
],
[
"Not idempotent",
(r) => (r.nonIdempotent === null ? null : r.nonIdempotent ? " (second pass errored)" : ""),
@@ -462,14 +484,19 @@ function writeReport() {
"Input variants differ",
(r) => (r.variants.size > 0 ? ` (${[...r.variants].join(", ")})` : null),
],
["Error", (r) => (r.error === null ? null : `: ${r.error}`)],
["Error", (r) => (r.error === null ? null : `: ${r.error}`), true],
];
for (const [heading, note] of lists) {
const entries = sorted.flatMap(([path, row]) => {
for (const [heading, note, withRefs] of lists) {
const items = sorted.flatMap(([path, row]): [string, string][] => {
const text = note(row);
return text === null ? [] : [`- ${path}${text}`];
return text === null ? [] : [[path, `- ${path}${text}`]];
});
if (entries.length > 0) lines.push(`### ${heading}`, "", ...entries, "");
if (items.length === 0) continue;
if (withRefs) listable.push(...items.map(([path]) => path));
const entries = withRefs
? listDivergences(divergences, items)
: items.map(([, line]) => line);
lines.push(`### ${heading}`, "", ...entries, "");
}
}
@@ -477,4 +504,5 @@ function writeReport() {
const outPath = join(SNAPSHOTS_DIR, "prettier.snap.md");
writeFileSync(outPath, lines.join("\n"));
console.log(`Report written to ${relative(process.cwd(), outPath)}`);
return listable;
}
+86
View File
@@ -0,0 +1,86 @@
// oxlint-disable no-console
import { globSync, readFileSync } from "node:fs";
import { join } from "node:path";
// Shared by `run_prettier.ts` and `run_live.ts`.
const REPO_ROOT = join(import.meta.dirname, "..", "..", "..");
export const FIXTURES_DIR = join(import.meta.dirname, "fixtures");
export const SNAPSHOTS_DIR = join(import.meta.dirname, "snapshots");
/** Prettier's own suite, the `run_prettier.ts` part of the fixture paths (relative to `FIXTURES_DIR`) */
export const PRETTIER_SUITE = "externals/prettier/";
/** A fixture path (relative to `FIXTURES_DIR`) and the `<file>#<slug>` of the DIVERGENCES.md entry listing it */
export type Divergence = [listed: string, ref: string];
/**
* Every path listed in a `Pin:` or `Conformance:` line, or its continuation lines.
* A path ending in `*` lists every path it starts with.
*/
export function collectDivergences(): Divergence[] {
const divergences: Divergence[] = [];
for (const file of globSync("{apps/oxfmt,crates/oxc_formatter*}/DIVERGENCES.md", {
cwd: REPO_ROOT,
})) {
let slug = "";
let inField = false;
for (const line of readFileSync(join(REPO_ROOT, file), "utf8").split("\n")) {
if (line.startsWith("## ")) slug = line.slice(3).trim();
if (line.startsWith("- ")) inField = /^- (Pin|Conformance):/.test(line);
else if (!line.startsWith(" ")) inField = false;
if (!inField) continue;
for (const [, name] of line.matchAll(
/`(?:conformance\/fixtures\/)?((?:externals|edge-cases)\/[^`]+)`/g,
)) {
divergences.push([name, `${file}#${slug}`]);
}
}
}
return divergences;
}
const covers = (listed: string, name: string) =>
listed.endsWith("*") ? name.startsWith(listed.slice(0, -1)) : name === listed;
/** The entries listing `name`. */
export function divergencesOf(divergences: Divergence[], name: string) {
return divergences.flatMap(([listed, ref]) => (covers(listed, name) ? [ref] : []));
}
/**
* Report lines of the failing `[name, line]`s, each followed by the entries listing it as sub-items.
* The ones no entry lists (`❓unclassified`) come first.
*/
export function listDivergences(
divergences: Divergence[],
failures: [name: string, line: string][],
) {
return failures
.map(([name, line]) => [line, divergencesOf(divergences, name)] as const)
.sort(([, a], [, b]) => Number(a.length > 0) - Number(b.length > 0))
.flatMap(([line, refs]) =>
[line].concat((refs.length > 0 ? refs : ["❓unclassified"]).map((ref) => ` - ${ref}`)),
);
}
/**
* Fails the run on every listed path of this runner's part (Prettier's suite or the rest) that no failure is.
* The report is written anyway, the exit code is what CI sees.
*/
export function checkStaleDivergences(
divergences: Divergence[],
failed: string[],
{ prettierSuite }: { prettierSuite: boolean },
) {
for (const [listed, ref] of divergences) {
if (
listed.startsWith(PRETTIER_SUITE) === prettierSuite
&& !failed.some((name) => covers(listed, name))
) {
console.error(`STALE: "${listed}" passes but ${ref} lists it, remove it`);
process.exitCode = 1;
}
}
}
+21 -12
View File
@@ -11,7 +11,8 @@
- [externals/vue-vben-admin/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue](diffs/js-in-vue/externals__vue-vben-admin__@core__ui-kit__shadcn-ui__src__components__render-content__render-content.vue.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue](diffs/js-in-vue/externals__vue-vben-admin__effects__common-ui__src__components__api-component__api-component.vue.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#ts-in-vue-generic-trailing-comma
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- apps/oxfmt/DIVERGENCES.md#ts-in-vue-generic-trailing-comma
### Option 2: 332/334 (99.40%)
@@ -22,7 +23,8 @@
- [edge-cases/js-in-vue/generic-trailing-comma.vue](diffs/js-in-vue/edge-cases__js-in-vue__generic-trailing-comma.vue.md)
- apps/oxfmt/DIVERGENCES.md#ts-in-vue-generic-trailing-comma
- [externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue](diffs/js-in-vue/externals__vue-vben-admin__effects__common-ui__src__components__api-component__api-component.vue.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#ts-in-vue-generic-trailing-comma
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- apps/oxfmt/DIVERGENCES.md#ts-in-vue-generic-trailing-comma
## gql-in-js
@@ -87,7 +89,8 @@
- [externals/webawesome/badge/badge.ts](diffs/html-in-js/externals__webawesome__badge__badge.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/webawesome/checkbox/checkbox.ts](diffs/html-in-js/externals__webawesome__checkbox__checkbox.ts.md)
@@ -111,7 +114,8 @@
- [externals/webawesome/number-input/number-input.ts](diffs/html-in-js/externals__webawesome__number-input__number-input.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/webawesome/page/page.styles.ts](diffs/html-in-js/externals__webawesome__page__page.styles.ts.md)
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position, crates/oxc_formatter_css/DIVERGENCES.md#grid-value-broken-in-function
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- crates/oxc_formatter_css/DIVERGENCES.md#grid-value-broken-in-function
- [externals/webawesome/popup/popup.ts](diffs/html-in-js/externals__webawesome__popup__popup.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/webawesome/qr-code/qr-code.ts](diffs/html-in-js/externals__webawesome__qr-code__qr-code.ts.md)
@@ -148,7 +152,8 @@
- [edge-cases/html-in-js/template-expression-indent.js](diffs/html-in-js/edge-cases__html-in-js__template-expression-indent.js.md)
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry, apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- apps/oxfmt/DIVERGENCES.md#dynamic-tag-name-end-tag
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
- [externals/webawesome/copy-button/copy-button.ts](diffs/html-in-js/externals__webawesome__copy-button__copy-button.ts.md)
@@ -232,10 +237,10 @@
{"printWidth":80,"astro":{}}
```
- [externals/plugin-astro/other/prettier-ignore-js/input.astro](diffs/astro/externals__plugin-astro__other__prettier-ignore-js__input.astro.md)
- ❓unclassified
- [edge-cases/astro/style-lang-sass.astro](diffs/astro/edge-cases__astro__style-lang-sass.astro.md)
- apps/oxfmt/DIVERGENCES.md#astro-style-lang-sass
- [externals/plugin-astro/other/prettier-ignore-js/input.astro](diffs/astro/externals__plugin-astro__other__prettier-ignore-js__input.astro.md)
- unclassified
- [externals/plugin-astro/styles/format-nested-sass-style-tag-content/input.astro](diffs/astro/externals__plugin-astro__styles__format-nested-sass-style-tag-content__input.astro.md)
- apps/oxfmt/DIVERGENCES.md#astro-style-lang-sass
- [externals/plugin-astro/styles/with-indented-sass/input.astro](diffs/astro/externals__plugin-astro__styles__with-indented-sass__input.astro.md)
@@ -392,9 +397,11 @@
- [externals/ng-zorro-antd/components/style/themes/dark.less](diffs/less/externals__ng-zorro-antd__components__style__themes__dark.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width
- [externals/ng-zorro-antd/components/style/themes/default.less](diffs/less/externals__ng-zorro-antd__components__style__themes__default.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width, crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- [externals/ng-zorro-antd/components/style/themes/variable.less](diffs/less/externals__ng-zorro-antd__components__style__themes__variable.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width, crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- [externals/ng-zorro-antd/components/table/style/index.less](diffs/less/externals__ng-zorro-antd__components__table__style__index.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#signed-value-resplit
- [externals/ng-zorro-antd/components/table/style/rtl.less](diffs/less/externals__ng-zorro-antd__components__table__style__rtl.less.md)
@@ -409,9 +416,11 @@
- [externals/ng-zorro-antd/components/style/mixins/customize.less](diffs/less/externals__ng-zorro-antd__components__style__mixins__customize.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#less-guard-list-inline
- [externals/ng-zorro-antd/components/style/themes/default.less](diffs/less/externals__ng-zorro-antd__components__style__themes__default.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width, crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- [externals/ng-zorro-antd/components/style/themes/variable.less](diffs/less/externals__ng-zorro-antd__components__style__themes__variable.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width, crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- crates/oxc_formatter_css/DIVERGENCES.md#trailing-line-comment-print-width
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
- [externals/ng-zorro-antd/components/table/style/rtl.less](diffs/less/externals__ng-zorro-antd__components__table__style__rtl.less.md)
- crates/oxc_formatter_css/DIVERGENCES.md#fill-break-position
@@ -535,4 +544,4 @@
```
- [externals/svelte/compiler/print/index.js](diffs/jsdoc/externals__svelte__compiler__print__index.js.md)
- unclassified
- ❓unclassified
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -18,7 +18,7 @@
"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",
"download-fixtures": "node conformance/download-fixtures.ts",
"generate-config-types": "node scripts/generate-config-types.ts"
},
"dependencies": {
+1
View File
@@ -4,6 +4,7 @@
"module": "Preserve",
"moduleResolution": "Bundler",
"noEmit": true,
"allowImportingTsExtensions": true,
"target": "ESNext",
"strict": true,
"skipLibCheck": true
-5
View File
@@ -252,8 +252,3 @@ cargo test -p oxc_formatter --test mod
# Run a subset by module path (directory structure = module hierarchy)
cargo test -p oxc_formatter fixtures::js::comments
```
### Prettier conformance
The failing JS/TS files in `apps/oxfmt/conformance/snapshots/prettier.snap.md` are only partly audited:
some are covered by an existing DIVERGENCES.md entry, the rest are unclassified, and no per-file list exists yet.
+56 -11
View File
@@ -6,7 +6,8 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
- Why: invariant
- Pin: `tests/fixtures/js/comments/array-holes.js`
- Conformance: `js/arrays/numbers-with-holes.js`
- Conformance:
- `externals/prettier/js/arrays/numbers-with-holes.js`
```js
// input
@@ -156,7 +157,9 @@ Binary-like chains hoist the comment in both formatters; the own-line invariant
- Why: uniform-rule (same construct, same output: any other node after the same suppression comment, see #eol-suppression-after-assign-colon)
- Pin: `tests/fixtures/ts/union/suppression.ts`
- Conformance: `typescript/union/comments/18379.ts`, `typescript/union/consistent-with-flow/prettier-ignore.ts`
- Conformance:
- `externals/prettier/typescript/union/comments/18379.ts`
- `externals/prettier/typescript/union/consistent-with-flow/prettier-ignore.ts`
A suppression comment before a union covers the whole union, wherever it sits: own-line, on the operator's line, or inline.
One after a `|` covers that member (both formatters).
@@ -230,7 +233,11 @@ Prettier moves the comment inside for `keyof`/type-operator operands while keepi
- Why: invariant (prettier/prettier#14617)
- Pin: `tests/fixtures/js/comments/assignment-eol-line-comment.js`, `tests/fixtures/ts/comments/operator-eol-line-comment.ts`
- Conformance: `js/comments/array-and-object.js`, `js/comments/assignment/variable-declarator.js`, `js/comments/variable-declarator.js`, `typescript/comments/type-literals.ts`
- Conformance:
- `externals/prettier/js/comments/array-and-object.js`
- `externals/prettier/js/comments/assignment/variable-declarator.js`
- `externals/prettier/js/comments/variable-declarator.js`
- `externals/prettier/typescript/comments/type-literals.ts`
Prettier's output crosses user content (the value the comment precedes, then the `;`) or changes line (own-lined).
An end-of-line line comment right after `=`/`:` keeps its position (`= // c` + mandatory break).
@@ -520,9 +527,33 @@ The shapes, each pinned by its fixture:
- Why: style-hold (oxc#25841)
- Pin: `tests/fixtures/ts/union/annotation-flat-retry.ts`
- Oxfmt: `externals/vue-vben-admin/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue`, `externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue`,
`externals/webawesome/badge/badge.ts`, `externals/webawesome/button/button.ts`, `externals/webawesome/callout/callout.ts`, `externals/webawesome/checkbox/checkbox.ts`, `externals/webawesome/color-picker/color-picker.ts`, `externals/webawesome/copy-button/copy-button.ts`, `externals/webawesome/details/details.ts`, `externals/webawesome/dropdown/dropdown.ts`, `externals/webawesome/dropdown-item/dropdown-item.ts`, `externals/webawesome/format-number/format-number.ts`, `externals/webawesome/icon/icon.ts`, `externals/webawesome/input/input.ts`,
`externals/webawesome/number-input/number-input.ts`, `externals/webawesome/page/page.ts`, `externals/webawesome/popup/popup.ts`, `externals/webawesome/qr-code/qr-code.ts`, `externals/webawesome/radio/radio.ts`, `externals/webawesome/radio-group/radio-group.ts`, `externals/webawesome/rating/rating.ts`, `externals/webawesome/select/select.ts`, `externals/webawesome/slider/slider.ts`, `externals/webawesome/switch/switch.ts`, `externals/webawesome/tag/tag.ts`, `externals/webawesome/textarea/textarea.ts`
- Conformance:
- `externals/vue-vben-admin/@core/ui-kit/shadcn-ui/src/components/render-content/render-content.vue`
- `externals/vue-vben-admin/effects/common-ui/src/components/api-component/api-component.vue`
- `externals/webawesome/badge/badge.ts`
- `externals/webawesome/button/button.ts`
- `externals/webawesome/callout/callout.ts`
- `externals/webawesome/checkbox/checkbox.ts`
- `externals/webawesome/color-picker/color-picker.ts`
- `externals/webawesome/copy-button/copy-button.ts`
- `externals/webawesome/details/details.ts`
- `externals/webawesome/dropdown/dropdown.ts`
- `externals/webawesome/dropdown-item/dropdown-item.ts`
- `externals/webawesome/format-number/format-number.ts`
- `externals/webawesome/icon/icon.ts`
- `externals/webawesome/input/input.ts`
- `externals/webawesome/number-input/number-input.ts`
- `externals/webawesome/page/page.ts`
- `externals/webawesome/popup/popup.ts`
- `externals/webawesome/qr-code/qr-code.ts`
- `externals/webawesome/radio/radio.ts`
- `externals/webawesome/radio-group/radio-group.ts`
- `externals/webawesome/rating/rating.ts`
- `externals/webawesome/select/select.ts`
- `externals/webawesome/slider/slider.ts`
- `externals/webawesome/switch/switch.ts`
- `externals/webawesome/tag/tag.ts`
- `externals/webawesome/textarea/textarea.ts`
- Drop when: the wait-and-see on Prettier 3.9's union style resolves (follow, or re-classify)
```ts
@@ -602,7 +633,9 @@ In the head the declaration has no terminator of its own; we keep it verbatim an
- Why: uniform-rule (same construct, same output: suppressed statement)
- Pin: `tests/fixtures/js/semicolons/suppressed-statement.js`, `tests/fixtures/ts/semicolons/suppressed-class-member.ts`
- Conformance: `js/ignore/issue-11077.js`, `markdown/markdown/real-world-case.md`
- Conformance:
- `externals/prettier/js/ignore/issue-11077.js`
- `externals/prettier/markdown/markdown/real-world-case.md`
```js
// input (semi: true)
@@ -813,7 +846,9 @@ The full callee list is `contains_a_test_pattern` in `src/utils/call_expression.
- Why: invariant
- Pin: `tests/fixtures/js/calls/callee-opener-comment.js`, `tests/fixtures/ts/calls/callee-opener-comment.ts`
- Conformance: `js/last-argument-expansion/edge_case.js`, `typescript/call/callee-comments.ts`
- Conformance:
- `externals/prettier/js/last-argument-expansion/edge_case.js`
- `externals/prettier/typescript/call/callee-comments.ts`
A call's `(` and `<` are delimiters (see AGENTS.md "Head-body and operator gaps (class 3)"), so a comment between the callee and the opener stays on the callee side (except AGENTS.md "Open debts").
@@ -933,7 +968,15 @@ A function type without type parameters, a constructor type (`<new <T>() => T>f`
- Why: uniform-rule (same construct, same output: expression statement)
- Pin: `tests/fixtures/ts/parenthesis/export-default-leftmost-parens.ts`
- Conformance: `js/export-default/class_instance.js`, `js/export-default/function_tostring.js`, `typescript/as/export_default_as.ts`, `typescript/export-default/function_as.ts`, `typescript/satisfies-operators/export-default-as.ts`
- Conformance:
- `externals/prettier/js/export-default/class_instance.js`
- `externals/prettier/js/export-default/function_tostring.js`
- `externals/prettier/js/export-default/parentheses/snippet: #9`
- `externals/prettier/js/export-default/parentheses/snippet: #11`
- `externals/prettier/js/export-default/parentheses/snippet: #12`
- `externals/prettier/typescript/as/export_default_as.ts`
- `externals/prettier/typescript/export-default/function_as.ts`
- `externals/prettier/typescript/satisfies-operators/export-default-as.ts`
```ts
// input
@@ -960,7 +1003,8 @@ Prettier's wrapping came from a syntax fix (prettier/prettier#6133), whose chang
- Why: uniform-rule (same construct, same output: `export default` and sequence arrow body)
- Pin: `tests/fixtures/ts/parenthesis/start-token-no-double-parens.ts`
- Conformance: `js/identifier/parentheses/let.js`
- Conformance:
- `externals/prettier/js/identifier/parentheses/let.js`
```ts
// input
@@ -1204,7 +1248,8 @@ Prettier moves it before the object for `.b`, and after the `.` for `.#b`; both
- Why: uniform-rule (same construct, same output: `(a + b).c` and `(await a)[c]`)
- Pin: `tests/fixtures/js/parenthesis/computed-member-object-paren-break.js`
- Conformance: `js/ternaries/indent-after-paren.js`
- Conformance:
- `externals/prettier/js/ternaries/indent-after-paren.js`
```js
// input (printWidth: 80)
@@ -64,21 +64,20 @@ Rules:
- The owning layer decides the behavior: a language crate for single-language behavior, `apps/oxfmt` for embedding
- A behavior core decides for every language (see "Text width") is a rule in this document;
each language crate where it shows has its own entry and pin fixture, citing that section
- The pin fixture is minimal and lives in the owning layer, distilled from whatever exposed the divergence; a conformance file is never the pin fixture, it stays a regression net
- The pin fixture is minimal and lives in the owning layer, distilled from whatever exposed the divergence; a file of an external suite (`conformance/fixtures/externals/`) is never the pin fixture, it stays a regression net
- The entry's `Pin:` names the fixture, which needs no link back; the why and the deviating lines live in the entry
- `DIVERGENCES.md#<slug>` is the one form for referring to an entry (docs, source, fixtures); never a quoted or bare slug, never an upstream issue alone
- Entry format, with OUR behavior as the spec
- H2 slug: the stable anchor
- `Why:` required; the reason keyword, its parenthetical, then upstream issues as bare references: `uniform-rule (same construct, same output: X; prettier/prettier#NNNN)`
- `Pin:` required; the fixture path
- `Conformance:` optional; the failing files in Prettier's suite (`apps/oxfmt`'s `conformance:prettier`)
- `Oxfmt:` optional, language crates only; the oxfmt externals failing on the entry
- `Conformance:` optional; the files failing `apps/oxfmt`'s conformance on the entry, one per sub-item (a note may follow it), as paths under `apps/oxfmt/conformance/fixtures/`; a path ending in `*` covers every path it starts with (e.g. a group of snippets)
- `Drop when:` optional; only for a condition more specific than the pin-bump default
- Body: an input/ours/prettier code example
- No status, dates, or authors: listed = accepted, condition met = delete (git holds history)
- Lifecycle
- A new conformance failure is acceptable only under this policy; it stays counted as a failure
- A failing file no entry lists is unclassified; the crate's `AGENTS.md` conformance section holds only those
- A failing file no entry lists is unclassified
- At a pin bump (`apps/oxfmt/package.json`, then `download-fixtures`), re-run conformance: an entry no longer observed is deleted, the rest are re-checked against their reason
- A documented divergence is never "fixed" by following Prettier
- `DIVERGENCES.md` holds entries only, opening with one back-reference to this section; `AGENTS.md` keeps policy translations and mechanism prose
@@ -155,7 +154,7 @@ node apps/oxfmt/node_modules/prettier/bin/prettier.cjs --parser <parser> --print
NOTE: Prettier's default `printWidth` is `80`, but Oxfmt is `100`.
Fixture tests and Prettier conformance re-format every output and record idempotency violations in their snapshots/reports.
Fixture tests and `conformance:prettier` re-format every output and record idempotency violations in their snapshots/reports.
Fixture tests also assert the lossless contract through `FixtureFormatter::fingerprint` (`oxc_formatter_tests`): a loss fails the test instead of being pinned.
### Fixture tests
-12
View File
@@ -201,18 +201,6 @@ NOT covered: `$(var)` interpolation inside values and selectors (`.icon.is-$(net
The harness snapshots both `--print-width 80` and `100`; verify fixtures at both widths.
### Prettier conformance
Every failing file is accounted for by a DIVERGENCES.md entry's `Conformance:` line, except these unclassified hunks:
- `css/parens/parens.css` (~93% match) token-soup math spacing; besides the css-glued-minus-paren / unary-plus-glue hunks:
- normalization-direction difference (open question, low value)
- a math operator adjacent to a function/paren boundary gets uniform `op` spacing from Prettier regardless of source (`round(1.5)+2` -> `round(1.5) + 2`, calc `*`/`/`);
- ours preserves the source spacing per token (`prop13/14`, `prop57-60`, `prop73/74`)
- within-a-word runs (`1+1+1+1`, `calc(100%+2px)`) match
- glued number-ish runs are ONE postcss word and print raw (see `is_word_glued_number`)
- `css/font/font.css` run as SCSS (its spec also lists `scss`): `--custom(1px)/2` gets a space after `/` (`--custom(1px)/ 2`)
### Manual checks
```sh
+96 -29
View File
@@ -6,8 +6,14 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
- Why: uniform-rule (line_suffix is never measured)
- Pin: `tests/fixtures/format/less/trailing-inline-comment-width.less`
- Conformance: `less/less-test-suite/globalVars/extended.less`, `less/less-test-suite/color-functions/rgba.less`, `less/less-test-suite/extend-selector/extend-selector.less`
- Oxfmt: `externals/ng-zorro-antd/components/style/themes/default.less`, `externals/ng-zorro-antd/components/style/themes/variable.less`, `externals/ng-zorro-antd/components/style/themes/dark.less`, `externals/gitlab/stylesheets/pages/profile.scss`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-config/globalVars/extended.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/color-functions/rgba.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/extend-selector/extend-selector.less`
- `externals/ng-zorro-antd/components/style/themes/default.less`
- `externals/ng-zorro-antd/components/style/themes/variable.less`
- `externals/ng-zorro-antd/components/style/themes/dark.less`
- `externals/gitlab/stylesheets/pages/profile.scss`
```scss
/* input */
@@ -316,7 +322,8 @@ Prettier keeps SELECTOR interpolation verbatim.
- Why: uniform-rule (option governs: singleQuote)
- Pin: `tests/fixtures/format/scss/unknown-at-rule-edges.scss`, `tests/fixtures/format/css/prelude-string-requote.css`
- Oxfmt: `externals/mdn-css-guides/values_and_units/textual_data_types/index.md`
- Conformance:
- `externals/mdn-css-guides/values_and_units/textual_data_types/index.md`
```scss
/* input */
@@ -413,7 +420,9 @@ Prettier double-indents it (closing `)` floating between levels) when the neares
- Why: uniform-rule (comment presence never changes layout)
- Pin: `tests/fixtures/format/scss/map-comment-block-value-comma.scss`, `tests/fixtures/format/scss/variable-inline-comment.scss`
- Conformance: `scss/map/comment.scss` (the `key4` indent; its dropped trailing comma is `map-leading-comment-layout`), `scss/parens/2.scss`
- Conformance:
- `externals/prettier/scss/map/comment.scss` (the `key4` indent)
- `externals/prettier/scss/parens/2.scss`
```scss
/* input */
@@ -536,8 +545,21 @@ so a comment, a sibling token or an overflow hard-breaks that outer list around
- Why: uniform-rule (the formatter owns the trivia up to a terminator)
- Pin: `tests/fixtures/format/scss/terminator-gap.scss`
- Conformance: `css/stylefmt-repo/at-media/at-media.css`, `css/stylefmt-repo/cssnext-example/cssnext-example.css`, `css/stylefmt-repo/media-queries-ranges/media-queries-ranges.css`, `css/postcss-plugins/postcss-nesting.css`, `css/comments/declaration.css`,
`scss/comments/4878.scss`, `scss/map/function-argument/functional-argument.scss`, `scss/parens/issue-16594.scss`, `scss/trailing-comma/comments.scss`, `scss/trailing-comma/list.scss`, `scss/trailing-comma/variable.scss`, `scss/function/arbitrary-arguments-comment.scss`, `scss/map/15193.scss`, `scss/comments/variable-declaration.scss`
- Conformance:
- `externals/prettier/css/stylefmt-repo/at-media/at-media.css`
- `externals/prettier/css/stylefmt-repo/cssnext-example/cssnext-example.css`
- `externals/prettier/css/stylefmt-repo/media-queries-ranges/media-queries-ranges.css`
- `externals/prettier/css/postcss-plugins/postcss-nesting.css`
- `externals/prettier/css/comments/declaration.css`
- `externals/prettier/scss/comments/4878.scss`
- `externals/prettier/scss/map/function-argument/functional-argument.scss`
- `externals/prettier/scss/parens/issue-16594.scss`
- `externals/prettier/scss/trailing-comma/comments.scss`
- `externals/prettier/scss/trailing-comma/list.scss`
- `externals/prettier/scss/trailing-comma/variable.scss`
- `externals/prettier/scss/function/arbitrary-arguments-comment.scss`
- `externals/prettier/scss/map/15193.scss`
- `externals/prettier/scss/comments/variable-declaration.scss`
```scss
/* input */
@@ -632,7 +654,8 @@ Same-line trailing comments still glue (matching Prettier); moving an own-line c
- Why: uniform-rule (comment presence never changes layout)
- Pin: `tests/fixtures/format/scss/map-comment-only.scss`
- Conformance: `scss/map/comment.scss` (the map's trailing comma under `trailingComma: es5`)
- Conformance:
- `externals/prettier/scss/map/comment.scss` (the map's trailing comma under `trailingComma: es5`)
```scss
/* input */
@@ -726,7 +749,13 @@ the same `join(line)` + deferred `lineSuffix` artifact as "comment-only-map-inde
- Why: invariant
- Pin: `tests/fixtures/format/scss/line-comment-after-comma.scss`, `tests/fixtures/format/less/line-comment-after-comma.less`
- Conformance: `scss/comments/4594.scss`, `scss/comments/lists.scss`, `scss/comments/maps.scss`, `scss/trailing-comma/issue-6920.scss`, `scss/trailing-comma/comments.scss`, `less/comments/value-lists.less`
- Conformance:
- `externals/prettier/scss/comments/4594.scss`
- `externals/prettier/scss/comments/lists.scss`
- `externals/prettier/scss/comments/maps.scss`
- `externals/prettier/scss/trailing-comma/issue-6920.scss`
- `externals/prettier/scss/trailing-comma/comments.scss`
- `externals/prettier/less/comments/value-lists.less`
```scss
/* input */
@@ -821,7 +850,9 @@ so the entry BEFORE the comment (`in a,`) breaks away from `$k` too; the same so
- Why: invariant
- Pin: `tests/fixtures/format/less/variable-value-comments.less`, `tests/fixtures/format/less/important-comments.less`
- Conformance: `less/less-test-suite/comments/comments2.less`, `less/less-test-suite/property-name-interp/property-name-interp.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/comments/comments2.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/property-name-interp/property-name-interp.less`
```less
/* input */
@@ -845,7 +876,8 @@ and its at-rule printer drops the block comments in there (a trailing one surviv
- Why: uniform-rule (same construct, same output: selector-position `&:extend(...)`; prettier/prettier#19550)
- Pin: `tests/fixtures/format/less/extend-rule.less`
- Conformance: `less/less-test-suite/extend-chaining/extend-chaining.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/extend-chaining/extend-chaining.less`
- Drop when: the selector-list leak is fixed (prettier/prettier#19550 covered only the indentation) and the pin catches up
```less
@@ -872,7 +904,8 @@ postcss-less models the statement as a rule node, so the top-level selector-list
- Why: cost
- Pin: `tests/fixtures/format/css/token-soup-math-glue.css` (Css mode),
`tests/fixtures/format/less/signed-value-args.less` (Less mode)
- Conformance: `css/parens/parens.css` (`prop34`; the file also has unclassified diffs, see AGENTS.md)
- Conformance:
- `externals/prettier/css/parens/parens.css` (`prop34`; the file also has unclassified diffs)
```css
/* input */
@@ -894,8 +927,10 @@ A sign GLUED in the source never gains a space in either implementation (that di
- Why: uniform-rule (same construct, same output: the item in first position, an unsigned item)
- Pin: `tests/fixtures/format/css/signed-value-resplit.css`
- Conformance: `css/fill-value/fill.css`
- Oxfmt: `externals/gitlab/stylesheets/framework/variables.scss`, `externals/ng-zorro-antd/components/table/style/index.less`
- Conformance:
- `externals/prettier/css/fill-value/fill.css`
- `externals/gitlab/stylesheets/framework/variables.scss`
- `externals/ng-zorro-antd/components/table/style/index.less`
```css
/* input */
@@ -932,7 +967,8 @@ Ours prints a signed item as one word, like the same item in first position or u
- Why: uniform-rule (same construct, same output: the `-(` shapes Prettier keeps glued)
- Pin: `tests/fixtures/format/css/token-soup-math-glue.css`
- Conformance: `css/parens/parens.css` (`prop`/`prop44`; the file also has unclassified diffs, see AGENTS.md)
- Conformance:
- `externals/prettier/css/parens/parens.css` (`prop`/`prop44`; the file also has unclassified diffs)
```css
/* input */
@@ -957,9 +993,15 @@ Css mode only (hence the prefix): in Less and Scss, Prettier keeps `3px -(4px)`
- Why: cost
- Pin: `tests/fixtures/format/scss/fill-break-position.scss`
- Oxfmt: `externals/webawesome/number-input/number-input.styles.ts`, `externals/webawesome/page/page.styles.ts`,
`externals/ng-zorro-antd/components/style/themes/default.less`, `externals/ng-zorro-antd/components/style/themes/variable.less`, `externals/ng-zorro-antd/components/table/style/rtl.less`,
`externals/gitlab/stylesheets/components/content_editor.scss`, `externals/gitlab/stylesheets/page_bundles/_ide_theme_overrides.scss`, `externals/gitlab/stylesheets/framework/sidebar.scss`
- Conformance:
- `externals/webawesome/number-input/number-input.styles.ts`
- `externals/webawesome/page/page.styles.ts`
- `externals/ng-zorro-antd/components/style/themes/default.less`
- `externals/ng-zorro-antd/components/style/themes/variable.less`
- `externals/ng-zorro-antd/components/table/style/rtl.less`
- `externals/gitlab/stylesheets/components/content_editor.scss`
- `externals/gitlab/stylesheets/page_bundles/_ide_theme_overrides.scss`
- `externals/gitlab/stylesheets/framework/sidebar.scss`
```scss
/* input (nested three levels, print width 80) */
@@ -1009,7 +1051,16 @@ we never format what the reference compiler rejects (a parse error is the SAFE f
- Why: uniform-rule (same construct, same output: the same expression in a declaration value; prettier/prettier#1811)
- Pin: `tests/fixtures/format/scss/media-query-operator-spacing.scss`
- Oxfmt: `externals/gitlab/stylesheets/framework/diffs.scss`, `externals/gitlab/stylesheets/page_bundles/editor.scss`, `externals/gitlab/stylesheets/page_bundles/issuable_list.scss`, `externals/gitlab/stylesheets/page_bundles/labels.scss`, `externals/gitlab/stylesheets/page_bundles/environments.scss`, `externals/gitlab/stylesheets/page_bundles/merge_requests.scss`, `externals/gitlab/stylesheets/page_bundles/settings.scss`, `externals/gitlab/stylesheets/page_bundles/projects.scss`, `externals/gitlab/stylesheets/pages/settings.scss`
- Conformance:
- `externals/gitlab/stylesheets/framework/diffs.scss`
- `externals/gitlab/stylesheets/page_bundles/editor.scss`
- `externals/gitlab/stylesheets/page_bundles/issuable_list.scss`
- `externals/gitlab/stylesheets/page_bundles/labels.scss`
- `externals/gitlab/stylesheets/page_bundles/environments.scss`
- `externals/gitlab/stylesheets/page_bundles/merge_requests.scss`
- `externals/gitlab/stylesheets/page_bundles/settings.scss`
- `externals/gitlab/stylesheets/page_bundles/projects.scss`
- `externals/gitlab/stylesheets/pages/settings.scss`
```scss
/* input */
@@ -1118,7 +1169,7 @@ Prettier drops it when the item's value is paren-ish (a call / paren group), an
- Why: semantics
- Pin: `tests/fixtures/format/css/paren-group-glued-word.css`
(`css/postcss-plugins/postcss-simple-vars.css` carries the case too, but its root declarations keep it skipped in conformance)
(`css/postcss-plugins/postcss-simple-vars.css` carries the case too, but its root declarations fail to parse)
```css
/* input */
@@ -1173,7 +1224,10 @@ Escaped custom property names therefore preserve their source spelling, like nam
- Why: uniform-rule (raw is verbatim; AGENTS.md "Printing raw vs typed")
- Pin: `tests/fixtures/format/css/custom-property-raw-verbatim.css`, `tests/fixtures/format/scss/custom-property-raw-verbatim.scss`, `tests/fixtures/format/scss/custom-property-text.scss`
- Conformance: `css/postcss-8-improment/test.css`, `scss/variables/postcss-8-improment.scss`, `less/postcss-8-improment/test.less`
- Conformance:
- `externals/prettier/css/postcss-8-improment/test.css`
- `externals/prettier/scss/variables/postcss-8-improment.scss`
- `externals/prettier/less/postcss-8-improment/test.less`
```css
/* input */
@@ -1214,7 +1268,8 @@ re-spacing a value we could not read is not a layout rule of ours.
- Why: semantics
- Pin: `tests/fixtures/format/css/postcss-simple-vars/vars.css`
- Conformance: `css/parens/empty-lines.css`
- Conformance:
- `externals/prettier/css/parens/empty-lines.css`
```css
/* input and ours */
@@ -1265,7 +1320,8 @@ and prints any other run verbatim, so a comment inside freezes the keyword's cas
- Why: semantics
- Pin: `tests/fixtures/format/less/javascript-verbatim.less`
- Conformance: `less/less-test-suite/javascript/javascript.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/javascript/javascript.less`
```less
/* input */
@@ -1296,7 +1352,9 @@ it pads the backticks, re-quotes JavaScript strings and reflows a multi-line fun
- Why: invariant
- Pin: `tests/fixtures/format/less/escaped-string-gap.less`
- Conformance: `less/less-test-suite/variables/variables.less`, `less/less-test-suite/strings/strings.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/variables/variables.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/strings/strings.less`
```less
/* input */
@@ -1316,7 +1374,10 @@ Prettier's `~` handling for escaped strings (`~"..."`) fires on a string whose c
- Why: semantics
- Pin: `tests/fixtures/format/less/lookup-glue.less`
- Conformance: `less/less-test-suite/namespacing/namespacing-functions.less`, `less/less-test-suite/namespacing/namespacing-media.less`, `less/less-test-suite/namespace-targeted/namespace-targeted.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-config/namespacing/namespacing-functions.less`
- `externals/prettier/less/less-test-suite/snippet: tests-config/namespacing/namespacing-media.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/namespace-targeted/namespace-targeted.less`
```less
/* input */
@@ -1348,8 +1409,10 @@ The `$@` half is fixed upstream after v3.9.6 (prettier/prettier#19782 keeps `[$@
- Why: uniform-rule (same construct, same output: guard alternatives are a list, not selectors; the `less-extend-statement-break` leak again)
- Pin: `tests/fixtures/format/less/mixin-selector-list.less`
- Conformance: `less/less-test-suite/mixins-guards/mixins-guards.less`, `less/less-test-suite/mixins-guards-default-func/mixins-guards-default-func.less`
- Oxfmt: `externals/ng-zorro-antd/components/style/mixins/customize.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/mixins-guards/mixins-guards.less`
- `externals/prettier/less/less-test-suite/snippet: tests-unit/mixins-guards-default-func/mixins-guards-default-func.less`
- `externals/ng-zorro-antd/components/style/mixins/customize.less`
```less
/* input */
@@ -1376,7 +1439,8 @@ over the width every word gap is a descendant combinator, so `when`, `and` and e
- Why: uniform-rule (same construct, same output: the width-wrapped continuation of the same operation)
- Pin: `tests/fixtures/format/less/line-comment-continuation-indent.less`, `tests/fixtures/format/scss/line-comment-continuation-indent.scss`
- Conformance: `less/less-test-suite/comments/comments2.less`
- Conformance:
- `externals/prettier/less/less-test-suite/snippet: tests-unit/comments/comments2.less`
```less
/* input */
@@ -1521,7 +1585,8 @@ A raw prelude (unknown at-rule params) still takes the same `//` test as Prettie
- Why: uniform-rule (same construct, same output: the SCSS `if()` branch separator; prettier/prettier#19384)
- Pin: `tests/fixtures/format/css/value-semicolon-glue.css`
- Oxfmt: `externals/mdn-css-guides/custom_functions_and_mixins/using_custom_functions/index.md`
- Conformance:
- `externals/mdn-css-guides/custom_functions_and_mixins/using_custom_functions/index.md`
```css
/* input */
@@ -1583,7 +1648,9 @@ Prettier indents the arguments and `)` one level further there, its prelude valu
- Why: invariant
- Pin: `tests/fixtures/format/scss/grid-value-broken-in-function.scss`
- Oxfmt: `externals/gitlab/stylesheets/page_bundles/work_items.scss`, `externals/webawesome/page/page.styles.ts`
- Conformance:
- `externals/gitlab/stylesheets/page_bundles/work_items.scss`
- `externals/webawesome/page/page.styles.ts`
```scss
/* input (= ours; Prettier's output for the value on one line) */
+2 -1
View File
@@ -3,7 +3,8 @@
//! (a divergence needs its own pin; the big conformance file is only a regression net).
//!
//! Expected outputs were verified against `prettier` by hand;
//! when adding a fixture, do the same (`npx prettier@<oxfmt-bundle-version> --parser <variant>`).
//! when adding a fixture, do the same with the oxfmt-bundled one
//! (`node apps/oxfmt/node_modules/prettier/bin/prettier.cjs --parser <variant>`, see `FORMATTER_POLICY.md` "Verification").
use std::path::Path;
-6
View File
@@ -42,10 +42,4 @@ DUMP_IR=1 cargo run -p oxc_formatter_htmlish --example htmlish_formatter [filena
node apps/oxfmt/node_modules/prettier/bin/prettier.cjs [filename]
```
### Prettier conformance
Every failing file is accounted for by a DIVERGENCES.md entry's `Conformance:` line, except this unclassified one:
- `misc/filename-matters/html-doctype/xhtml-doctype.xhtml` (run as `.html`, also as `.xhtml`) fails to parse: `missing end tag of raw text element` for its self-closing `<script />`
Fixtures and the example read `.vue` as Vue, `.xhtml` / `.xht` as XHTML and `*.component.html` as Angular.
+15 -7
View File
@@ -29,8 +29,9 @@ which changes what `pre` renders and what `textarea` submits.
- Why: uniform-rule (same construct, same output: the doctype of a `.html` file, which Prettier lowercases)
- Pin: `tests/fixtures/vue/doctype-case.vue`
- Conformance: `misc/filename-matters/html-doctype/snippet: #0`
- Oxfmt: `externals/mdn-learn/css_layout/multiple-column_layout/index.md`, `externals/mdn-learn/css_layout/positioning/index.md`
- Conformance:
- `externals/mdn-learn/css_layout/multiple-column_layout/index.md`
- `externals/mdn-learn/css_layout/positioning/index.md`
```vue
<!-- input -->
@@ -213,7 +214,8 @@ Prettier prints `class` and `srcset` in its embed step, which `off` skips togeth
- Why: semantics (whitespace added on both sides of a `script` renders between its inline neighbors)
- Pin: `tests/fixtures/html/script-display-none.html`
- Conformance: `html/tags/tags.html`
- Conformance:
- `externals/prettier/html/tags/tags.html`
```html
<!-- input -->
@@ -331,7 +333,8 @@ Prettier lays the content out as blocks, adding and removing whitespace that ren
- Why: semantics (whitespace added on both sides of a `dialog` renders between its inline neighbors)
- Pin: `tests/fixtures/html/dialog-out-of-flow.html`
- Conformance: `html/tags/tags2.html`
- Conformance:
- `externals/prettier/html/tags/tags2.html`
```html
<!-- input -->
@@ -489,7 +492,9 @@ An email template pads its preview text this way, NBSP alternating with zero-wid
- Why: semantics (an end tag added where a fragment ends changes the page it is joined into; prettier/prettier#17761, prettier/prettier#5665)
- Pin: `tests/fixtures/html/omitted-end-tag-at-eof.html`
- Conformance: `html/basics/broken-html.html`, `html/prettier_ignore/unclosed2.html`
- Conformance:
- `externals/prettier/html/basics/broken-html.html`
- `externals/prettier/html/prettier_ignore/unclosed2.html`
```html
<!-- input (header.html, closed by footer.html) -->
@@ -650,7 +655,8 @@ Fixed in Prettier `main` by prettier/prettier#19709 and prettier/prettier#19707,
- Why: uniform-rule (same construct, same output: `<html:p>` and every other prefixed name, which Prettier lays out as an unknown element)
- Pin: `tests/fixtures/html/prefixed-script-style.html`
- Conformance: `html/basics/with-colon.html`
- Conformance:
- `externals/prettier/html/basics/with-colon.html`
```html
<!-- input -->
@@ -680,7 +686,9 @@ Angular's compiler reads a prefix as a namespace (`svg:rect`), and so does the A
- Why: semantics (Prettier ends an unquoted value at `/`: `src=a.png/` becomes `src="a.png"`, another URL)
- Pin: `tests/fixtures/html/unquoted-value-with-slash.html`
- Oxfmt: `externals/mdn-learn/structuring_content/basic_html_syntax/index.md`, `externals/vite/html/valid.html`
- Conformance:
- `externals/mdn-learn/structuring_content/basic_html_syntax/index.md`
- `externals/vite/html/valid.html`
```html
<!-- input -->
+1 -5
View File
@@ -44,7 +44,7 @@ A fenced code block with a language dispatches its content as a `VirtualDocument
the child IR is wrapped in `mark_as_root` with every newline in its texts made a literal line, so continuation lines keep the fence's column,
and the fence outnumbers the backtick runs of the printed content (printed once to count them when a text holds a backtick).
No dispatcher, an unknown language or a failed parse keeps the block verbatim, as does a meta with line ranges (`apps/oxfmt/DIVERGENCES.md#line-ranged-code-block`).
Embedding is verified end to end by oxfmt's conformance (`conformance:prettier`, and `md-in-js` in `conformance:live`), not here.
Embedding is verified end to end by oxfmt's conformance (`conformance:prettier`, and `xxx-in-md` / `markdown-mdn` in `conformance:live`), not here.
## Dialects
@@ -78,10 +78,6 @@ node apps/oxfmt/node_modules/prettier/bin/prettier.cjs --parser markdown --no-co
node -e 'const p=require("./apps/oxfmt/node_modules/prettier");p.__debug.printToDoc(require("fs").readFileSync(process.argv[1],"utf8"),{parser:"markdown"}).then(d=>p.__debug.formatDoc(d)).then(console.log)' [filename]
```
### Prettier conformance
Every failing file is accounted for by a DIVERGENCES.md entry's `Conformance:` line; there are no unclassified failures.
### Fixture fingerprint
`tests/fixtures/fingerprint.rs` serializes the AST's meaning (structure, decoded text, destinations, labels) and ignores what formatting may change
+22 -8
View File
@@ -6,7 +6,14 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
- Why: semantics (prettier/prettier#19839)
- Pin: `tests/fixtures/markdown/leading-thematic-break.md`
- Conformance: `markdown/thematicBreak/simple.md`, `markdown/commonmark-test-suite/snippet: example-11.md`, `example-50.md` to `example-54.md`
- Conformance:
- `externals/prettier/markdown/thematicBreak/simple.md`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-11.md*`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-50.md*`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-51.md*`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-52.md*`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-53.md*`
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-54.md*`
```markdown
<!-- input -->
@@ -40,7 +47,8 @@ Prettier `main` prints `***` since #19839; the pin (3.9.10) still prints `---`.
- Why: semantics (prettier/prettier#19482, prettier/prettier#19849, prettier/prettier#19891)
- Pin: `tests/fixtures/markdown/url-escaping.md`
- Conformance: `markdown/link/encodedLink.md`
- Conformance:
- `externals/prettier/markdown/link/encodedLink.md`
```markdown
<!-- input -->
@@ -62,7 +70,8 @@ Titles get the same `\&` treatment.
- Why: uniform-rule (same construct, same output: space-indented code block)
- Pin: `tests/fixtures/markdown/indented-code-tab.md`
- Conformance: `markdown/commonmark-test-suite/snippet: example-2.md`
- Conformance:
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-2.md*`
````markdown
<!-- input: two spaces, a tab -->
@@ -85,7 +94,8 @@ so an indent written as spaces plus a tab becomes a fenced block.
- Why: uniform-rule (same construct, same output: a blockquote's trailing blank `>` line under `proseWrap: preserve`)
- Pin: `tests/fixtures/markdown/prose-wrap/ignored-block-trailing-quote-line.md`
- Conformance: `markdown/blockquote/ignore-code.md`
- Conformance:
- `externals/prettier/markdown/blockquote/ignore-code.md`
```markdown
<!-- input -->
@@ -289,7 +299,8 @@ Prettier alternates from the marker it would have printed (`-`), prints `*` next
- Why: semantics
- Pin: `tests/fixtures/markdown/prose-wrap/container-directive.md`
- Conformance: `markdown/paragraph/cjk.md`
- Conformance:
- `externals/prettier/markdown/paragraph/cjk.md`
```markdown
<!-- input -->
@@ -323,7 +334,8 @@ so the closing fence is printed explicitly: a reader without directives (CommonM
- Why: semantics
- Pin: `tests/fixtures/markdown/prose-wrap/line-shapes.md`
- Conformance: `markdown/blockquote/notext-end.md`
- Conformance:
- `externals/prettier/markdown/blockquote/notext-end.md`
```markdown
<!-- input -->
@@ -403,7 +415,8 @@ which its regex takes for a list marker (a marker has at most 9 digits).
- Why: semantics (prettier/prettier#6035, prettier/prettier#17303, prettier/prettier#17353)
- Pin: `tests/fixtures/markdown/prose-wrap/stray-delimiters.md`
- Conformance: `markdown/commonmark-test-suite/snippet: example-412.md`
- Conformance:
- `externals/prettier/markdown/commonmark-test-suite/snippet: example-412.md*`
```markdown
<!-- input -->
@@ -518,7 +531,8 @@ Prettier prints it as written (the leading blank line that kept it out of front
- Why: semantics (prettier/prettier#20143)
- Pin: `tests/fixtures/markdown/prose-wrap/cj-line-break.md`
- Conformance: `markdown/splitCjkText/symbolSpaceNewLine.md`
- Conformance:
- `externals/prettier/markdown/splitCjkText/symbolSpaceNewLine.md`
```markdown
<!-- input, proseWrap always -->
+45 -11
View File
@@ -6,7 +6,13 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
- Why: uniform-rule (source order of user tokens is preserved; prettier/prettier#19524, prettier/prettier#19599)
- Pin: `tests/fixtures/yaml/anchor-tag-order.yaml`
- Conformance: `yaml/spec/anchors-and-tags.yml`, `yaml/spec/node-anchor-and-tag-on-seperate-lines.yml`, `yaml/spec/various-combinations-of-tags-and-anchors.yml` and yaml-test-suite `9KAX`, `BU8L`, `F2C7`
- Conformance:
- `externals/prettier/yaml/spec/anchors-and-tags.yml`
- `externals/prettier/yaml/spec/node-anchor-and-tag-on-seperate-lines.yml`
- `externals/prettier/yaml/spec/various-combinations-of-tags-and-anchors.yml`
- `externals/prettier/yaml/yaml-test-suite/snippet: 9KAX.yaml*`
- `externals/prettier/yaml/yaml-test-suite/snippet: BU8L.yaml*`
- `externals/prettier/yaml/yaml-test-suite/snippet: F2C7.yaml*`
```yaml
# input
@@ -25,8 +31,19 @@ Anchor/tag source order is preserved, never reordered, as mapping keys and seque
- Why: semantics (prettier/prettier#19764)
- Pin: `tests/fixtures/yaml/block-scalar-trailing-spaces.yaml`, `tests/fixtures/yaml/prose-wrap/trailing-spaces.yaml`
- Conformance: `yaml/block-folded/block-folded-strip.yml`, the `yaml/block-value` snippets whose scalar ends in a space/tab-only line (64 rows),
and yaml-test-suite `L24T`, `L24T-2`, `Y79Y-2`, whose JSON keeps that line as content
- Conformance:
- `externals/prettier/yaml/block-folded/block-folded-strip.yml`
- `externals/prettier/yaml/block-value/snippet: "foo: >\n x\n \n*`
- `externals/prettier/yaml/block-value/snippet: "foo: >\n x\n \t\n*`
- `externals/prettier/yaml/block-value/snippet: "foo: >-\n x\n \n*`
- `externals/prettier/yaml/block-value/snippet: "foo: >-\n x\n \t\n*`
- `externals/prettier/yaml/block-value/snippet: "foo: |\n x\n \n*`
- `externals/prettier/yaml/block-value/snippet: "foo: |\n x\n \t\n*`
- `externals/prettier/yaml/block-value/snippet: "foo: |-\n x\n \n*`
- `externals/prettier/yaml/block-value/snippet: "foo: |-\n x\n \t\n*`
- `externals/prettier/yaml/yaml-test-suite/snippet: L24T.yaml*` (its JSON keeps that line as content)
- `externals/prettier/yaml/yaml-test-suite/snippet: L24T-2.yaml*` (its JSON keeps that line as content)
- `externals/prettier/yaml/yaml-test-suite/snippet: Y79Y-2.yaml*` (its JSON keeps that line as content)
```yaml
# input ("␣" marks a real space)
@@ -45,7 +62,10 @@ When converging, keep the blank line after such a scalar: post-prettier/prettier
- Why: uniform-rule (one final newline)
- Pin: `tests/fixtures/yaml/eof-blank-lines.yaml`
- Conformance: `yaml/block-value` `"foo: >+\n"` / `"foo: |+\n"` and yaml-test-suite `2G84-4`, where Prettier prints NO final newline after a contentless keep-chomped scalar
- Conformance:
- `externals/prettier/yaml/block-value/snippet: "foo: >+\n"`
- `externals/prettier/yaml/block-value/snippet: "foo: |+\n"`
- `externals/prettier/yaml/yaml-test-suite/snippet: 2G84-4.yaml*`
```yaml
# input
@@ -59,12 +79,14 @@ a: 1
```
Like every other formatter crate, the file always ends with exactly one newline (`|+` keep-chomped verbatim tails excepted); Prettier YAML alone preserves EOF blank lines.
After a contentless keep-chomped scalar (its conformance failures), Prettier prints no final newline at all.
## keep-chomped-space-only-eof-line
- Why: semantics (prettier/prettier#19256)
- Pin: `tests/fixtures/yaml/keep-chomped-eos-spaces-only.yaml`, `tests/fixtures/yaml/keep-chomped-eos-trailing-spaces.yaml`
- Conformance: yaml-test-suite `JEF9-3`, for which the suite asserts no JSON value, only yaml@2's dump
- Conformance:
- `externals/prettier/yaml/yaml-test-suite/snippet: JEF9-3.yaml*` (the suite asserts no JSON value, only yaml@2's dump)
```yaml
# input (no final newline; the last line is two spaces)
@@ -81,7 +103,8 @@ A space-only EOF line at-or-below the block's indent holds no line break, so it
- Why: uniform-rule (blank lines are never invented; see "blank-lines")
- Pin: `tests/fixtures/yaml/empty-document-end-marker.yaml`
- Conformance: yaml-test-suite `HWV9`
- Conformance:
- `externals/prettier/yaml/yaml-test-suite/snippet: HWV9.yaml*`
```yaml
# input
@@ -146,7 +169,8 @@ A suppression comment freezes exactly ONE node, never everything after it; the s
- Why: uniform-rule (blank-line preservation; prettier/prettier#15528)
- Pin: `tests/fixtures/yaml/blank-lines.yaml`, `tests/fixtures/yaml/nested-end-comment-blank.yaml`
- Conformance: `yaml/mapping/duplicated-keys/flow-mapping.yml` (a blank after a flow mapping, before `---`)
- Conformance:
- `externals/prettier/yaml/mapping/duplicated-keys/flow-mapping.yml` (a blank after a flow mapping, before `---`)
```yaml
# input
@@ -195,8 +219,15 @@ More-indented lines in a folded scalar are never re-flowed under `proseWrap: alw
- Why: uniform-rule (a group is flat or fully expanded)
- Pin: `tests/fixtures/yaml/flow-multiline-pair.yaml`, `tests/fixtures/yaml/flow-comments.yaml`
- Conformance: `yaml/spec/spec-example-7-20-single-pair-explicit-entry.yml`, `yaml/spec/spec-example-9-4-explicit-documents.yml` and yaml-test-suite `CT4Q`, `UT92`, `NJ66`, `9SA2` (multiline pairs),
`yaml/flow-mapping/comments/key.yml` and yaml-test-suite `K3WX` (key trailing comment)
- Conformance:
- `externals/prettier/yaml/spec/spec-example-7-20-single-pair-explicit-entry.yml`
- `externals/prettier/yaml/spec/spec-example-9-4-explicit-documents.yml`
- `externals/prettier/yaml/yaml-test-suite/snippet: CT4Q.yaml*` (multiline pairs)
- `externals/prettier/yaml/yaml-test-suite/snippet: UT92.yaml*` (multiline pairs)
- `externals/prettier/yaml/yaml-test-suite/snippet: NJ66.yaml*` (multiline pairs)
- `externals/prettier/yaml/yaml-test-suite/snippet: 9SA2.yaml*` (multiline pairs)
- `externals/prettier/yaml/flow-mapping/comments/key.yml` (key trailing comment)
- `externals/prettier/yaml/yaml-test-suite/snippet: K3WX.yaml*` (key trailing comment)
```yaml
# input
@@ -224,7 +255,9 @@ Prettier sometimes emits a newline inside flow brackets while keeping them flat
- Why: invariant
- Pin: `tests/fixtures/yaml/flow-comments.yaml`
- Conformance: `yaml/spec/spec-example-6-1-indentation-spaces.yml` and yaml-test-suite `6HB6`, the same example
- Conformance:
- `externals/prettier/yaml/spec/spec-example-6-1-indentation-spaces.yml`
- `externals/prettier/yaml/yaml-test-suite/snippet: 6HB6.yaml*` (the same example)
```yaml
# input
@@ -250,7 +283,8 @@ A comment stays at its syntactic position; Prettier's attachment hoists a commen
- Why: uniform-rule (comment presence never changes layout)
- Pin: `tests/fixtures/yaml/comment-over-indented.yaml`
- Oxfmt: `externals/aws-cloudformation-templates/RainModules/load-balancer.yml`
- Conformance:
- `externals/aws-cloudformation-templates/RainModules/load-balancer.yml`
```yaml
# input