import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import ts from "typescript"; import { cloneTypeNodeDeepForEmission, getPreservedBindingTypeNode, } from "../../src/ast/type-building.ts"; // A wrapper counts only where its name resolves to the declaration // `commonfabric` exports, so the wrappers are declared in that module and each // module under test imports them. They are generic aliases here as they are in // `commonfabric`. const COMMONFABRIC = `declare module "commonfabric" { export type Writable = { get(): T }; export type Default = T & { readonly __default?: V }; export type PerUser = T & { readonly __scope?: "user" }; } `; const IMPORTS = `import type { Default, PerUser, Writable } from "commonfabric"; `; /** * Compiles `declarations` and returns what `getPreservedBindingTypeNode()` * makes of the type node of `Input`'s property `c`, printed. */ function preserved( declarations: string, files: Record = {}, ): string | undefined { const fileName = "/main.ts"; const sources: Record = { ...files, "/commonfabric.d.ts": COMMONFABRIC, [fileName]: declarations, }; const compilerOptions: ts.CompilerOptions = { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ESNext, strict: true, noLib: true, skipLibCheck: true, }; const host = ts.createCompilerHost(compilerOptions, true); host.getSourceFile = (name) => name in sources ? ts.createSourceFile(name, sources[name]!, compilerOptions.target!, true) : undefined; host.getCurrentDirectory = () => "/"; host.getDirectories = () => []; host.fileExists = (name) => name in sources; host.readFile = (name) => sources[name]; host.writeFile = () => {}; host.useCaseSensitiveFileNames = () => true; host.getCanonicalFileName = (name) => name; host.getNewLine = () => "\n"; const program = ts.createProgram( ["/commonfabric.d.ts", fileName], compilerOptions, host, ); const sourceFile = program.getSourceFile(fileName)!; const input = sourceFile.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === "Input" ) as ts.InterfaceDeclaration; const member = input.members[0] as ts.PropertySignature; const node = getPreservedBindingTypeNode( member.type!, program.getTypeChecker(), ); // Printed as its callers emit it: cloned without source positions, since the // node may come from a module other than the one being printed. return node && ts.createPrinter({ removeComments: true }).printNode( ts.EmitHint.Unspecified, cloneTypeNodeDeepForEmission(node, undefined, undefined), sourceFile, ); } describe("getPreservedBindingTypeNode()", () => { it("returns the declared node for a wrapper written in place", () => { expect( preserved( `${IMPORTS}interface Input { c: Writable>; }`, ), ) .toBe(`Writable>`); }); it("returns `undefined` for a type that carries no wrapper", () => { expect(preserved(`${IMPORTS}interface Input { c: Writable; }`)) .toBeUndefined(); }); it("returns `undefined` for an alias of a type that carries no wrapper", () => { expect(preserved(`${IMPORTS} type Plain = Writable; interface Input { c: Plain; } `)).toBeUndefined(); }); it("returns the type an alias names", () => { expect(preserved(`${IMPORTS} type Draft = Writable>; interface Input { c: Draft; } `)).toBe(`Writable>`); }); it("returns the type at the end of a chain of aliases", () => { expect(preserved(`${IMPORTS} type Draft = Writable>; type Renamed = Draft; interface Input { c: Renamed; } `)).toBe(`Writable>`); }); it("returns a `Writable` of the type its aliased argument names", () => { expect(preserved(`${IMPORTS} type Blank = string | Default<"">; interface Input { c: Writable; } `)).toBe(`Writable>`); }); it("returns a scope wrapper of the cell type its aliased argument names", () => { expect(preserved(`${IMPORTS} type Draft = Writable>; interface Input { c: PerUser; } `)).toBe(`PerUser>>`); }); it("returns a scope wrapper of the type its aliased argument names", () => { expect(preserved(`${IMPORTS} type Blank = string | Default<"">; interface Input { c: PerUser; } `)).toBe(`PerUser>`); }); it("returns one flat union for a member that names a union", () => { expect(preserved(`${IMPORTS} type Blank = string | Default<"">; interface Input { c: Blank | number; } `)).toBe(`string | Default<""> | number`); }); it("leaves a reference to an alias that carries no wrapper as written", () => { expect(preserved(`${IMPORTS} type Status = "idle" | "running"; interface Input { c: Writable>; } `)).toBe(`Writable>`); }); it("returns an intersection of the types its aliased members name", () => { expect(preserved(`${IMPORTS} type Nickname = PerUser; interface Input { c: Nickname & { readonly tag?: "nick" }; } `)).toBe(`PerUser & { readonly tag?: "nick"; }`); }); it("returns a parenthesized type around the type its alias names", () => { expect(preserved(`${IMPORTS} type Draft = Writable>; interface Input { c: (Draft); } `)).toBe(`(Writable>)`); }); it("returns the type an alias imported from another module names", () => { expect(preserved( ` import type { Draft } from "./types.ts"; interface Input { c: Draft; } `, { "/types.ts": `${IMPORTS} export type Draft = Writable>; `, }, )).toBe(`Writable>`); }); it("returns `undefined` for the author's own type that shares a wrapper's name", () => { expect(preserved(` type Default = { mine: T; tag?: V }; interface Input { c: Default; } `)).toBeUndefined(); }); it("returns `undefined` for an alias of the author's own type that shares a wrapper's name", () => { expect(preserved(` type Default = { mine: T; tag?: V }; type Mine = Default; interface Input { c: Mine; } `)).toBeUndefined(); }); it("returns `undefined` for the author's own `Writable` around a wrapper", () => { expect(preserved(` import type { Default } from "commonfabric"; type Writable = { mine: T }; interface Input { c: Writable>; } `)).toBeUndefined(); }); it("leaves a reference to a generic alias as written", () => { expect(preserved(`${IMPORTS} type Defaulted = Writable>; interface Input { c: Defaulted; } `)).toBeUndefined(); }); it("leaves an alias's reference to itself as written", () => { expect(preserved(`${IMPORTS} type Nested = Writable>; interface Input { c: Nested; } `)).toBe(`Writable>`); }); });