testNoCircularImports

tcmts.cli.test.e2e.cycles.testNoCircularImports · #mk2zwyid · term

No circular imports: definitions that refer to each other are stored together, as one module, so the modules code runs from import each other without ever going round in a loop (no import order to get wrong, nothing half-loaded)

import { okIn, tempDir } from "#codebase/tcmts/cli/test/e2e/harness";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";

export const testNoCircularImports = () => {
  const dir = tempDir("no circular imports");
  const tcm = (...args: string[]) => okIn(dir, ...args);
  tcm("init");
  fs.writeFileSync(
    path.join(dir, "scratch.ts"),
    `// tcm: namespace lib
export const isEven = (n: number): boolean => (n === 0 ? true : isOdd(n - 1));
export const isOdd = (n: number): boolean => (n === 0 ? false : isEven(n - 1));
export const describe = (n: number): string => (isEven(n) ? "even" : "odd") + " " + label(n);
export const label = (n: number): string => (n > 9 ? "big: " + describe(n % 10) : "small");
export const main = (n: string) => console.log(describe(Number(n)));
`,
  );
  tcm("add");
  assert.equal(tcm("run", "lib.main", "17").trim(), "odd big: odd small");
  // the modules it ran from, and what each imports
  const cache = path.join(dir, ".tcm", "cache");
  const modules = new Map<string, string[]>();
  for (const f of fs.readdirSync(cache).filter((f) => /^[a-z2-7]{52}\.tsx?$/.test(f))) {
    const text = fs.readFileSync(path.join(cache, f), "utf8");
    modules.set(
      f.replace(/\.tsx?$/, ""),
      [...text.matchAll(/from "\.\/([a-z2-7]{52})\.tsx?"/g)].map((m) => m[1]!),
    );
  }
  // isEven and isOdd, which call each other, are one module; and so are describe and label
  const holding = (name: string) =>
    [...modules.keys()].filter((h) =>
      new RegExp(`\\b(const|function) ${name}\\b`).test(
        fs.readFileSync(path.join(cache, `${h}.ts`), "utf8"),
      ),
    );
  for (const [a, b] of [
    ["isEven", "isOdd"],
    ["describe", "label"],
  ] as const) {
    assert.equal(holding(a).length, 1, `${a} is in one module`);
    assert.deepEqual(holding(a), holding(b));
  }
  // and no module imports itself, directly or round a loop
  const visiting = new Set<string>();
  const done = new Set<string>();
  const visit = (h: string) => {
    if (done.has(h)) return;
    assert.ok(!visiting.has(h), `an import loop through ${h}`);
    visiting.add(h);
    for (const d of modules.get(h) ?? []) visit(d);
    visiting.delete(h);
    done.add(h);
  };
  for (const h of modules.keys()) visit(h);
  assert.ok(modules.size >= 3, `${modules.size} modules`);
}

uses