queryShape

tcmts.core.shape.queryShape · #v5frne3a · term · runs on the server

uses tcmts.core.ts.parse, which uses tcmts.core.ts.compiler, which uses process

The shape of a type written as a query, e.g. (xs: T[]) => T or (u: lib.User) => Promise<string>. resolve maps names written in the query (User, lib.User) to codebase types; names that are neither those nor globals become type variables, as in Unison ((xs: a[]) => a). The query is checked as a file at file (it must sit where #codebase resolves); it's never written to disk.

import { createProgram } from "#codebase/tcmts/core/program";
import type { CompilerOptions } from "#codebase/tcmts/core/program";
import { cacheRefOf, shapeBuilder } from "#codebase/tcmts/core/shape";
import type { QueryImport, TypeShape } from "#codebase/tcmts/core/shape";
import { forEachChild, parse } from "#codebase/tcmts/core/ts";
import {
  isTypeAliasDeclaration as ts_isTypeAliasDeclaration, isTypeReferenceNode as ts_isTypeReferenceNode,
} from "typescript/ast";
import type { Node as ts_Node, TypeAliasDeclaration as ts_TypeAliasDeclaration } from "typescript/ast";

export function queryShape(
  text: string,
  file: string,
  options: CompilerOptions,
  resolve: (name: string) => QueryImport | undefined,
): { shape: TypeShape; variables: string[] } {
  const parsed = parse("query.ts", `type __Q = ${text};`);
  const alias = parsed.statements[0];
  if (parsed.statements.length !== 1 || !alias || !ts_isTypeAliasDeclaration(alias) || alias.type.end !== parsed.text.length - 1) {
    throw new Error(`'${text}' isn't a TypeScript type`);
  }
  // codebase names become imports under fresh local names, replacing their text in the query
  const imports: string[] = [];
  const edits: { start: number; end: number; local: string }[] = [];
  const visit = (n: ts_Node) => {
    if (ts_isTypeReferenceNode(n)) {
      const name = n.typeName.getText(parsed);
      const imp = resolve(name);
      if (imp) {
        const local = `__T${imports.length}`;
        imports.push(`import type { ${imp.export} as ${local} } from ${JSON.stringify(imp.module)};`);
        edits.push({ start: n.typeName.getStart(parsed), end: n.typeName.end, local });
      }
    }
    forEachChild(n, visit);
  };
  visit(alias.type);
  let body = parsed.text.slice(alias.type.getStart(parsed), alias.type.end);
  const offset = alias.type.getStart(parsed);
  for (const e of edits.sort((a, b) => b.start - a.start)) body = body.slice(0, e.start - offset) + e.local + body.slice(e.end - offset);

  const check = (variables: string[]) => {
    const params = variables.length ? `<${variables.join(", ")}>` : "";
    const { program, checker } = createProgram([file], options, new Map([[file, `${imports.join("\n")}\ntype __Q${params} = ${body};\nexport {};\n`]]));
    const sf = program.getSourceFile(file)!;
    const diags = [...program.getSyntacticDiagnostics(file), ...program.getSemanticDiagnostics(file)];
    return { checker, sf, diags };
  };
  // names TypeScript can't find are the query's type variables
  let r = check([]);
  const unknown = [...new Set(r.diags.filter((d) => d.code === 2304).map((d) => /'([^']+)'/.exec(d.text)?.[1]).filter((n): n is string => !!n))];
  if (unknown.length) r = check(unknown);
  if (r.diags.length) throw new Error(`'${text}': ${r.diags[0]!.text}`);
  const { checker } = r;
  const decl = r.sf.statements.find((s): s is ts_TypeAliasDeclaration => ts_isTypeAliasDeclaration(s) && s.name.text === "__Q")!;
  return { shape: shapeBuilder(checker, cacheRefOf(checker)).typeShape(checker.getTypeFromTypeNode(decl.type)), variables: unknown };
}

uses

used by