withTheirCycles

tcmts.cli.main.withTheirCycles · #rg6eel5i · term · runs on the server

uses node:fs

A scratch file analyzed with the rest of the cycles it holds members of: definitions that refer to each other are hashed as one, so the members it doesn't hold are saved with it, as they are, against its new code (none goes on using an old one). They're found from the paths it declares, before it's checked: alone, a cycle's member may not even check (a class whose others take the stored one). So are definitions it imports that use what it changes (an interface of a class it changes, a function taking that): alone, the file would be checked against their old versions, typed with the old code; with it, they're re-derived against the new, and saved with it. Undefined, said why, if it can't be. text is what was analyzed, when that's the file and the definitions added to it rather than the file as it is.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import {
  analyzeScratch, classify, importedPaths, pathsIn, USED_BEFORE_DECLARED, withCycle,
} from "#codebase/tcmts/cli/main";
import type { Row } from "#codebase/tcmts/cli/main";
import { dim, red } from "#codebase/tcmts/cli/style";
import type { Analysis } from "#codebase/tcmts/core/analyze";
import fs from "node:fs";
import path_1 from "node:path";

export function withTheirCycles(
  cb: Codebase,
  file: string,
  into: string | undefined,
): { a: Analysis; rows: Row[]; text?: string } | undefined {
  const text = fs.readFileSync(file, "utf8");
  const here = new Set(pathsIn(file, text, into));
  const missing = new Map<string, string>();
  for (const p of here) {
    for (const old of cb.store.hashesAt(p)) {
      for (const m of cb.store.componentMembers(cb.store.getDef(old)!.componentHash)) {
        const mp = cb.store.pathsOf(m.hash)[0];
        if (m.hash !== old && mp && !here.has(mp)) missing.set(m.hash, mp);
      }
    }
  }
  // what the file imports that leads back to what it holds: in a cycle with it once it's saved (the file uses it,
  // and it uses the file's), or using what it changes, so the file checks against it as it'll be
  const holds = new Set([...[...here].flatMap((p) => cb.store.hashesAt(p)), ...missing.keys()]);
  const usersOf = (from: Iterable<string>) => {
    const out = new Set<string>();
    for (const queue = [...from]; queue.length;) {
      for (const d of cb.store.dependents(queue.pop()!))
        if (!out.has(d) && !holds.has(d)) {
          out.add(d);
          queue.push(d);
        }
    }
    return out;
  };
  let leadsBack = usersOf(holds);
  // (only what uses what the file changes: one it holds as it's stored, as it checks alone, changes nothing for
  // what uses it, which needn't come, all of them, with a type that's only had a comment changed)
  if (importedPaths(file, text).some((p) => cb.store.hashesAt(p).some((h) => leadsBack.has(h)))) {
    cb.refreshShim();
    const alone = cb.analyze(file, cb.scratchSalts(into));
    if (!alone.diagnostics.length && !alone.errors.length) {
      const same = new Set(
        alone.defs
          .filter((d) => cb.store.getDef(d.hash) && cb.store.pathsOf(d.hash).length)
          .map((d) => d.hash),
      );
      leadsBack = usersOf([...holds].filter((h) => !same.has(h)));
    }
  }
  const seen = new Set<string>();
  const imported = new Set<string>();
  for (
    const stack = importedPaths(file, text).flatMap((p) => cb.store.hashesAt(p));
    stack.length;
  ) {
    const h = stack.pop()!;
    if (seen.has(h) || holds.has(h) || !leadsBack.has(h)) continue;
    seen.add(h);
    // (with the rest of its cycle, which is hashed with it: one member can't be written apart from the others)
    for (const m of [
      h,
      ...cb.store.componentMembers(cb.store.getDef(h)!.componentHash).map((m) => m.hash),
    ]) {
      const p = cb.store.pathsOf(m)[0];
      if (p && !here.has(p) && !missing.has(m) && !holds.has(m)) {
        missing.set(m, p);
        imported.add(m);
      }
    }
    for (const d of cb.store.getDef(h)?.deps ?? [])
      if (d.ref.startsWith("#")) stack.push(d.ref.slice(1));
  }
  if (!missing.size) {
    const a = analyzeScratch(cb, file, into);
    return a ? { a, rows: classify(cb, a.defs, into ?? a.namespace ?? "") } : undefined;
  }
  const whole = withCycle(cb, file, text, [...missing.keys()]);
  // (written after the file's code, what its constants' values use is declared after them: the module that runs
  // puts each after what it uses, so that's no problem here; what the file's own lines get wrong is said)
  const b =
    "problem" in whole
      ? undefined
      : analyzeScratch(cb, file, into, whole.text, {
          ignore: USED_BEFORE_DECLARED,
          own: text.split("\n").length,
        });
  if (b === undefined && !("problem" in whole)) return undefined;
  const names = [...missing.values()];
  const listed = (ns: string[]) =>
    `${ns.slice(0, 5).join(", ")}${ns.length > 5 ? `, and ${ns.length - 5} more` : ""}`;
  if (!b) {
    console.log(
      red(
        `${listed(names)}, in a cycle with what ${path_1.basename(file)} holds or used by it, can't be saved with it as ${names.length > 1 ? "they are" : "it is"}${"problem" in whole ? `: ${whole.problem}` : " (above)"}; \`tcm edit\` ${names.length > 1 ? "them" : "it"} too`,
      ),
    );
    return undefined;
  }
  const added = new Set(names);
  const classified: Row[] = classify(cb, b.defs, into ?? b.namespace ?? "");
  // what it imports that's in a cycle with it, and what's only re-derived against it, a dependent (what doesn't use
  // what changed is as it was)
  const theirs = new Set(
    classified.filter((r) => here.has(r.path)).map((r) => r.def.componentHash),
  );
  const importedPath = new Set([...imported].map((h) => missing.get(h)!));
  const rows: Row[] = classified.map((r) =>
    !added.has(r.path)
      ? r
      : importedPath.has(r.path) && !theirs.has(r.def.componentHash)
        ? { ...r, mate: true, dependent: true }
        : { ...r, mate: true },
  );
  const inCycle = rows.filter((r) => r.mate && !r.dependent);
  // (brief, for an agent: the report says how many, in a line)
  if (inCycle.some((r) => r.status !== "identical") && process.env.TCM_BRIEF !== "1")
    console.log(
      dim(
        `with ${listed(inCycle.map((r) => r.path))}: in a cycle with what changed, so saved with it, as ${inCycle.length > 1 ? "they are" : "it is"}`,
      ),
    );
  return { a: b, rows, text: "problem" in whole ? undefined : whole.text };
}

uses

used by