diff --git a/changelog_unreleased/typescript/14548.md b/changelog_unreleased/typescript/14548.md new file mode 100644 index 000000000000..82262a8462ea --- /dev/null +++ b/changelog_unreleased/typescript/14548.md @@ -0,0 +1,24 @@ +#### Allow decorators on private members and class expressions (#14548 by @fisker) + + +```ts +// Input +class A { + @decorator() + #privateMethod () {} +} + +// Prettier stable +SyntaxError: Decorators are not valid here. (2:3) + 1 | class A { +> 2 | @decorator() + | ^^^^^^^^^^^^ + 3 | #privateMethod () {} + 4 | } + +// Prettier main +class A { + @decorator() + #privateMethod() {} +} +``` diff --git a/src/language-js/parse/postprocess/typescript.js b/src/language-js/parse/postprocess/typescript.js index 9b343a766a74..926f60432b8e 100644 --- a/src/language-js/parse/postprocess/typescript.js +++ b/src/language-js/parse/postprocess/typescript.js @@ -30,6 +30,20 @@ function throwErrorOnTsNode(node, message) { throwSyntaxError({ loc: { start, end } }, message); } +function nodeCanBeDecorated(node) { + const ts = require("typescript"); + + return [true, false].some((useLegacyDecorators) => + // @ts-expect-error -- internal? + ts.nodeCanBeDecorated( + useLegacyDecorators, + node, + node.parent, + node.parent.parent + ) + ); +} + // Invalid decorators are removed since `@typescript-eslint/typescript-estree` v4 // https://github.com/typescript-eslint/typescript-eslint/pull/2375 // There is a `checkGrammarDecorators` in `typescript` package, consider use it directly in future @@ -44,16 +58,7 @@ function throwErrorForInvalidDecorator(node) { const { SyntaxKind } = ts; for (const modifier of modifiers) { if (ts.isDecorator(modifier)) { - const legacyDecorators = true; - if ( - // @ts-expect-error -- internal? - !ts.nodeCanBeDecorated( - legacyDecorators, - node, - node.parent, - node.parent.parent - ) - ) { + if (!nodeCanBeDecorated(node)) { if ( node.kind === SyntaxKind.MethodDeclaration && // @ts-expect-error -- internal? @@ -67,9 +72,8 @@ function throwErrorForInvalidDecorator(node) { throwErrorOnTsNode(modifier, "Decorators are not valid here."); } } else if ( - legacyDecorators && - (node.kind === SyntaxKind.GetAccessor || - node.kind === SyntaxKind.SetAccessor) + node.kind === SyntaxKind.GetAccessor || + node.kind === SyntaxKind.SetAccessor ) { // @ts-expect-error -- internal? const accessors = ts.getAllAccessorDeclarations( diff --git a/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap index e400eac0c500..a820d988515c 100644 --- a/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap @@ -24,14 +24,6 @@ exports[`arguments.js [flow] format 1`] = ` 3 |" `; -exports[`arguments.js [typescript] format 1`] = ` -"Decorators are not valid here. (1:13) -> 1 | console.log(@deco class Foo {}) - | ^^^^^ - 2 | console.log(@deco class {}) - 3 |" -`; - exports[`arguments.js - {"semi":false} [acorn] format 1`] = ` "Unexpected character '@' (1:13) > 1 | console.log(@deco class Foo {}) @@ -56,14 +48,6 @@ exports[`arguments.js - {"semi":false} [flow] format 1`] = ` 3 |" `; -exports[`arguments.js - {"semi":false} [typescript] format 1`] = ` -"Decorators are not valid here. (1:13) -> 1 | console.log(@deco class Foo {}) - | ^^^^^ - 2 | console.log(@deco class {}) - 3 |" -`; - exports[`arguments.js - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] @@ -132,15 +116,6 @@ exports[`class-expression.js [flow] format 1`] = ` 4 | (@deco class Foo {});" `; -exports[`class-expression.js [typescript] format 1`] = ` -"Decorators are not valid here. (1:13) -> 1 | const a1 = (@deco class Foo {}); - | ^^^^^ - 2 | const a2 = (@deco class {}); - 3 | - 4 | (@deco class Foo {});" -`; - exports[`class-expression.js - {"semi":false} [acorn] format 1`] = ` "Unexpected character '@' (1:13) > 1 | const a1 = (@deco class Foo {}); @@ -168,15 +143,6 @@ exports[`class-expression.js - {"semi":false} [flow] format 1`] = ` 4 | (@deco class Foo {});" `; -exports[`class-expression.js - {"semi":false} [typescript] format 1`] = ` -"Decorators are not valid here. (1:13) -> 1 | const a1 = (@deco class Foo {}); - | ^^^^^ - 2 | const a2 = (@deco class {}); - 3 | - 4 | (@deco class Foo {});" -`; - exports[`class-expression.js - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] @@ -324,14 +290,6 @@ exports[`member-expression.js [flow] format 1`] = ` 3 |" `; -exports[`member-expression.js [typescript] format 1`] = ` -"Decorators are not valid here. (1:2) -> 1 | (@deco class Foo {}).name; - | ^^^^^ - 2 | (@deco class {}).name; - 3 |" -`; - exports[`member-expression.js - {"semi":false} [acorn] format 1`] = ` "Unexpected character '@' (1:2) > 1 | (@deco class Foo {}).name; @@ -356,14 +314,6 @@ exports[`member-expression.js - {"semi":false} [flow] format 1`] = ` 3 |" `; -exports[`member-expression.js - {"semi":false} [typescript] format 1`] = ` -"Decorators are not valid here. (1:2) -> 1 | (@deco class Foo {}).name; - | ^^^^^ - 2 | (@deco class {}).name; - 3 |" -`; - exports[`member-expression.js - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] @@ -436,15 +386,6 @@ exports[`super-class.js [flow] format 1`] = ` 4 |" `; -exports[`super-class.js [typescript] format 1`] = ` -"Decorators are not valid here. (1:20) -> 1 | class Foo extends (@deco class Foo {}){} - | ^^^^^ - 2 | - 3 | class Foo extends (@deco class {}){} - 4 |" -`; - exports[`super-class.js - {"semi":false} [acorn] format 1`] = ` "Unexpected character '@' (1:20) > 1 | class Foo extends (@deco class Foo {}){} @@ -472,15 +413,6 @@ exports[`super-class.js - {"semi":false} [flow] format 1`] = ` 4 |" `; -exports[`super-class.js - {"semi":false} [typescript] format 1`] = ` -"Decorators are not valid here. (1:20) -> 1 | class Foo extends (@deco class Foo {}){} - | ^^^^^ - 2 | - 3 | class Foo extends (@deco class {}){} - 4 |" -`; - exports[`super-class.js - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["babel", "flow", "typescript"] diff --git a/tests/format/js/decorators/class-expression/jsfmt.spec.js b/tests/format/js/decorators/class-expression/jsfmt.spec.js index b7c768416a69..028ed3fa8eac 100644 --- a/tests/format/js/decorators/class-expression/jsfmt.spec.js +++ b/tests/format/js/decorators/class-expression/jsfmt.spec.js @@ -1,6 +1,5 @@ const errors = { flow: true, - typescript: true, acorn: true, espree: true, }; diff --git a/tests/format/typescript/decorators/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/decorators/__snapshots__/jsfmt.spec.js.snap index 641a611284ce..8c577e4f59a5 100644 --- a/tests/format/typescript/decorators/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/typescript/decorators/__snapshots__/jsfmt.spec.js.snap @@ -484,6 +484,48 @@ class MyContainerComponent { ================================================================================ `; +exports[`legacy.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +[ + @decorator() class {}, + @decorator() class A {}, +]; + +class A { + @decorator() accessor #field; +} + +class B { + @decorator() #field () {} +} + +=====================================output===================================== +[ + ( + @decorator() + class {} + ), + ( + @decorator() + class A {} + ), +]; + +class A { + @decorator() accessor #field; +} + +class B { + @decorator() #field() {} +} + +================================================================================ +`; + exports[`mobx.ts format 1`] = ` ====================================options===================================== parsers: ["typescript"] diff --git a/tests/format/typescript/decorators/legacy.ts b/tests/format/typescript/decorators/legacy.ts new file mode 100644 index 000000000000..450e40cbcbba --- /dev/null +++ b/tests/format/typescript/decorators/legacy.ts @@ -0,0 +1,12 @@ +[ + @decorator() class {}, + @decorator() class A {}, +]; + +class A { + @decorator() accessor #field; +} + +class B { + @decorator() #field () {} +} diff --git a/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap index 8c1278a79a21..409110a22a02 100644 --- a/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap @@ -49,7 +49,7 @@ exports[`3.ts [typescript] format 1`] = ` 2 |" `; -exports[`4.ts [babel-ts] format 1`] = ` +exports[`4.ts format 1`] = ` ====================================options===================================== parsers: ["typescript"] printWidth: 80 @@ -66,13 +66,6 @@ printWidth: 80 ================================================================================ `; -exports[`4.ts [typescript] format 1`] = ` -"Decorators are not valid here. (1:2) -> 1 | (@f<(v: T) => void>() class {}); - | ^^^^^^^^^^^^^^^^^^^^^^^ - 2 |" -`; - exports[`5.tsx [babel-ts] format 1`] = ` ====================================options===================================== parsers: ["typescript"] diff --git a/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js b/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js index 2775ae27811f..b3acebe65b16 100644 --- a/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js +++ b/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js @@ -1,3 +1,3 @@ run_spec(__dirname, ["typescript"], { - errors: { typescript: ["3.ts", "4.ts", "5.tsx"] }, + errors: { typescript: ["3.ts", "5.tsx"] }, });