From 9102b73d8cd01795e7ba5af8a476d500c5dbcf2c Mon Sep 17 00:00:00 2001 From: y-schneider Date: Sat, 13 Jul 2024 14:15:40 +0200 Subject: [PATCH] Add parentheses for decorator expressions (#16458) Co-authored-by: fisker --- changelog_unreleased/typescript/16458.md | 18 +++++ cspell.json | 1 + src/language-js/needs-parens.js | 70 ++++++++++--------- .../__snapshots__/format.test.js.snap | 36 +++++++++- .../format/js/decorators/member-expression.js | 16 +++++ .../__snapshots__/format.test.js.snap | 8 +-- 6 files changed, 109 insertions(+), 40 deletions(-) create mode 100644 changelog_unreleased/typescript/16458.md diff --git a/changelog_unreleased/typescript/16458.md b/changelog_unreleased/typescript/16458.md new file mode 100644 index 000000000..ed889ee99 --- /dev/null +++ b/changelog_unreleased/typescript/16458.md @@ -0,0 +1,18 @@ +#### Add parentheses for decorator expressions (#16458 by @y-schneider) + +Prevent parentheses around member expressions or tagged template literals from being removed to follow the stricter parsing rules of TypeScript 5.5. + + +```ts +// Input +@(foo`tagged template`) +class X {} + +// Prettier stable +@foo`tagged template` +class X {} + +// Prettier main +@(foo`tagged template`) +class X {} +``` diff --git a/cspell.json b/cspell.json index 145a67347..b88278a5c 100644 --- a/cspell.json +++ b/cspell.json @@ -295,6 +295,7 @@ "unaries", "uncook", "unparenthesised", + "Unparenthesized", "Unrestrict", "upvoted", "upvotes", diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 67383e9e8..74d4b151d 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -224,40 +224,10 @@ function needsParens(path, options) { ); case "Decorator": - if (key === "expression") { - if (isMemberExpression(node) && node.computed) { - return true; - } - - let hasCallExpression = false; - let hasMemberExpression = false; - let current = node; - while (current) { - switch (current.type) { - case "MemberExpression": - hasMemberExpression = true; - current = current.object; - break; - case "CallExpression": - if ( - /** @(x().y) */ hasMemberExpression || - /** @(x().y()) */ hasCallExpression - ) { - return options.parser !== "typescript"; - } - hasCallExpression = true; - current = current.callee; - break; - case "Identifier": - return false; - case "TaggedTemplateExpression": - // babel-parser cannot parse - // @foo`bar` - return options.parser !== "typescript"; - default: - return true; - } - } + if ( + key === "expression" && + !canDecoratorExpressionUnparenthesized(node) + ) { return true; } break; @@ -1276,4 +1246,36 @@ function shouldAddParenthesesToChainElement(path) { return false; } +function isDecoratorMemberExpression(node) { + if (node.type === "Identifier") { + return true; + } + + if (isMemberExpression(node)) { + return ( + !node.computed && + !node.optional && + node.property.type === "Identifier" && + isDecoratorMemberExpression(node.object) + ); + } + + return false; +} + +// Based on babel implementation +// https://github.com/nicolo-ribaudo/babel/blob/c4b88a4e5005364255f7e964fe324cf7bfdfb019/packages/babel-generator/src/node/index.ts#L111 +function canDecoratorExpressionUnparenthesized(node) { + if (node.type === "ChainExpression") { + node = node.expression; + } + + return ( + isDecoratorMemberExpression(node) || + (isCallExpression(node) && + !node.optional && + isDecoratorMemberExpression(node.callee)) + ); +} + export default needsParens; diff --git a/tests/format/js/decorators/__snapshots__/format.test.js.snap b/tests/format/js/decorators/__snapshots__/format.test.js.snap index 749084e88..d7732fb5c 100644 --- a/tests/format/js/decorators/__snapshots__/format.test.js.snap +++ b/tests/format/js/decorators/__snapshots__/format.test.js.snap @@ -176,7 +176,7 @@ exports[`member-expression.js [acorn] format 1`] = ` "Unexpected character '@' (3:5) 1 | [ 2 | class { -> 3 | @(decorators[0]) +> 3 | @(decorator) | ^ 4 | method() {} 5 | }, @@ -188,7 +188,7 @@ exports[`member-expression.js [espree] format 1`] = ` "Unexpected character '@' (3:5) 1 | [ 2 | class { -> 3 | @(decorators[0]) +> 3 | @(decorator) | ^ 4 | method() {} 5 | }, @@ -203,6 +203,18 @@ printWidth: 80 | printWidth =====================================input====================================== [ + class { + @(decorator) + method() {} + }, + class { + @(decorator()) + method() {} + }, + class { + @(decorator?.()) + method() {} + }, class { @(decorators[0]) method() {} @@ -223,6 +235,10 @@ printWidth: 80 @(decorators?.at(0)) method() {} }, + class { + @(decorators.at?.(0)) + method() {} + }, class { @(decorators.first) method() {} @@ -255,6 +271,18 @@ printWidth: 80 =====================================output===================================== [ + class { + @decorator + method() {} + }, + class { + @decorator() + method() {} + }, + class { + @(decorator?.()) + method() {} + }, class { @(decorators[0]) method() {} @@ -275,6 +303,10 @@ printWidth: 80 @(decorators?.at(0)) method() {} }, + class { + @(decorators.at?.(0)) + method() {} + }, class { @decorators.first method() {} diff --git a/tests/format/js/decorators/member-expression.js b/tests/format/js/decorators/member-expression.js index 05baf20bc..89eda8886 100644 --- a/tests/format/js/decorators/member-expression.js +++ b/tests/format/js/decorators/member-expression.js @@ -1,4 +1,16 @@ [ + class { + @(decorator) + method() {} + }, + class { + @(decorator()) + method() {} + }, + class { + @(decorator?.()) + method() {} + }, class { @(decorators[0]) method() {} @@ -19,6 +31,10 @@ @(decorators?.at(0)) method() {} }, + class { + @(decorators.at?.(0)) + method() {} + }, class { @(decorators.first) method() {} diff --git a/tests/format/misc/typescript-only/__snapshots__/format.test.js.snap b/tests/format/misc/typescript-only/__snapshots__/format.test.js.snap index b58d035e7..3759f3e30 100644 --- a/tests/format/misc/typescript-only/__snapshots__/format.test.js.snap +++ b/tests/format/misc/typescript-only/__snapshots__/format.test.js.snap @@ -301,7 +301,7 @@ semi: false class X {} =====================================output===================================== -@test().x("global").y() +@(test().x("global").y()) class X {} ================================================================================ @@ -317,7 +317,7 @@ printWidth: 80 class X {} =====================================output===================================== -@test().x("global").y() +@(test().x("global").y()) class X {} ================================================================================ @@ -337,7 +337,7 @@ class Test { =====================================output===================================== class Test { - @foo\`bar\` + @(foo\`bar\`) text: string = "text" } @@ -357,7 +357,7 @@ class Test { =====================================output===================================== class Test { - @foo\`bar\` + @(foo\`bar\`) text: string = "text"; }