queryShape
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
- tcmts.core.program.CompilerOptions
- tcmts.core.program.createProgram
- tcmts.core.shape.QueryImport
- tcmts.core.shape.TypeShape
- tcmts.core.shape.cacheRefOf
- tcmts.core.shape.shapeBuilder
- tcmts.core.ts.forEachChild
- tcmts.core.ts.parse