testFindByType

tcmts.cli.test.e2e.find.testFindByType · #tmz2spxe · term

Find by type: up to renaming, reordered, awaited, more general or specific; codebase types by hash.

import { okIn, tcmIn, 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 testFindByType = () => {
  const dir = tempDir("find");
  const tcm = (...args: string[]) => tcmIn(dir, ...args);
  const ok = (...args: string[]) => okIn(dir, ...args);
  /** The names `find` lists, in order. */
  const found = (query: string) => [...ok("find", query).matchAll(/^(\S+)  #/gm)].map((m) => m[1]);
  ok("init");
  fs.writeFileSync(
    path.join(dir, "scratch.ts"),
    `// tcm: namespace lib
export type User = { name: string; age: number };
export const head = <T>(xs: T[]): T | undefined => xs[0];
export const first = <A>(xs: A[]): A | undefined => xs[0];
export const last = <T>(xs: T[]): T => xs[xs.length - 1]!;
export const sum = (xs: number[]): number => xs.reduce((a, b) => a + b, 0);
export const repeat = (s: string, n: number): string => s.repeat(n);
export const nameOf = (u: User): string => u.name;
export async function loadUser(id: string): Promise<User> {
  return { name: id, age: 0 };
}
`,
  );
  ok("add");

  // the same type up to type-variable names; unknown names in the query are type variables
  assert.deepEqual(found(":(xs: T[]) => T | undefined"), ["lib.first", "lib.head"]);
  assert.match(ok("find", ":(xs: a[]) => a"), /type variables: a/);
  // exact first, then the instance or generalization
  assert.deepEqual(found(":(xs: a[]) => a"), ["lib.last", "lib.sum"]);
  assert.match(ok("find", ":(xs: a[]) => a"), /lib\.sum .*more specific/);
  assert.deepEqual(found(":(xs: number[]) => number"), ["lib.sum", "lib.last"]);
  assert.match(ok("find", ":(xs: number[]) => number"), /lib\.last .*more general/);
  assert.match(ok("find", ":(n: number, s: string) => string"), /lib\.repeat .*arguments reordered/);
  assert.match(ok("find", ":(id: string) => User"), /lib\.loadUser .*async/);
  assert.deepEqual(found(":(u: lib.User) => string"), ["lib.nameOf"]);

  // shapes refer to types by hash: a rename doesn't lose the match
  ok("mv", "lib.User", "lib.Person");
  assert.deepEqual(found(":(p: Person) => string"), ["lib.nameOf"]);

  // a name the query can't pin down says so
  fs.writeFileSync(path.join(dir, "scratch.ts"), `// tcm: namespace other\nexport type Person = { id: string };\n`);
  ok("add");
  const ambiguous = tcm("find", ":(p: Person) => string");
  assert.notEqual(ambiguous.status, 0);
  assert.match(ambiguous.out, /'Person' in the query could be lib\.Person, other\.Person/);
  assert.match(tcm("find", ":(x: ").out, /isn't a TypeScript type/);
}

uses