formattedRows

tcmts.cli.main.formattedRows · #3tlooxbh · term · runs on the server

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

used by