cycleWarnings

tcmts.cli.main.cycleWarnings · #47ddgfj2 · term · runs anywhere

What to say of the cycles a save makes or grows: definitions that refer to each other are hashed as one, so a change to any re-hashes and re-checks all of them, and editing one brings the rest along. One that's a cycle only because types name each other (a function taking the class that calls it) says which, since an interface of what's needed breaks it; a big one of code calling itself says so too. Cycles that were there already, as big, aren't mentioned again.

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { BIG_CYCLE } from "#codebase/tcmts/cli/main";
import type { Row } from "#codebase/tcmts/cli/main";
import { tarjan } from "#codebase/tcmts/core/analyze";

export function cycleWarnings(cb: Codebase, rows: Row[]): string[] {
  const out: string[] = [];
  const byComponent = new Map<string, Row[]>();
  for (const r of rows)
    byComponent.set(r.def.componentHash, [...(byComponent.get(r.def.componentHash) ?? []), r]);
  for (const members of byComponent.values()) {
    const size = members[0]!.def.componentSize;
    if (size < 2 || members.length < size) continue;
    // as big as before: nothing new to say
    const before = Math.max(
      0,
      ...members.flatMap((m) =>
        cb.store.hashesAt(m.path).map((h) => cb.store.getDef(h)?.componentSize ?? 0),
      ),
    );
    if (size <= before) continue;
    const index = new Map(members.map((m, i) => [m.def.hash, i]));
    const edges = (pos: "value" | "type") =>
      members.map((m) => [
        ...new Set(
          m.def.deps
            .filter((d) => d.pos === pos)
            .map((d) => index.get(d.ref.slice(1)))
            .filter((i): i is number => i !== undefined),
        ),
      ]);
    const value = edges("value");
    const type = edges("type");
    const parts = tarjan(size, (v) => value[v]!);
    const names = members.map((m) => m.path);
    const listed = `${names.slice(0, 3).join(", ")}${names.length > 3 ? ` and ${names.length - 3} more` : ""}`;
    const head = `${listed} ${before ? "grew into" : "became"} one cycle (${size} definitions): they're hashed as one, so a change to any re-hashes and re-checks them all.`;
    if (parts.length > 1) {
      // the type references that join what calling alone doesn't
      const partOf = new Map<number, number>();
      parts.forEach((p, k) => p.forEach((v) => partOf.set(v, k)));
      const joining = type
        .flatMap((ts, from) =>
          ts
            .filter((to) => partOf.get(to) !== partOf.get(from))
            .map((to) => `${names[from]}'s types name ${names[to]}`),
        )
        .slice(0, 3);
      out.push(
        `${head} It's a cycle only because types name each other (${joining.join("; ")}): typing those with an interface of what they need, not the definition itself, breaks it.`,
      );
    } else if (size >= BIG_CYCLE)
      out.push(`${head} They call each other: splitting them where you can keeps changes small.`);
  }
  return out;
}

uses

used by