/// import { describe, expect, test } from "bun:test"; import { makeSyntaxErasable } from "./erasable-syntax.ts"; describe("makeSyntaxErasable", () => { test("replaces a scalar enum with a const object and a union type", () => { const code = makeSyntaxErasable( ["export enum Key {", " Uninitialized,", " Asset,", "}"].join("\n"), ); expect(code).toBe( [ `const KeyLookup = { 0: "Uninitialized", 1: "Asset", Uninitialized: 0, Asset: 1 } as const;`, "", "export const Key: Omit = KeyLookup;", "", "export type Key = (typeof Key)[keyof typeof Key];", ].join("\n"), ); }); test("keeps the docblock on the exported declaration", () => { const code = makeSyntaxErasable( [ "/**", " * Schema version.", " */", "export enum Version {", " V1,", "}", ].join("\n"), ); expect(code).toBe( [ `const VersionLookup = { 0: "V1", V1: 0 } as const;`, "", "/**", " * Schema version.", " */", "export const Version: Omit = VersionLookup;", "", "export type Version = (typeof Version)[keyof typeof Version];", ].join("\n"), ); }); test("swallows the trailing semicolon of a defined-type enum", () => { const code = makeSyntaxErasable("export enum Key { Asset };"); expect(code).toEndWith("export type Key = (typeof Key)[keyof typeof Key];"); }); test("leaves an unrelated docblock above other code alone", () => { const code = makeSyntaxErasable( [ "/** Unrelated. */", "export const answer = 42;", "", "export enum Key {", " Asset,", "}", ].join("\n"), ); expect(code).toContain("/** Unrelated. */\nexport const answer = 42;"); expect(code).toContain( "export const Key: Omit = KeyLookup;", ); }); test("keeps the enum members reachable in value positions", () => { const code = makeSyntaxErasable( [ "export enum SplTokenInstruction {", " MintTokens,", "}", "", "export function identify(): SplTokenInstruction {", " return SplTokenInstruction.MintTokens;", "}", ].join("\n"), ); expect(code).toContain("return SplTokenInstruction.MintTokens;"); }); test("queries instruction discriminants in type positions with typeof", () => { const code = makeSyntaxErasable( [ "export enum SplTokenInstruction {", " MintTokens,", "}", "", "export type ParsedSplTokenInstruction =", " | ({", " instructionType: SplTokenInstruction.MintTokens;", " } & ParsedMintTokensInstruction);", "", "const parsed = {", " instructionType: SplTokenInstruction.MintTokens,", "};", ].join("\n"), ); expect(code).toContain( "instructionType: typeof SplTokenInstruction.MintTokens;", ); expect(code).toContain("instructionType: SplTokenInstruction.MintTokens,"); }); test("queries single-line union discriminants with typeof", () => { const code = makeSyntaxErasable( [ "export enum SplTokenInstruction { MintTokens };", "", "export type ParsedSplTokenInstruction =", "| { instructionType: SplTokenInstruction.MintTokens } & ParsedMintTokensInstruction", ].join("\n"), ); expect(code).toContain( "| { instructionType: typeof SplTokenInstruction.MintTokens } &", ); }); test("leaves discriminants of enums it did not rewrite alone", () => { const code = makeSyntaxErasable( " instructionType: SomeExternalEnum.Transfer;", ); expect(code).toBe(" instructionType: SomeExternalEnum.Transfer;"); }); test("replaces the angle-bracket plugin assertion with an as assertion", () => { const code = makeSyntaxErasable( [ "return extendClient(client, {", " splToken: {", " pdas: { mint: findMintPda },", " },", "});", ].join("\n"), ); expect(code).toBe( [ "return extendClient(client, {", " splToken: {", " pdas: { mint: findMintPda },", " } as SplTokenPlugin,", "});", ].join("\n"), ); }); test("does not end the plugin object on a brace inside a string", () => { const code = makeSyntaxErasable( ['const x = { name: "}" };'].join("\n"), ); expect(code).toBe('const x = { name: "}" } as SplTokenPlugin;'); }); test("leaves code without non-erasable syntax untouched", () => { const code = "export type Key = 0 | 1;\n"; expect(makeSyntaxErasable(code)).toBe(code); }); test("stays linear on many unterminated enum declarations", () => { // The witness CodeQL reported for js/polynomial-redos: every prefix looks // like a declaration but none closes, so a pattern that scans for the // closing brace restarts that scan at each one. Quadratic behaviour takes // minutes here; linear behaviour takes milliseconds. const code = "export enum A {|".repeat(50_000); const started = performance.now(); expect(makeSyntaxErasable(code)).toBe(code); expect(performance.now() - started).toBeLessThan(1_000); }); });