shapeBuilder
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
- tcmts.core.shape.MAX_DEPTH
- tcmts.core.shape.MAX_PROPS
- tcmts.core.shape.MAX_SHAPE
- tcmts.core.shape.Shape
- tcmts.core.shape.TypeShape
- tcmts.core.shape.key
- tcmts.core.ts.TypeChecker