formattedRows
uses node:fs
What's saved is formatted: the scratch file's definitions as oxfmt formats them (the file itself is left as
written). Formatting can't change a hash, and the formatted text is only used if, analyzed again, every
definition hashes the same. Where formatting one would change what it does (oxfmt breaks JSX text around a
{" "}, giving React other children), that one is kept as written, and the rest formatted. analyzed is the
text a is of, when that isn't the file's (the file with its cycles' other members): that's what's formatted
and analyzed again.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { formatText } from "#codebase/tcmts/cli/fmt"; import type { Row } from "#codebase/tcmts/cli/main"; import { loadMirror } from "#codebase/tcmts/cli/mirror"; import { dim, yellow } from "#codebase/tcmts/cli/style"; import type { Analysis } from "#codebase/tcmts/core/analyze"; import fs from "node:fs"; import path from "node:path"; export function formattedRows( cb: Codebase, file: string, into: string | undefined, a: Analysis, rows: Row[], analyzed?: string, ): Row[] { // a mirrored codebase's code reads as its files do: their style is the project's, not the formatter's if (loadMirror(cb)) return rows; const text = analyzed ?? fs.readFileSync(file, "utf8"); let formatted = formatText(cb, text, file); if (formatted === undefined || formatted === text) return rows; const kept = new Set<string>(); for (let round = 0; ; round++) { const b = cb.analyze(file, cb.scratchSalts(into), [], new Map([[file, formatted]])); const comparable = !b.errors.length && !b.diagnostics.length && b.defs.length === a.defs.length; const changed = comparable ? a.defs.flatMap((d, i) => (b.defs[i]!.hash === d.hash ? [] : [i])) : []; if (comparable && !changed.length) { if (kept.size) console.log( dim( ` ${[...kept].join(", ")} saved as written: formatted, ${kept.size > 1 ? "they'd" : "it'd"} do something else (JSX text broken around {" "} gives React other children)`, ), ); const byHash = new Map(b.defs.map((d) => [d.hash, d])); return rows.map((r) => ({ ...r, def: byHash.get(r.def.hash) ?? r.def })); } // the ones formatting changed by themselves get their text as written back, not those that only use one of them // (their hashes follow its); in a cycle where every member changed, all of them const hashes = new Set(changed.map((i) => a.defs[i]!.hash)); const own = changed.filter((i) => !a.defs[i]!.deps.some((d) => hashes.has(d.ref.slice(1)))); const back = own.length ? own : changed; if (!comparable || round >= 3) { const names = changed.length ? changed.map((i) => a.defs[i]!.nameHint) : ["its definitions"]; console.log( yellow( `warning: formatting ${path.basename(file)} would change the hashes of ${names.slice(0, 4).join(", ")}${names.length > 4 ? `, and ${names.length - 4} more` : ""}, so it's saved as written (a tcm bug: please report it)`, ), ); return rows; } for (const i of back.sort((x, y) => b.defs[y]!.range.start - b.defs[x]!.range.start)) { const was = a.defs[i]!.range; const now = b.defs[i]!.range; formatted = formatted.slice(0, now.start) + text.slice(was.start, was.end) + formatted.slice(now.end); kept.add(a.defs[i]!.nameHint); } } }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.fmt.formatText
- tcmts.cli.main.Row
- tcmts.cli.mirror.loadMirror
- tcmts.cli.style.dim
- tcmts.cli.style.yellow
- tcmts.core.analyze.Analysis