mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
32d00c55bc
Preserve parentheses around TypeScript instantiation expressions in postfix contexts. Previously, `(f<T>).x` and `(f<T>)[x]` were emitted as `f<T>.x` and `f<T>[x]`, producing invalid syntax or changing how the expression is parsed. The printer now respects the surrounding precedence while retaining grouping for lower-precedence operands.
602 lines
24 KiB
Rust
602 lines
24 KiB
Rust
use oxc_codegen::CodegenOptions;
|
|
use oxc_parser::ParseOptions;
|
|
use oxc_span::SourceType;
|
|
|
|
use crate::{
|
|
snapshot, snapshot_options, test_ts,
|
|
tester::{
|
|
default_options, test, test_idempotency, test_idempotency_options,
|
|
test_options_with_source_type, test_same, test_tsx, test_with_parse_options,
|
|
},
|
|
};
|
|
|
|
#[test]
|
|
fn abstract_accessor_modifiers() {
|
|
test_ts(
|
|
"abstract class C { public abstract accessor x: number; protected abstract accessor y: string; }",
|
|
"abstract class C {\n\tpublic abstract accessor x: number;\n\tprotected abstract accessor y: string;\n}\n",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn minify_abstract_accessor_modifiers() {
|
|
test_options_with_source_type(
|
|
"abstract class C { public abstract accessor x: number; protected abstract accessor y: string; }",
|
|
"abstract class C{public abstract accessor x:number;protected abstract accessor y:string}",
|
|
SourceType::ts(),
|
|
CodegenOptions::minify(),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn accessor_modifier_combinations() {
|
|
let check = |source: &str, minified: &str| {
|
|
test_ts(source, source);
|
|
test_idempotency(source);
|
|
test_options_with_source_type(source, minified, SourceType::ts(), CodegenOptions::minify());
|
|
test_idempotency_options(source, &CodegenOptions::minify());
|
|
};
|
|
check(
|
|
"abstract class C {\n\tabstract accessor x: number;\n}\n",
|
|
"abstract class C{abstract accessor x:number}",
|
|
);
|
|
check(
|
|
"abstract class C extends B {\n\tpublic abstract override accessor x: number;\n}\n",
|
|
"abstract class C extends B{public abstract override accessor x:number}",
|
|
);
|
|
check(
|
|
"abstract class C extends B {\n\tprotected abstract override accessor x: number;\n}\n",
|
|
"abstract class C extends B{protected abstract override accessor x:number}",
|
|
);
|
|
check("class C {\n\tpublic static accessor x = 1;\n}\n", "class C{public static accessor x=1}");
|
|
check(
|
|
"class C {\n\tprotected static accessor x = 1;\n}\n",
|
|
"class C{protected static accessor x=1}",
|
|
);
|
|
check("class C {\n\tprivate accessor x = 1;\n}\n", "class C{private accessor x=1}");
|
|
check(
|
|
"class C extends B {\n\tpublic override accessor x = 1;\n}\n",
|
|
"class C extends B{public override accessor x=1}",
|
|
);
|
|
check("class C {\n\taccessor #x = 1;\n}\n", "class C{accessor#x=1}");
|
|
check("class C {\n\tpublic accessor x!: number;\n}\n", "class C{public accessor x!:number}");
|
|
}
|
|
|
|
#[test]
|
|
fn cases() {
|
|
test_same("({ foo(): string {} });\n");
|
|
test_same("({ method(this: Foo): void {} });\n");
|
|
test_same("({ methodWithParam(this: Foo, bar: string): void {} });\n");
|
|
test_same("type T = (A | B)[];\n");
|
|
test_same("type T = (A & B)[];\n");
|
|
test_same("type T = (keyof A)[];\n");
|
|
test_same("type T = (() => A)[];\n");
|
|
test_same("type T = (new () => A)[];\n");
|
|
test_same("type T = (A extends B ? C : D)[];\n");
|
|
test_same("type T = (A | B)[K];\n");
|
|
test_same("type T = (A & B)[K];\n");
|
|
test_same("type T = (keyof A)[K];\n");
|
|
test_same("type T = (A extends B ? C : D)[K];\n");
|
|
test_same("type T = A extends (B extends C ? D : E) ? F : G;\n");
|
|
test_same("type T = { [K in U]: V };\n");
|
|
test_same("type T = { [K in U]?: V };\n");
|
|
test_same("type T = { -readonly [K in U]-?: V };\n");
|
|
test_same("type T = (A extends B ? C : D) extends E ? F : G;\n");
|
|
test_same("type T = A & (B extends C ? D : E);\n");
|
|
test_same("type T = (A | B) & C;\n");
|
|
test_same("declare interface A {}\n");
|
|
test_same("interface I<in out T,> {}\n");
|
|
test_same("function F<const T,>() {}\n");
|
|
test_same("class C<const in out T,> {}\n");
|
|
test_same("class C {\n\tp = await(0);\n}\n");
|
|
test_same(
|
|
"class Foo {\n\t#name: string;\n\tf() {\n\t\t#name in other && this.#name === other.#name;\n\t}\n}\n",
|
|
);
|
|
test_same("class B {\n\tconstructor(override readonly a: number) {}\n}\n");
|
|
test_same("class C extends B {\n\toverride show(): void;\n\toverride hide(): void;\n}\n");
|
|
test_same("class D extends B {\n\toverride readonly x: number;\n}\n");
|
|
test_same(
|
|
"declare namespace ns {\n\tclass Foo {}\n\tenum Bar {}\n\ttype Baz = undefined;\n}\n",
|
|
);
|
|
test_same("class E {\n\tsubscribe!: string;\n}\n");
|
|
test_same("class F {\n\taccessor value!: string;\n}\n");
|
|
test_same("class E {\n\tstatic [key: string]: string;\n}\n");
|
|
test_same("export { type as as };\n");
|
|
test_same("import type from = require(\"./a\");\n");
|
|
test_same("try {} catch (e: unknown) {} finally {}\n");
|
|
test_same("const Bar = class<T,> {};\n");
|
|
}
|
|
|
|
#[test]
|
|
fn quoted_type_import_names() {
|
|
test(r#"import type { "foo" as foo } from "m";"#, "import type { foo } from \"m\";\n");
|
|
test(r#"import { type "foo" as foo } from "m";"#, "import { type foo } from \"m\";\n");
|
|
test(r#"import type { "a\u0062" as ab } from "m";"#, "import type { ab } from \"m\";\n");
|
|
test(r#"import { type "a\u0062" as ab } from "m";"#, "import { type ab } from \"m\";\n");
|
|
test(r#"import type { foo as foo } from "m";"#, "import type { foo } from \"m\";\n");
|
|
test(r#"import { type foo as foo } from "m";"#, "import { type foo } from \"m\";\n");
|
|
|
|
let min = |source: &str, expected: &str| {
|
|
test_options_with_source_type(source, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
min(r#"import type { "foo" as foo } from "m";"#, r#"import type{foo}from"m";"#);
|
|
min(r#"import { type "foo" as foo } from "m";"#, r#"import{type foo}from"m";"#);
|
|
min(r#"import type { "a\u0062" as ab } from "m";"#, r#"import type{ab}from"m";"#);
|
|
min(r#"import { type "a\u0062" as ab } from "m";"#, r#"import{type ab}from"m";"#);
|
|
min(r#"import type { foo as foo } from "m";"#, r#"import type{foo}from"m";"#);
|
|
min(r#"import { type foo as foo } from "m";"#, r#"import{type foo}from"m";"#);
|
|
}
|
|
|
|
#[test]
|
|
fn export_default_interface() {
|
|
test_same("export default interface X {}\nconst y = 1;\n");
|
|
}
|
|
|
|
#[test]
|
|
fn decorators() {
|
|
test_same("@a abstract class C {}\n");
|
|
test_tsx("@a @b export default abstract class {}", "export default @a @b abstract class {}\n");
|
|
}
|
|
|
|
#[test]
|
|
fn tsx() {
|
|
test_tsx("<T,>() => {}", "<T,>() => {};\n");
|
|
test_tsx("<T, B>() => {}", "<\n\tT,\n\tB\n>() => {};\n");
|
|
test_tsx("<Foo<T> />", "<Foo<T> />;\n");
|
|
}
|
|
|
|
#[test]
|
|
fn ts() {
|
|
let cases = [
|
|
"let x: string = `\\x01`;",
|
|
"function foo<T extends string>(x: T, y: string, ...restOfParams: Omit<T, 'x'>): T {\n\treturn x;\n}",
|
|
"let x: string[] = ['abc', 'def', 'ghi'];",
|
|
"let x: Array<string> = ['abc', 'def', 'ghi',];",
|
|
"let x: [string, number] = ['abc', 123];",
|
|
"let x: string | number = 'abc';",
|
|
"let x: string & number = 'abc';",
|
|
"let x: typeof String = 'string';",
|
|
"let x: keyof string = 'length';",
|
|
"let x: keyof typeof String = 'length';",
|
|
"let x: string['length'] = 123;",
|
|
"function isString(value: unknown): asserts value is string {
|
|
if (typeof value !== 'string') {
|
|
throw new Error('Not a string');
|
|
}
|
|
}",
|
|
"import type { Foo } from 'foo';",
|
|
"import { Foo, type Bar } from 'foo';",
|
|
"export { Foo, type Bar } from 'foo';",
|
|
"type A<T> = { [K in keyof T as K extends string ? B<K> : K ]: T[K] }",
|
|
"class A {readonly type = 'frame'}",
|
|
"let foo: { <T>(t: T): void }",
|
|
"let foo: { new <T>(t: T): void }",
|
|
"function <const T>(){}",
|
|
"class A {m?(): void}",
|
|
"class A {constructor(public readonly a: number) {}}",
|
|
"abstract class A {protected abstract m()}",
|
|
"class A {private static readonly prop: string}",
|
|
"interface A { a: string, 'b': number, 'c'(): void }",
|
|
"enum A { a, 'b' }",
|
|
"module a {}",
|
|
"declare module 'a'",
|
|
"a = x!;",
|
|
"b = (x as y);",
|
|
"c = foo<string>;",
|
|
"new Map<string, number>();",
|
|
"d = x satisfies y;",
|
|
"export @x declare abstract class C {}",
|
|
"div<T>``",
|
|
"export type Component<Props = any> = Foo;",
|
|
"
|
|
export type Component<
|
|
Props = any,
|
|
RawBindings = any,
|
|
D = any,
|
|
C extends ComputedOptions = ComputedOptions,
|
|
M extends MethodOptions = MethodOptions,
|
|
E extends EmitsOptions | Record<string, any[]> = {},
|
|
S extends Record<string, any> = any,
|
|
> =
|
|
| ConcreteComponent<Props, RawBindings, D, C, M, E, S>
|
|
| ComponentPublicInstanceConstructor<Props>
|
|
",
|
|
"(a || b) as any",
|
|
"(a ** b) as any",
|
|
"(function g() {}) as any",
|
|
r#"
|
|
import defaultExport from "module-name";
|
|
import * as name from "module-name";
|
|
import { export1 } from "module-name";
|
|
import { export1 as alias1 } from "module-name";
|
|
import { default as alias } from "module-name";
|
|
import { export1, export2 } from "module-name";
|
|
import { export1, export2 as alias2, /* … */ } from "module-name";
|
|
import { "string name" as alias } from "module-name";
|
|
import defaultExport, { export1, /* … */ } from "module-name";
|
|
import defaultExport, * as name from "module-name";
|
|
import "module-name";
|
|
import {} from 'mod';
|
|
|
|
export let name1, name2/*, … */; // also var
|
|
export const name3 = 1, name4 = 2/*, … */; // also var, let
|
|
export function functionName() { /* … */ }
|
|
export class ClassName { /* … */ }
|
|
export function* generatorFunctionName() { /* … */ }
|
|
export const { name5, name2: bar } = o;
|
|
export const [ name6, name7 ] = array;
|
|
|
|
export { name8, /* …, */ name81 };
|
|
export { variable1 as name9, variable2 as name10, /* …, */ name82 };
|
|
export { variable1 as "string name" };
|
|
export { name1 as default1 /*, … */ };
|
|
|
|
export * from "module-name";
|
|
export * as name11 from "module-name";
|
|
export { name12, /* …, */ nameN } from "module-name";
|
|
export { import1 as name13, import2 as name14, /* …, */ name15 } from "module-name";
|
|
export { default, /* …, */ } from "module-name";
|
|
export { default as name16 } from "module-name";
|
|
"#,
|
|
r#"
|
|
import a = require("a");
|
|
export import b = require("b");
|
|
"#,
|
|
"class C {
|
|
static
|
|
static
|
|
static
|
|
bar() {}
|
|
}",
|
|
// TSImportType - ensure backticks are not used in minify mode
|
|
"type T = typeof import('react');",
|
|
"type U = typeof import(\"vue\");",
|
|
"type V = typeof import('some-module').SomeType;",
|
|
"type W = typeof import('pkg').default<string>;",
|
|
];
|
|
|
|
snapshot("ts", &cases);
|
|
snapshot_options("minify", &cases, &CodegenOptions::minify());
|
|
}
|
|
|
|
#[test]
|
|
fn minify_export_default() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// A leading `interface`/`abstract`/keyword needs the space; `{`/`<` does not.
|
|
min("export default interface I { x: number }", "export default interface I{x:number;}");
|
|
min("export default abstract class {}", "export default abstract class{}");
|
|
min("export default <const>x;", "export default<const>x;");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_return_type_colon() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// No space after `:` in a function return type / `this` param annotation,
|
|
// matching method/arrow return types.
|
|
min("function f(): Promise<void> {}", "function f():Promise<void>{}");
|
|
min(
|
|
"function g(a: string): boolean { return true; }",
|
|
"function g(a:string):boolean{return true}",
|
|
);
|
|
min("function h(this: Foo): void {}", "function h(this:Foo):void{}");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_ts_type_space() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// Conditional type: `?`/`:` are tight like a JS conditional expression.
|
|
min("type T = A extends B ? C : D;", "type T=A extends B?C:D;");
|
|
min("type T = A extends {} ? B : C;", "type T=A extends{}?B:C;");
|
|
// Constructor type: no space after `new` before `(`/`<`.
|
|
min("type N = new () => Foo;", "type N=new()=>Foo;");
|
|
min("type N = abstract new (x: number) => Foo;", "type N=abstract new(x:number)=>Foo;");
|
|
// A JSDoc-nullable branch must not merge into `??`.
|
|
min("type T = A extends B ? ?C : D;", "type T=A extends B? ?C:D;");
|
|
min("type T = A extends C? ? D : E;", "type T=A extends C? ?D:E;");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_decorator_space() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// `@dec class` keeps a space (decorator ends in an identifier char), but
|
|
// `@dec() class` does not (`)` is not an identifier char).
|
|
min("@dec class C {}", "@dec class C{}");
|
|
min("@dec() class C {}", "@dec()class C{}");
|
|
min("@a.b @c class C {}", "@a.b @c class C{}");
|
|
// `export` before a decorated class needs no space (`@` is not an identifier char).
|
|
min("export @dec class C {}", "export@dec class C{}");
|
|
min("export @dec() class C {}", "export@dec()class C{}");
|
|
// `accessor` before a private name needs no space.
|
|
min("class C { accessor #x = 1 }", "class C{accessor#x=1}");
|
|
min("class C { accessor x = 1 }", "class C{accessor x=1}");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_ts_operator_space() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// `as`/`satisfies` need no leading space after a non-identifier char.
|
|
min("x = foo() as Bar;", "x=foo()as Bar;");
|
|
min("x = arr[0] as Bar;", "x=arr[0]as Bar;");
|
|
min("x = `s` as const;", "x=`s`as const;");
|
|
min("x = foo() satisfies Bar;", "x=foo()satisfies Bar;");
|
|
// ...but a space is required after an identifier.
|
|
min("x = y as Bar;", "x=y as Bar;");
|
|
min("x = y satisfies Bar;", "x=y satisfies Bar;");
|
|
// No trailing space when the target type starts with punctuation.
|
|
min("x = y as { a: 1 };", "x=y as{a:1;};");
|
|
min("x = y as (a: number) => void;", "x=y as(a:number)=>void;");
|
|
min("x = y satisfies [number];", "x=y satisfies[number];");
|
|
// A non-null `!` needs no trailing space before a member, statement end, or call.
|
|
min("x = a!.b;", "x=a!.b;");
|
|
min("a!;", "a!;");
|
|
min("x = a!();", "x=a!();");
|
|
// ...but must keep one before `=`/`==`/`===` so it does not become `!=`/`!==`.
|
|
min("a! = b;", "a! =b;");
|
|
min("x = a! == b;", "x=a! ==b;");
|
|
min("x = a! === b;", "x=a! ===b;");
|
|
// `!=`/`!==` do not retokenize, so no space is needed.
|
|
min("x = a! != b;", "x=a!!=b;");
|
|
// `enum` body brace needs no leading space.
|
|
min("enum E { A, B }", "enum E{A,B}");
|
|
min("const enum F { X = 1 }", "const enum F{X=1}");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_conditional_type_space() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// `extends`/`?`/`:` need no surrounding space next to non-identifier tokens.
|
|
min("type A<T> = T extends string ? 1 : 2;", "type A<T>=T extends string?1:2;");
|
|
min("type B<T> = Foo<T> extends string ? X : Y;", "type B<T>=Foo<T>extends string?X:Y;");
|
|
min("type C<T> = T[K] extends string ? X : Y;", "type C<T>=T[K]extends string?X:Y;");
|
|
// A prefix JSDoc nullable true branch must keep the `? ` separator (no `??`).
|
|
min("type D<T> = T extends B ? ?C : D;", "type D<T>=T extends B? ?C:D;");
|
|
// `extends`/`keyof` need no trailing space before a punctuation-starting type.
|
|
min("type E<T> = T extends [infer U] ? U : never;", "type E<T>=T extends[infer U]?U:never;");
|
|
min("type F<T> = T extends { a: 1 } ? 1 : 2;", "type F<T>=T extends{a:1;}?1:2;");
|
|
min("type G = keyof [a, b];", "type G=keyof[a,b];");
|
|
// ...but keep it before an identifier-starting type.
|
|
min("type H<T> = T extends Base ? 1 : 2;", "type H<T>=T extends Base?1:2;");
|
|
// `readonly`/`extends` before a parenthesized array element need no trailing space.
|
|
min("type I<T> = readonly (keyof T)[];", "type I<T>=readonly(keyof T)[];");
|
|
min(
|
|
"type J<T> = T extends (infer A)[] ? A : never;",
|
|
"type J<T>=T extends(infer A)[]?A:never;",
|
|
);
|
|
min("type K = readonly string[];", "type K=readonly string[];");
|
|
// `new` constructor type needs no space before `(`/`<`.
|
|
min("type L = new () => Foo;", "type L=new()=>Foo;");
|
|
min("type M = abstract new (x: number) => Bar;", "type M=abstract new(x:number)=>Bar;");
|
|
// A type-parameter constraint needs no trailing space before a punctuation-starting type.
|
|
min("type N<T extends (...a: any) => any> = T;", "type N<T extends(...a:any)=>any>=T;");
|
|
min("type O<T extends [a, b]> = T;", "type O<T extends[a,b]>=T;");
|
|
min("type P<T extends Base> = T;", "type P<T extends Base>=T;");
|
|
// `as`/`satisfies` before a union/intersection whose first member starts with punctuation.
|
|
min("x = res as { a: 1 } | undefined;", "x=res as{a:1;}|undefined;");
|
|
min("x = res as Foo | Bar;", "x=res as Foo|Bar;");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_export_type_star() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
min("export type * from \"m\";", "export type*from\"m\";");
|
|
min("export * from \"m\";", "export*from\"m\";");
|
|
min("export type * as ns from \"m\";", "export type*as ns from\"m\";");
|
|
}
|
|
|
|
#[test]
|
|
fn minify_heritage_and_mapped_space() {
|
|
let min = |src: &str, expected: &str| {
|
|
test_options_with_source_type(src, expected, SourceType::ts(), CodegenOptions::minify());
|
|
};
|
|
// Heritage `extends`/`implements` need no leading space after type-argument `>`.
|
|
min(
|
|
"class A<T> extends Base<T> implements I, J {}",
|
|
"class A<T>extends Base<T>implements I,J{}",
|
|
);
|
|
min("class B extends Base implements I {}", "class B extends Base implements I{}");
|
|
min(
|
|
"interface I<T = string> extends Omit<A, B>, C {}",
|
|
"interface I<T=string>extends Omit<A,B>,C{}",
|
|
);
|
|
min("interface J extends K {}", "interface J extends K{}");
|
|
// Mapped-type `readonly` needs no trailing space before `[`.
|
|
min(
|
|
"type M<T> = { readonly [P in keyof T]: T[P] };",
|
|
"type M<T>={readonly[P in keyof T]:T[P]};",
|
|
);
|
|
min(
|
|
"type N<T> = { -readonly [P in keyof T]-?: T[P] };",
|
|
"type N<T>={-readonly[P in keyof T]-?:T[P]};",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ts_as_expression_in_binary_expr() {
|
|
test_idempotency("key in (that as object)");
|
|
test_idempotency("'foo' in (x as Record<string, unknown>)");
|
|
test_idempotency("(x as object) instanceof Map");
|
|
test_idempotency("'foo' in ((x as object) as Record<string, unknown>)");
|
|
test_idempotency(
|
|
"!(typeof that === 'object' && 'keys' in that && typeof (that as object & { keys: unknown }).keys === 'function')",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ts_cast_expression_in_conditional_test() {
|
|
test_same("let a = typeof ((\"a\" as unknown) ? /a/ : false);\n");
|
|
test_same("let a = typeof ((\"a\" satisfies foo) ? /a/ : false);\n");
|
|
}
|
|
|
|
#[test]
|
|
fn ts_type_assertion() {
|
|
// `<T>x` (TS angle-bracket assertion) is only valid in non-tsx source.
|
|
let test_ts =
|
|
|src: &str| test_options_with_source_type(src, src, SourceType::ts(), default_options());
|
|
// `<T>x` is a unary expression; it should not be over-parenthesized.
|
|
test_ts("y = <T>x;\n");
|
|
test_ts("z = <T>x + 1;\n");
|
|
test_ts("foo(<T>x);\n");
|
|
test_ts("c = -<T>x;\n");
|
|
// Parentheses are required where a unary expression would re-associate.
|
|
test_ts("m = (<T>x).foo;\n");
|
|
test_ts("o = (<T>x)();\n");
|
|
// The base of `**` must be an UpdateExpression, so a type assertion is wrapped.
|
|
test_ts("n = (<T>x) ** 2;\n");
|
|
// Minified `a < <T>x` must keep a space so `<` + `<` isn't tokenized as `<<`.
|
|
test_options_with_source_type(
|
|
"a < <T>x;",
|
|
"a< <T>x;",
|
|
SourceType::ts(),
|
|
CodegenOptions::minify(),
|
|
);
|
|
// The assertion operand is a UnaryExpression and must not be over-parenthesized.
|
|
test_ts("a = <T>-x;\n");
|
|
test_ts("b = <T>typeof x;\n");
|
|
test_ts("c = <T><U>x;\n");
|
|
test_ts("d = <T>x();\n");
|
|
// Looser operands still need parentheses.
|
|
test_ts("e = <T>(b + c);\n");
|
|
test_ts("f = <T>(d ** e);\n");
|
|
test_ts("g = <T>(h ? i : j);\n");
|
|
}
|
|
|
|
#[test]
|
|
fn ts_instantiation_expression() {
|
|
test_same("v = (a ?? b)<T>;\n");
|
|
test_same("w = (a + b)<T>;\n");
|
|
test_same("x = (a, b)<T>;\n");
|
|
test_same("q = (a as B)<T>;\n");
|
|
test_same("r = (-a)<T>;\n");
|
|
test_same("y = a.b<T>;\n");
|
|
test_same("z = f<T>;\n");
|
|
test_same("p = a()<T>;\n");
|
|
}
|
|
|
|
#[test]
|
|
fn ts_instantiation_expression_precedence() {
|
|
for source in
|
|
["(f<T>).x", "(f<T>)[x]", "(f<T>)()", "new (f<T>)()", "(f<T>)`text`", "((a ?? b)<T>).x"]
|
|
{
|
|
test_ts(source, &format!("{source};\n"));
|
|
test_idempotency(source);
|
|
test_idempotency_options(source, &CodegenOptions::minify());
|
|
}
|
|
for source in ["(f<T>).x", "(f<T>)[x]"] {
|
|
test_options_with_source_type(
|
|
source,
|
|
&format!("{source};"),
|
|
SourceType::ts(),
|
|
CodegenOptions::minify(),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ts_satisfies_expression() {
|
|
test_same("(foo satisfies null) | ~\"\";\n");
|
|
test_same("(foo satisfies null) & ~\"\";\n");
|
|
test_idempotency("d = x satisfies y");
|
|
test_idempotency("const Foo = (() => {})() satisfies X");
|
|
test_idempotency("const Bar = (x as Y) satisfies Z");
|
|
test_idempotency("(x satisfies Y).foo");
|
|
test_idempotency("(x satisfies Y)[0]");
|
|
test_idempotency("(x satisfies Y)()");
|
|
test_idempotency("x satisfies Y || z");
|
|
test_idempotency("x satisfies Y && z");
|
|
test_idempotency("x satisfies Y === z");
|
|
}
|
|
|
|
#[test]
|
|
fn type_codegen_with_preserve_parens_off() {
|
|
let parse_options = ParseOptions { preserve_parens: false, ..Default::default() };
|
|
|
|
test_with_parse_options(
|
|
"type T = keyof (EventMap & Extra);",
|
|
"type T = keyof (EventMap & Extra);\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options(
|
|
"type T = [(Anno | undefined)?];",
|
|
"type T = [(Anno | undefined)?];\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options("const foo = (a ?? b)!;", "const foo = (a ?? b)!;\n", parse_options);
|
|
test_with_parse_options(
|
|
"type T = (Out & (Step extends A ? B : C)) & (Step extends D ? E : F);",
|
|
"type T = (Out & (Step extends A ? B : C)) & (Step extends D ? E : F);\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options(
|
|
"type T = ({ [K in keyof Obj]: Obj[K] } & { a: 1 }) & { b: 2 };",
|
|
"type T = ({ [K in keyof Obj]: Obj[K] } & {\n\ta: 1;\n}) & {\n\tb: 2;\n};\n",
|
|
parse_options,
|
|
);
|
|
// `type t = (intrinsic)` is a reference to a type named `intrinsic`, but `type t = intrinsic`
|
|
// is the `intrinsic` keyword, so the parentheses must be kept to preserve the meaning.
|
|
test_with_parse_options("type t = (intrinsic);", "type t = (intrinsic);\n", parse_options);
|
|
test_with_parse_options(
|
|
"type t = (intrinsic.foo);",
|
|
"type t = intrinsic.foo;\n",
|
|
parse_options,
|
|
);
|
|
// A leftmost bare `intrinsic` reference inside a compound type must also keep parentheses,
|
|
// otherwise the body re-parses as the `intrinsic` keyword and then fails before `[]`/`|`/`&`/`extends`.
|
|
test_with_parse_options("type t = (intrinsic)[];", "type t = (intrinsic[]);\n", parse_options);
|
|
test_with_parse_options(
|
|
"type t = (intrinsic)[\"x\"];",
|
|
"type t = (intrinsic[\"x\"]);\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options(
|
|
"type t = (intrinsic) | string;",
|
|
"type t = (intrinsic | string);\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options(
|
|
"type t = (intrinsic) & string;",
|
|
"type t = (intrinsic & string);\n",
|
|
parse_options,
|
|
);
|
|
test_with_parse_options(
|
|
"type t = (intrinsic) extends string ? 1 : 2;",
|
|
"type t = (intrinsic extends string ? 1 : 2);\n",
|
|
parse_options,
|
|
);
|
|
// `intrinsic` is not the leftmost type here, so no parentheses are needed.
|
|
test_with_parse_options(
|
|
"type t = string | (intrinsic);",
|
|
"type t = string | intrinsic;\n",
|
|
parse_options,
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ts_cast_assignment_target_is_parenthesized() {
|
|
test_same("(foo as Bar) = baz;\n");
|
|
test_same("(foo satisfies Bar) = baz;\n");
|
|
test_same("(foo.bar as Bar) = baz;\n");
|
|
test_same("(foo[key] as Bar) = baz;\n");
|
|
test_options_with_source_type(
|
|
"(<Bar>foo) = baz;\n",
|
|
"(<Bar>foo) = baz;\n",
|
|
SourceType::ts(),
|
|
default_options(),
|
|
);
|
|
test_idempotency("(foo as Bar) = baz");
|
|
test_idempotency("(foo satisfies Bar) = baz");
|
|
}
|