recast/test/typescript.ts
2024-03-02 12:27:04 +00:00

531 lines
13 KiB
TypeScript

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<number> = list;",
"let matrix: number[][];",
"let x: [string, number];",
"let f: <E>(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<number> = [1, 2, 3];"]);
check(["let n = a!.c();"]);
check(["a!.c();"]);
check(['type A = "cat" | "dog" | "bird";']);
check([
"type A<T, U> = {",
' u: "cat";',
" x: number;",
" y: T;",
" z: U;",
"};",
]);
check([
"type F = <T, U>(",
" a: string,",
" b: {",
" y: T;",
" z: U;",
" }",
") => void;",
]);
check([
"type Readonly<T> = {",
" readonly [P in keyof T]: T[P];",
"};",
"",
"type Pick<T, K extends keyof T> = {",
" [P in K]: T[P];",
"};",
]);
check([
"let strLength1: string = (<string> someValue).length;",
"let strLength2: string = <string> someValue;",
"let square = <Square> {};",
"let strLength3: number = (someValue as string).length;",
"let strLength4: number = someValue as string;",
]);
check([
"(<string> someValue).length;",
"<string> someValue;",
"<Square> {};",
"(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<string, string | RGB> as const;",
]);
check(["let counter = <Counter> function(start: number) {};"]);
check(["<Counter> function(start: number) {};"]);
check(["if ((<F> p).s) {", " (<F> p).s();", "}"]);
check([
"function i(p): p is F {}",
"function i(p): p is string | number {}",
]);
check(["function f<T, U>(a: T, b: U): void {", " let c: number;", "}"]);
check([
"function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {",
" console.log(o);",
"}",
]);
check([
"let myAdd: <T, U>(x: T, y?: number) => U = function(x: number, y?: number): number {};",
"function bb(f: string, ...r: string[]): void {}",
"function f(this: void) {}",
"function l<T extends L>(arg: T): T {}",
"function l<T extends A.B.C>(arg: T): T {}",
"function l<T extends keyof U>(obj: T) {}",
"",
"function create<T>(",
" c: {",
" new<U>(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<T> extends B {",
" f(a: T) {",
" c(a as D);",
" }",
"}",
]);
check([
"interface LabelledContainer<T> {",
" 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<T, U> extends Shape<T, U>, Visible<T, U> {",
" sideLength: number;",
"}",
]);
check([
"class Button extends Control<T, U> implements SelectableControl<T, U>, ClickableControl<U> {",
" 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<K, V>(arg: T = getDefault()): R"]);
check([
"class Animal {",
" public static async *[name]<T>(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<T, U> = 4;',
'const justParameterized: import("package")<T> = 5;',
]);
check(["const a = function<T>(a: T): void {};"]);
check(["new A<number>();"]);
check(["createPlugin<number>();"]);
check(["type Class<T> = new (...args: any) => T;"]);
check(["type T1 = [...Array<any>];", "type T2 = [...any[]];"]);
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<T> {",
" 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<T> {",
" 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<Hammer>;",
"const ErrorMap = Map<string, Error>;",
].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;
}
}