withCycle
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
- tcmts.cli.codebase.Codebase
- tcmts.cli.main.blankImports
- tcmts.cli.main.importBindings
- tcmts.cli.main.replacedUses
- tcmts.cli.mirror.declarations
- tcmts.loader.materialize
- tcmts.loader.render