import assert from "assert"; import * as types from "ast-types"; import fs from "fs"; import { EOL as eol } from "os"; import path from "path"; import * as recast from "../main"; import * as parser from "../parsers/typescript"; // Babel 7 no longer supports Node 4 or 5. const nodeMajorVersion = parseInt(process.versions.node, 10); (nodeMajorVersion >= 6 ? describe : xdescribe)("TypeScript", function () { it("basic printing", function () { function check(lines: any) { const code = lines.join(eol); const ast = recast.parse(code, { parser }); const output = recast.prettyPrint(ast, { tabWidth: 2 }).code; assert.strictEqual(code, output); } check([ 'let color: string = "blue";', "let isDone: boolean = false;", "let decimal: number = 6;", "let hex: number = 0xf00d;", "let binary: number = 0b1010;", "let octal: number = 0o744;", "let list: number[] = [1, 2, 3];", "let list2: Array = list;", "let matrix: number[][];", "let x: [string, number];", "let f: (e: E) => void;", 'let sn: string | null = "isNull";', ]); check([ "type A = number;", "type B = string;", "type C = never;", "type D = any;", "type E = [string, number];", "type F = void;", "type G = undefined;", "type H = bigint;", "type I = intrinsic;", "", "type J = {", " a: string;", " b?: number;", "};", ]); check(["type c = T & U & V;"]); check(["let list: Array = [1, 2, 3];"]); check(["let n = a!.c();"]); check(["a!.c();"]); check(['type A = "cat" | "dog" | "bird";']); check([ "type A = {", ' u: "cat";', " x: number;", " y: T;", " z: U;", "};", ]); check([ "type F = (", " a: string,", " b: {", " y: T;", " z: U;", " }", ") => void;", ]); check([ "type Readonly = {", " readonly [P in keyof T]: T[P];", "};", "", "type Pick = {", " [P in K]: T[P];", "};", ]); check([ "let strLength1: string = ( someValue).length;", "let strLength2: string = someValue;", "let square = {};", "let strLength3: number = (someValue as string).length;", "let strLength4: number = someValue as string;", ]); check([ "( someValue).length;", " someValue;", " {};", "(someValue as string).length;", "someValue as string;", ]); // TSSatisfiesExpression check([ // Adapted from https://dev.to/ayc0/typescript-49-satisfies-operator-1e4i "const myColor = {", ' value: "red"', "} satisfies Color;", "", "const myIncorrectColor = {", " value: 100", "} satisfies Color;", "", "const invalidPalette = {", " red: [255, 0, 0],", ' green: "#00ff00",', " blue: [1, 2, 3]", "} satisfies Record as const;", ]); check(["let counter = function(start: number) {};"]); check([" function(start: number) {};"]); check(["if (( p).s) {", " ( p).s();", "}"]); check([ "function i(p): p is F {}", "function i(p): p is string | number {}", ]); check(["function f(a: T, b: U): void {", " let c: number;", "}"]); check([ "function pluck(o: T, names: K[]): T[K][] {", " console.log(o);", "}", ]); check([ "let myAdd: (x: T, y?: number) => U = function(x: number, y?: number): number {};", "function bb(f: string, ...r: string[]): void {}", "function f(this: void) {}", "function l(arg: T): T {}", "function l(arg: T): T {}", "function l(obj: T) {}", "", "function create(", " c: {", " new(a: U): T;", " }", "): void {}", ]); check(["const a = b as U as V;"]); check(["b as U as V;"]); check(["const highlight = (n => false) as (a: number) => boolean;"]); check(["(n => false) as (a: number) => boolean;"]); check([ "const highlight = (function(n) {", " return false;", "}) as (a: number) => boolean;", ]); check([ "(function(n) {", " return false;", "}) as (a: number) => boolean;", ]); check([ "enum Color {", " Red,", " Green,", " Blue", "}", "", "enum Color {", " Red = 1,", " Green = 2,", " Blue = 3", "}", "", "enum Color {", " Red = 1,", " Green", "}", "", "enum Color {", ' Red = "RED",', ' Green = "GREEN",', ' Blue = "BLUE"', "}", "", "enum Color {", " Red = init(),", ' Green = "GREEN"', "}", "", "enum E {", " A = 1,", " B,", " C", "}", "", "enum F {", " A = 1 << 1,", " B = C | D.G,", ' E = "1".length', "}", "", "const enum G {", " A = 1", "}", "", "declare enum H {", " A = 1", "}", ]); check([ "class C extends B {", " f(a: T) {", " c(a as D);", " }", "}", ]); check([ "interface LabelledContainer {", " label: string;", " content: T;", " option?: boolean;", " readonly x: number;", " [index: number]: string;", " [propName: string]: any;", " readonly [index: number]: string;", " (source: string, subString: string): boolean;", " (start: number): string;", " reset(): void;", " a(c: (this: void, e: E) => void): void;", "}", ]); check([ "interface Square extends Shape, Visible {", " sideLength: number;", "}", ]); check([ "interface LabelledContainer {", " get label(): string;", " set label(a: string);", "}", ]); check([ "class Button extends Control implements SelectableControl, ClickableControl {", " select() {}", "}", ]); check([ "class Animal {", ' static className: string = "Animal";', "", " constructor(theName: string) {", " this.name = theName;", " }", "", " private name: string;", " public fur: boolean;", " protected sound: string;", " private getName() {}", " public talk() {}", " protected getSound() {}", " static createAnimal() {}", "}", ]); check([ "class Dog extends Animal {", " protected override getSound() {", ' return "bark";', " }", "}", ]); check(["export interface S {", " i(s: string): boolean;", "}"]); check([ "namespace Validation {", " export interface S {", " i(j: string): boolean;", " }", "}", ]); check(["export interface S {", " i(j: string): boolean;", "}"]); check(["declare namespace D3 {", " export const f: number;", "}"]); check(["declare function foo(arg: T = getDefault()): R"]); check([ "class Animal {", " public static async *[name](arg: U): V;", "}", ]); check(["function myFunction(", " {", " param1", " }: Params", ") {}"]); check([ 'const unqualified: import("package") = 1;', 'const qualified: import("package").ns = 2;', 'const veryQualified: import("package").ns.foo.bar = 3;', 'const qualifiedWithParams: import("package").ns.foo = 4;', 'const justParameterized: import("package") = 5;', ]); check(["const a = function(a: T): void {};"]); check(["new A();"]); check(["createPlugin();"]); check(["type Class = new (...args: any) => T;"]); check(["type T1 = [...Array];", "type T2 = [...any[]];"]); check([ "const a = styled.h1<{", " $upsideDown?: boolean;", "}>`", ' ${props => props.$upsideDown && "transform: rotate(180deg);"}', " text-align: center;", "`;", ]); check([ "type Color = [r: number, g: number, b: number, a?: number];", "type Red = Color.r;", "type Green = Color.g;", "type Blue = Color.b;", "type Alpha = Color.a;", ]); check([ "type alias = boolean;", "const value = 0;", "export { type alias, value };", ]); }); it("InterfaceBody: duplicate semicolon", function () { const code = [ "interface Hello {", " 'hello': any;", " 'hello': string;", "}", ].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body.pop(); assert.strictEqual( recast.print(ast).code, ["interface Hello {", " 'hello': any;", "}"].join(eol), ); }); it("InterfaceBody: duplicate semicolon: a lot of properties", function () { const code = [ "interface LabelledContainer {", " label: string;", " content: T;", " option?: boolean;", " readonly x: number;", " [index: number]: string;", " [propName: string]: any;", " readonly [index: number]: string;", " (source: string, subString: string): boolean;", " (start: number): string;", " reset(): void;", " a(c: (this: void, e: E) => void): void;", "}", ].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body.shift(); assert.strictEqual( recast.print(ast).code, [ "interface LabelledContainer {", " content: T;", " option?: boolean;", " readonly x: number;", " [index: number]: string;", " [propName: string]: any;", " readonly [index: number]: string;", " (source: string, subString: string): boolean;", " (start: number): string;", " reset(): void;", " a(c: (this: void, e: E) => void): void;", "}", ].join(eol), ); }); it("TSModuleBlock: removed child", function () { const code = [ "namespace Numbers {", " export const four = 4;", " export const five = 5;", "}", ].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].body.body.shift(); assert.strictEqual( recast.print(ast).code, ["namespace Numbers {", " export const five = 5;", "}"].join(eol), ); }); it("TypeLiteral: unwanted comma", function () { const code = [ "type A = {", " x: boolean;", " y: number;", " z: string;", "}", ].join(eol); const ast = recast.parse(code, { parser }); ast.program.body[0].typeAnnotation.members.pop(); assert.strictEqual( recast.print(ast).code, ["type A = {", " x: boolean;", " y: number;", "}"].join(eol), ); }); it("TSInstantiationExpression: round trip", function () { const code = [ "const makeHammerBox = makeBox;", "const ErrorMap = Map;", ].join(eol); const ast = recast.parse(code, { parser }); assert.strictEqual(recast.prettyPrint(ast).code, code); }); }); testReprinting( "data/babel-parser/test/fixtures/typescript/**/input.js", "Reprinting @babel/parser TypeScript test fixtures", ); testReprinting( "data/graphql-tools-src/**/*.ts", "Reprinting GraphQL-Tools TypeScript files", ); function testReprinting(pattern: any, description: any) { // Babel no longer supports Node 4 or 5. if (nodeMajorVersion < 6) { return; } describe(description, function () { require("glob") .sync(pattern, { cwd: __dirname, }) .filter( (file: string) => !( // Skip the following files, because they have problems that are not due // to any problems in Recast. TODO Revisit this list periodically. ( file.indexOf("/tsx/") >= 0 || file.endsWith("stitching/errors.ts") || file.endsWith("decorators/type-arguments-invalid/input.js") || // Throws an error with a slightly different message than expected: file.endsWith("types/tuple-rest-invalid/input.js") || // @babel/parser can't handle naked arrow function expression statements: file.endsWith("/optional-parameter/input.js") ) ), ) .forEach((file: string) => it(file, function () { const absPath = path.join(__dirname, file); const source = fs.readFileSync(absPath, "utf8"); const ast = tryToParseFile(source, absPath); if (ast === null) { return; } this.timeout(20000); assert.strictEqual(recast.print(ast).code, source); const reprintedCode = recast.prettyPrint(ast).code; const reparsedAST = recast.parse(reprintedCode, { parser }); types.astNodesAreEquivalent(ast, reparsedAST); }), ); }); } function tryToParseFile(source: any, absPath: any) { try { return recast.parse(source, { parser }); } catch (e1: any) { let options; try { options = JSON.parse( fs .readFileSync(path.join(path.dirname(absPath), "options.json")) .toString(), ); } catch (e2: any) { if (e2.code !== "ENOENT") { console.error(e2); } throw e1; } if (options.throws === e1.message) { return null; } throw e1; } }