withCycle

tcmts.cli.main.withCycle · #ebxrmbom · term · runs on the server

uses node:path

A scratch file's text followed by definitions of a cycle it changes, as stored (written as tcm edit would). Its imports of them go, and so do their imports of what it declares or imports already: blanked, so positions in the file stay where they were. A name the two would bind differently is a problem, and so is a function or class that would have to be written with another name (its name is part of its hash).

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { blankImports, importBindings, replacedUses } from "#codebase/tcmts/cli/main";
import { declarations } from "#codebase/tcmts/cli/mirror";
import { materialize, render } from "#codebase/tcmts/loader";
import path from "node:path";

export function withCycle(
  cb: Codebase,
  file: string,
  text: string,
  members: string[],
): { text: string } | { problem: string } {
  const declared = new Set(declarations(file, text).keys());
  const ours = importBindings(file, text);
  const recs = members.map((h) => cb.store.getDef(h)!);
  const renderMates = (avoid: string[]) =>
    render(recs, cb.source(), cb.lay, {
      style: "scratch",
      exportDeclarations: true,
      remap: (h) => cb.store.patched(h),
      dir: path.dirname(path.resolve(file)),
      annotate: (h) => `// tcm: path ${cb.store.pathsOf(h)[0]}`,
      avoid,
    });
  // a function's or class's name is part of its hash: one that can't keep it here (another declares it, the file
  // included, or uses a global by that name) can't be written here
  const keepNames = (r: ReturnType<typeof renderMates>) => {
    const renamed = recs.find(
      (m) => m.nominal && (r.memberNames.get(m.hash) !== m.nameHint || declared.has(m.nameHint)),
    );
    if (!renamed) return undefined;
    const p = cb.store.pathsOf(renamed.hash)[0] ?? renamed.nameHint;
    return {
      problem: declared.has(renamed.nameHint)
        ? `${p} would be written beside the ${renamed.nameHint} the file declares, which takes its name, and a function's or class's name is part of its hash`
        : `${p} would be written as ${r.memberNames.get(renamed.hash)} with the rest, and a function's or class's name is part of its hash`,
    };
  };
  // a name they'd import from elsewhere than the file does is given another (`Move_1`), rather than clashing
  const { byHash } = replacedUses(cb, recs);
  if (byHash.length) materialize(byHash, cb.source(), cb.lay, { keepExisting: true });
  const first = renderMates([]);
  const kept = keepNames(first);
  if (kept) return kept;
  let rendered = first.text;
  const clashing = [...importBindings(file, rendered)]
    .filter(([local, theirs]) => ours.has(local) && ours.get(local)!.module !== theirs.module)
    .map(([local]) => local);
  if (clashing.length) {
    const again = renderMates(clashing);
    const renamed = keepNames(again);
    if (renamed) return renamed;
    rendered = again.text;
  }
  const drop = new Set<string>();
  // (the same import, perhaps only of its type on one side: the one importing the value is the one kept)
  const dropOurs = new Set<string>();
  for (const [local, theirs] of importBindings(file, rendered)) {
    const mine = ours.get(local);
    if (declared.has(local)) drop.add(local);
    else if (mine && mine.module === theirs.module)
      (mine.type && !theirs.type ? dropOurs : drop).add(local);
    else if (mine)
      return {
        problem: `${local} is imported from ${mine.module} here, and from ${theirs.module} there`,
      };
  }
  return {
    text: `${blankImports(file, text, new Set([...declarations(file, rendered).keys(), ...dropOurs]))}\n${blankImports(file, rendered, drop)}`,
  };
}

uses

used by