shapeBuilder

tcmts.core.shape.shapeBuilder · #viynbet7 · term · runs anywhere

Shapes from checker types. refOf says which symbols are codebase types (by hash).

import { key as key_1, MAX_DEPTH, MAX_PROPS, MAX_SHAPE } from "#codebase/tcmts/core/shape";
import type { Shape, TypeShape } from "#codebase/tcmts/core/shape";
import type { TypeChecker } from "#codebase/tcmts/core/ts";
import {
  ObjectFlags as ts_ObjectFlags, SymbolFlags as ts_SymbolFlags, TypeFlags as ts_TypeFlags,
} from "typescript/sync";
import type {
  NumberLiteralType as ts_NumberLiteralType, ObjectType as ts_ObjectType, Signature as ts_Signature,
  StringLiteralType as ts_StringLiteralType, Symbol as ts_Symbol, Type as ts_Type, TypeReference as ts_TypeReference,
  UnionOrIntersectionType as ts_UnionOrIntersectionType,
} from "typescript/sync";

export function shapeBuilder(checker: TypeChecker, refOf: (sym: ts_Symbol) => string | undefined) {
  const vars = new Map<number, number>();
  const named = (sym: ts_Symbol, args: readonly ts_Type[], depth: number): Shape => {
    const s = sym.flags & ts_SymbolFlags.Alias ? checker.getAliasedSymbol(sym) : sym;
    const hash = refOf(s);
    const a = args.map((t) => of(t, depth + 1));
    return hash ? { k: "ref", hash, args: a } : { k: "named", name: s.name, args: a };
  };
  const sorted = (xs: Shape[]) =>
    xs.sort((a, b) => (key_1(a) < key_1(b) ? -1 : key_1(a) > key_1(b) ? 1 : 0));
  const fn = (sig: ts_Signature, depth: number): Shape => {
    const params: Shape[] = [];
    let rest: Shape | undefined;
    const rests = sig.hasRestParameter;
    const ps = sig.getParameters();
    for (const [i, p] of ps.entries()) {
      const t = checker.getTypeOfSymbol(p);
      if (rests && i === ps.length - 1) {
        rest = checker.isArrayType(t)
          ? of(checker.getTypeArguments(t as ts_TypeReference)[0]!, depth + 1)
          : of(t, depth + 1);
      } else params.push(of(t, depth + 1));
    }
    return {
      k: "fn",
      params,
      ...(rest ? { rest } : {}),
      ret: of(checker.getReturnTypeOfSignature(sig), depth + 1),
    };
  };
  const build = (t: ts_Type, depth: number): Shape => {
    if (depth > MAX_DEPTH) return { k: "opaque" };
    const f = t.flags;
    if (f & ts_TypeFlags.TypeParameter) {
      let i = vars.get(t.id);
      if (i === undefined) vars.set(t.id, (i = vars.size));
      return { k: "var", i };
    }
    if (f & ts_TypeFlags.Any) return { k: "prim", name: "any" };
    if (f & ts_TypeFlags.Unknown) return { k: "prim", name: "unknown" };
    if (f & ts_TypeFlags.Never) return { k: "prim", name: "never" };
    if (f & ts_TypeFlags.Void) return { k: "prim", name: "void" };
    if (f & ts_TypeFlags.Undefined) return { k: "prim", name: "undefined" };
    if (f & ts_TypeFlags.Null) return { k: "prim", name: "null" };
    if (f & ts_TypeFlags.StringLiteral)
      return { k: "lit", value: (t as ts_StringLiteralType).value };
    if (f & ts_TypeFlags.NumberLiteral)
      return { k: "lit", value: (t as ts_NumberLiteralType).value };
    if (f & ts_TypeFlags.BooleanLiteral)
      return {
        k: "lit",
        value: (t as unknown as { intrinsicName: string }).intrinsicName === "true",
      };
    if (f & ts_TypeFlags.String) return { k: "prim", name: "string" };
    if (f & ts_TypeFlags.Number) return { k: "prim", name: "number" };
    if (f & ts_TypeFlags.Boolean) return { k: "prim", name: "boolean" };
    if (f & ts_TypeFlags.BigInt) return { k: "prim", name: "bigint" };
    if (f & ts_TypeFlags.ESSymbolLike) return { k: "prim", name: "symbol" };
    if (f & ts_TypeFlags.NonPrimitive) return { k: "prim", name: "object" };
    // a library alias like Partial<User> or Record<string, number> keeps its name; a codebase alias its hash
    const alias = t.getAliasSymbol();
    if (alias && (refOf(alias) || /\.d\.[cm]?ts$/.test(alias.declarations?.[0]?.path ?? "")))
      return named(alias, t.getAliasTypeArguments(), depth);
    if (f & ts_TypeFlags.Union) {
      // `boolean` inside a union is true | false: put it back together
      const types = (t as ts_UnionOrIntersectionType).getTypes();
      const members = types.filter((m) => !(m.flags & ts_TypeFlags.BooleanLiteral));
      const shapes = members.map((m) => of(m, depth + 1));
      if (members.length < types.length) shapes.push({ k: "prim", name: "boolean" });
      return shapes.length === 1 ? shapes[0]! : { k: "union", of: sorted(shapes) };
    }
    if (f & ts_TypeFlags.Intersection)
      return {
        k: "inter",
        of: sorted((t as ts_UnionOrIntersectionType).getTypes().map((m) => of(m, depth + 1))),
      };
    if (checker.isArrayType(t))
      return { k: "array", of: of(checker.getTypeArguments(t as ts_TypeReference)[0]!, depth + 1) };
    if (checker.isTupleType(t))
      return {
        k: "tuple",
        of: checker.getTypeArguments(t as ts_TypeReference).map((x) => of(x, depth + 1)),
      };
    if (alias) return named(alias, t.getAliasTypeArguments(), depth);
    const obj = t as ts_ObjectType;
    const sym = t.getSymbol();
    if (f & ts_TypeFlags.Object && obj.objectFlags & ts_ObjectFlags.Reference) {
      const target = (t as ts_TypeReference).getTarget().getSymbol();
      if (target) return named(target, checker.getTypeArguments(t as ts_TypeReference), depth);
    }
    if (
      f & ts_TypeFlags.Object &&
      obj.objectFlags & (ts_ObjectFlags.Interface | ts_ObjectFlags.Class) &&
      sym
    )
      return named(sym, [], depth);
    // a class itself (typeof Server) or a module's namespace, not an instance: by name, as `typeof` and the class.
    // Their properties are statics, which can hold the class again (Node's stream.Duplex.Duplex…): described as
    // objects, each level multiplied the last, and one http.Server took megabytes
    if (
      f & ts_TypeFlags.Object &&
      sym &&
      sym.flags & (ts_SymbolFlags.Class | ts_SymbolFlags.ValueModule)
    )
      return { k: "named", name: "typeof", args: [named(sym, [], depth)] };
    if (f & ts_TypeFlags.Object) {
      const props = checker.getPropertiesOfType(t);
      const calls = t.getCallSignatures();
      if (calls.length && !props.length) return fn(calls[0]!, depth);
      if (props.length > MAX_PROPS) return { k: "opaque" };
      return {
        k: "obj",
        props: props
          .map((p): [string, Shape, boolean] => [
            p.name,
            of(checker.getTypeOfSymbol(p), depth + 1),
            !!(p.flags & ts_SymbolFlags.Optional),
          ])
          .sort((a, b) => (a[0] < b[0] ? -1 : 1)),
      };
    }
    return { k: "opaque" };
  };
  // a type is described the same way wherever it's met (at the same depth), and the checker is a process away:
  // describe each once. Not those with type variables, numbered by the signature they're met in.
  const memo = new Map<string, Shape>();
  const of = (t: ts_Type, depth = 0): Shape => {
    const key = `${t.id}:${depth}`;
    const hit = memo.get(key);
    if (hit) return hit;
    let shape = build(t, depth);
    const text = JSON.stringify(shape);
    // (a shape is kept with every version of its definition: one too big to be worth comparing isn't kept whole)
    if (text.length > MAX_SHAPE) shape = { k: "opaque" };
    if (!text.includes('"k":"var"') || shape.k === "opaque") memo.set(key, shape);
    return shape;
  };
  return {
    /** A value's type: its call signatures if it has them, else the type itself. Type variables restart at 0. */
    typeShape(t: ts_Type): TypeShape {
      const calls = t.getCallSignatures();
      return calls.length ? calls.map((s) => (vars.clear(), fn(s, 0))) : [(vars.clear(), of(t))];
    },
  };
}

uses

used by