Files
camc314 32d00c55bc fix(codegen): preserve instantiation expression precedence (#26424)
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.
2026-09-07 18:20:11 +00:00

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");
}