From c609058fd3085cf4c5781b894ec9bd5fa5978ee1 Mon Sep 17 00:00:00 2001 From: Seiya <20365512+seiyab@users.noreply.github.com> Date: Mon, 16 Oct 2023 16:04:28 +0900 Subject: [PATCH] Support `(type) satisfies never;` (#15514) --- changelog_unreleased/typescript/15514.md | 15 ++ src/language-js/needs-parens.js | 23 +++ .../__snapshots__/jsfmt.spec.js.snap | 100 +++++++++- .../flow/as-satisfies-expression/as-const.js | 2 + .../format/flow/as-satisfies-expression/as.js | 2 + .../flow/as-satisfies-expression/satisfies.js | 37 ++++ .../as/__snapshots__/jsfmt.spec.js.snap | 20 ++ .../typescript/as/expression-statement.ts | 4 + .../__snapshots__/jsfmt.spec.js.snap | 175 ++++++++++++++++++ .../expression-statement.ts | 38 ++++ 10 files changed, 409 insertions(+), 7 deletions(-) create mode 100644 changelog_unreleased/typescript/15514.md create mode 100644 tests/format/typescript/as/expression-statement.ts create mode 100644 tests/format/typescript/satisfies-operators/expression-statement.ts diff --git a/changelog_unreleased/typescript/15514.md b/changelog_unreleased/typescript/15514.md new file mode 100644 index 000000000..d733fd059 --- /dev/null +++ b/changelog_unreleased/typescript/15514.md @@ -0,0 +1,15 @@ +#### Keep required parenthesis around some specific keyword like identifiers in expression statement of satisfies / as expression (#15514 by @seiyab) + + +```ts +// Input +(type) satisfies never; + + +// Prettier stable +type satisfies never; + + +// Prettier main +(type) satisfies never; +``` diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 6814a8d98..545e4272d 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -110,6 +110,29 @@ function needsParens(path, options) { } } + // `(type) satisfies never;` and similar cases + if (key === "expression") { + switch (node.name) { + case "await": + case "interface": + case "module": + case "using": + case "yield": + case "let": + case "type": { + const ancestorNeitherAsNorSatisfies = path.findAncestor( + (node) => !isBinaryCastExpression(node), + ); + if ( + ancestorNeitherAsNorSatisfies !== parent && + ancestorNeitherAsNorSatisfies.type === "ExpressionStatement" + ) { + return true; + } + } + } + } + return false; } diff --git a/tests/format/flow/as-satisfies-expression/__snapshots__/jsfmt.spec.js.snap b/tests/format/flow/as-satisfies-expression/__snapshots__/jsfmt.spec.js.snap index 8195464b2..f8301732e 100644 --- a/tests/format/flow/as-satisfies-expression/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/flow/as-satisfies-expression/__snapshots__/jsfmt.spec.js.snap @@ -56,6 +56,8 @@ const iter2 = createIterator(self.controller, child, self.tag as BlahFunctionCom x as any as T; +(type) as T; + =====================================output===================================== // @flow @@ -123,6 +125,8 @@ const iter2 = createIterator( x as any as T; +(type) as T; + ================================================================================ `; @@ -132,7 +136,9 @@ exports[`as-const.js [babel-flow] format 1`] = ` 11 | DEBUG: 7, > 12 | } as const; | ^ - 13 |" + 13 | + 14 | (type) as const; + 15 |" `; exports[`as-const.js format 1`] = ` @@ -154,6 +160,8 @@ export const LOG_LEVEL = { DEBUG: 7, } as const; +(type) as const; + =====================================output===================================== // @flow @@ -168,6 +176,8 @@ export const LOG_LEVEL = { DEBUG: 7, } as const; +(type) as const; + ================================================================================ `; @@ -400,12 +410,14 @@ function foo() { `; exports[`satisfies.js [babel-flow] format 1`] = ` -"Missing semicolon. (3:12) - 1 | // @flow - 2 | -> 3 | const x = y satisfies T; - | ^ - 4 |" +"Unexpected token, expected "type" (32:8) + 30 | satisfies satisfies mixed; + 31 | as satisfies mixed; +> 32 | opaque satisfies mixed; + | ^ + 33 | + 34 | abc satisfies mixed; // not a keyword + 35 | }" `; exports[`satisfies.js format 1`] = ` @@ -418,11 +430,85 @@ printWidth: 80 const x = y satisfies T; +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = (type: 'foo' | 'bar') => { +switch (type) { + case 'foo': + return 1; + case 'bar': + return 2; + default: + // exhaustiveness check idiom + (type) satisfies empty; + throw new Error('unreachable'); +} +} + +function needParens() { +(let) satisfies mixed; +(interface) satisfies mixed; +(module) satisfies mixed; +(using) satisfies mixed; +(yield) satisfies mixed; +(await) satisfies mixed; +} + +function noNeedParens() { +async satisfies mixed; +satisfies satisfies mixed; +as satisfies mixed; +opaque satisfies mixed; + +abc satisfies mixed; // not a keyword +} + +function satisfiesChain() { +satisfies satisfies satisfies satisfies satisfies; +(type) satisfies empty satisfies mixed; +} + =====================================output===================================== // @flow const x = y satisfies T; +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = (type: "foo" | "bar") => { + switch (type) { + case "foo": + return 1; + case "bar": + return 2; + default: + // exhaustiveness check idiom + (type) satisfies empty; + throw new Error("unreachable"); + } +}; + +function needParens() { + (let) satisfies mixed; + (interface) satisfies mixed; + (module) satisfies mixed; + (using) satisfies mixed; + (yield) satisfies mixed; + (await) satisfies mixed; +} + +function noNeedParens() { + async satisfies mixed; + satisfies satisfies mixed; + as satisfies mixed; + opaque satisfies mixed; + + abc satisfies mixed; // not a keyword +} + +function satisfiesChain() { + satisfies satisfies satisfies satisfies satisfies; + (type) satisfies empty satisfies mixed; +} + ================================================================================ `; diff --git a/tests/format/flow/as-satisfies-expression/as-const.js b/tests/format/flow/as-satisfies-expression/as-const.js index cd57df052..7504a3835 100644 --- a/tests/format/flow/as-satisfies-expression/as-const.js +++ b/tests/format/flow/as-satisfies-expression/as-const.js @@ -10,3 +10,5 @@ export const LOG_LEVEL = { INFO: 6, DEBUG: 7, } as const; + +(type) as const; diff --git a/tests/format/flow/as-satisfies-expression/as.js b/tests/format/flow/as-satisfies-expression/as.js index 68d52b1ee..69dd74461 100644 --- a/tests/format/flow/as-satisfies-expression/as.js +++ b/tests/format/flow/as-satisfies-expression/as.js @@ -36,3 +36,5 @@ const iter1 = createIterator(this.controller, child, this.tag as BlahFunctionCom const iter2 = createIterator(self.controller, child, self.tag as BlahFunctionComponent); x as any as T; + +(type) as T; diff --git a/tests/format/flow/as-satisfies-expression/satisfies.js b/tests/format/flow/as-satisfies-expression/satisfies.js index 727b027e7..6052407f6 100644 --- a/tests/format/flow/as-satisfies-expression/satisfies.js +++ b/tests/format/flow/as-satisfies-expression/satisfies.js @@ -1,3 +1,40 @@ // @flow const x = y satisfies T; + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = (type: 'foo' | 'bar') => { +switch (type) { + case 'foo': + return 1; + case 'bar': + return 2; + default: + // exhaustiveness check idiom + (type) satisfies empty; + throw new Error('unreachable'); +} +} + +function needParens() { +(let) satisfies mixed; +(interface) satisfies mixed; +(module) satisfies mixed; +(using) satisfies mixed; +(yield) satisfies mixed; +(await) satisfies mixed; +} + +function noNeedParens() { +async satisfies mixed; +satisfies satisfies mixed; +as satisfies mixed; +opaque satisfies mixed; + +abc satisfies mixed; // not a keyword +} + +function satisfiesChain() { +satisfies satisfies satisfies satisfies satisfies; +(type) satisfies empty satisfies mixed; +} diff --git a/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap index e31f35ce9..eec50753b 100644 --- a/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/typescript/as/__snapshots__/jsfmt.spec.js.snap @@ -327,6 +327,26 @@ export default (function log() {} as typeof console.log); ================================================================================ `; +exports[`expression-statement.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== +// expression statemnt of "as" expression hardly ever makes sense, but it's still valid. +const [type, x] = [0, 0]; +(type) as unknown; +x as unknown; + +=====================================output===================================== +// expression statemnt of "as" expression hardly ever makes sense, but it's still valid. +const [type, x] = [0, 0]; +(type) as unknown; +x as unknown; + +================================================================================ +`; + exports[`long-identifiers.ts format 1`] = ` ====================================options===================================== parsers: ["typescript"] diff --git a/tests/format/typescript/as/expression-statement.ts b/tests/format/typescript/as/expression-statement.ts new file mode 100644 index 000000000..abdb2ed74 --- /dev/null +++ b/tests/format/typescript/as/expression-statement.ts @@ -0,0 +1,4 @@ +// expression statemnt of "as" expression hardly ever makes sense, but it's still valid. +const [type, x] = [0, 0]; +(type) as unknown; +x as unknown; diff --git a/tests/format/typescript/satisfies-operators/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/satisfies-operators/__snapshots__/jsfmt.spec.js.snap index 10020bdd9..17e5bc52c 100644 --- a/tests/format/typescript/satisfies-operators/__snapshots__/jsfmt.spec.js.snap +++ b/tests/format/typescript/satisfies-operators/__snapshots__/jsfmt.spec.js.snap @@ -498,6 +498,181 @@ export default (function log() {} satisfies typeof console.log); ================================================================================ `; +exports[`expression-statement.ts - {"semi":false} format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 +semi: false + | printWidth +=====================================input====================================== + +let type: 'foo' | 'bar' = 'foo'; + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = () => { +switch (type) { + case 'foo': + return 1; + case 'bar': + return 2; + default: + // exhaustiveness check idiom + (type) satisfies never; + throw new Error('unreachable'); +} +} + +function needParens() { +(let) satisfies unknown; +(interface) satisfies unknown; +(module) satisfies unknown; +(using) satisfies unknown; +(yield) satisfies unknown; +(await) satisfies unknown; +} + +function noNeedParens() { +async satisfies unknown; +satisfies satisfies unknown; +as satisfies unknown; + +abc satisfies unknown; // not a keyword +} + +function satisfiesChain() { +satisfies satisfies satisfies satisfies satisfies; +(type) satisfies never satisfies unknown; +} + +=====================================output===================================== +let type: "foo" | "bar" = "foo" + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = () => { + switch (type) { + case "foo": + return 1 + case "bar": + return 2 + default: + // exhaustiveness check idiom + ;(type) satisfies never + throw new Error("unreachable") + } +} + +function needParens() { + ;(let) satisfies unknown + ;(interface) satisfies unknown + ;(module) satisfies unknown + ;(using) satisfies unknown + ;(yield) satisfies unknown + ;(await) satisfies unknown +} + +function noNeedParens() { + async satisfies unknown + satisfies satisfies unknown + as satisfies unknown + + abc satisfies unknown // not a keyword +} + +function satisfiesChain() { + satisfies satisfies satisfies satisfies satisfies + ;(type) satisfies never satisfies unknown +} + +================================================================================ +`; + +exports[`expression-statement.ts format 1`] = ` +====================================options===================================== +parsers: ["typescript"] +printWidth: 80 + | printWidth +=====================================input====================================== + +let type: 'foo' | 'bar' = 'foo'; + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = () => { +switch (type) { + case 'foo': + return 1; + case 'bar': + return 2; + default: + // exhaustiveness check idiom + (type) satisfies never; + throw new Error('unreachable'); +} +} + +function needParens() { +(let) satisfies unknown; +(interface) satisfies unknown; +(module) satisfies unknown; +(using) satisfies unknown; +(yield) satisfies unknown; +(await) satisfies unknown; +} + +function noNeedParens() { +async satisfies unknown; +satisfies satisfies unknown; +as satisfies unknown; + +abc satisfies unknown; // not a keyword +} + +function satisfiesChain() { +satisfies satisfies satisfies satisfies satisfies; +(type) satisfies never satisfies unknown; +} + +=====================================output===================================== +let type: "foo" | "bar" = "foo"; + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = () => { + switch (type) { + case "foo": + return 1; + case "bar": + return 2; + default: + // exhaustiveness check idiom + (type) satisfies never; + throw new Error("unreachable"); + } +}; + +function needParens() { + (let) satisfies unknown; + (interface) satisfies unknown; + (module) satisfies unknown; + (using) satisfies unknown; + (yield) satisfies unknown; + (await) satisfies unknown; +} + +function noNeedParens() { + async satisfies unknown; + satisfies satisfies unknown; + as satisfies unknown; + + abc satisfies unknown; // not a keyword +} + +function satisfiesChain() { + satisfies satisfies satisfies satisfies satisfies; + (type) satisfies never satisfies unknown; +} + +================================================================================ +`; + exports[`gt-lt.ts - {"semi":false} format 1`] = ` ====================================options===================================== parsers: ["typescript"] diff --git a/tests/format/typescript/satisfies-operators/expression-statement.ts b/tests/format/typescript/satisfies-operators/expression-statement.ts new file mode 100644 index 000000000..bf9a9845a --- /dev/null +++ b/tests/format/typescript/satisfies-operators/expression-statement.ts @@ -0,0 +1,38 @@ + +let type: 'foo' | 'bar' = 'foo'; + +// demonstrating how "satisfies" expression can be practically used as expression statement. +const _ = () => { +switch (type) { + case 'foo': + return 1; + case 'bar': + return 2; + default: + // exhaustiveness check idiom + (type) satisfies never; + throw new Error('unreachable'); +} +} + +function needParens() { +(let) satisfies unknown; +(interface) satisfies unknown; +(module) satisfies unknown; +(using) satisfies unknown; +(yield) satisfies unknown; +(await) satisfies unknown; +} + +function noNeedParens() { +async satisfies unknown; +satisfies satisfies unknown; +as satisfies unknown; + +abc satisfies unknown; // not a keyword +} + +function satisfiesChain() { +satisfies satisfies satisfies satisfies satisfies; +(type) satisfies never satisfies unknown; +}