diff --git a/changelog_unreleased/flow/14767.md b/changelog_unreleased/flow/14767.md new file mode 100644 index 000000000..5fe53cf2c --- /dev/null +++ b/changelog_unreleased/flow/14767.md @@ -0,0 +1,15 @@ +#### Support type guards (#14767 by @panagosg7) + + +```jsx +// Input +function isString (x: mixed): x is string { return typeof x === "string"; } + +// Prettier stable +// Does not parse + +// Prettier main +function isString(x: mixed): x is string { + return typeof x === 'string'; +} +``` diff --git a/package.json b/package.json index 1305d5e86..95482b7d5 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "file-entry-cache": "6.0.1", "find-cache-dir": "4.0.0", "find-parent-dir": "0.3.1", - "flow-parser": "0.204.0", + "flow-parser": "0.205.0", "get-stdin": "9.0.0", "graphql": "16.6.0", "hermes-parser": "0.10.1", diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js index 98790c743..98821f07c 100644 --- a/src/language-js/needs-parens.js +++ b/src/language-js/needs-parens.js @@ -585,6 +585,27 @@ function needsParens(path, options) { return true; } + /* Matches the following case in Flow: + ``` + const a = (x: any): x is (number => string) => true; + ``` + This case is not necessary in TS since `number => string` is not a valid + arrow type there. + */ + if ( + path.match( + undefined, + (node, key) => + key === "typeAnnotation" && node.type === "TypePredicate", + (node, key) => + key === "typeAnnotation" && node.type === "TypeAnnotation", + (node, key) => + key === "returnType" && node.type === "ArrowFunctionExpression" + ) + ) { + return true; + } + const ancestor = parent.type === "NullableTypeAnnotation" ? path.grandparent : parent; diff --git a/src/language-js/print/flow.js b/src/language-js/print/flow.js index c07cf501b..8c5f0eb89 100644 --- a/src/language-js/print/flow.js +++ b/src/language-js/print/flow.js @@ -25,6 +25,7 @@ import { printTypeAnnotationProperty, printArrayType, printTypeQuery, + printTypePredicate, } from "./type-annotation.js"; import { printInterface } from "./interface.js"; import { printTypeParameter, printTypeParameters } from "./type-parameters.js"; @@ -252,6 +253,9 @@ function printFlow(path, options, print) { ")", ]; + case "TypePredicate": + return printTypePredicate(path, print); + case "TypeParameterDeclaration": case "TypeParameterInstantiation": return printTypeParameters(path, options, print, "params"); diff --git a/src/language-js/print/type-annotation.js b/src/language-js/print/type-annotation.js index 55b70c34c..9553371d1 100644 --- a/src/language-js/print/type-annotation.js +++ b/src/language-js/print/type-annotation.js @@ -529,6 +529,17 @@ function printTypeQuery({ node }, print) { ]; } +function printTypePredicate(path, print) { + const { node } = path; + return [ + node.asserts ? "asserts " : "", + print("parameterName"), + node.typeAnnotation + ? [" is ", printTypeAnnotationProperty(path, print)] + : "", + ]; +} + export { printOpaqueType, printTypeAlias, @@ -545,4 +556,5 @@ export { printTypeAnnotation, printArrayType, printTypeQuery, + printTypePredicate, }; diff --git a/src/language-js/print/typescript.js b/src/language-js/print/typescript.js index 0f5367df1..90b970c61 100644 --- a/src/language-js/print/typescript.js +++ b/src/language-js/print/typescript.js @@ -51,6 +51,7 @@ import { printTypeAnnotationProperty, printArrayType, printTypeQuery, + printTypePredicate, } from "./type-annotation.js"; import { printEnumDeclaration, printEnumMember } from "./enum.js"; import { printImportKind } from "./module.js"; @@ -211,13 +212,7 @@ function printTypescript(path, options, print) { ]; } case "TSTypePredicate": - return [ - node.asserts ? "asserts " : "", - print("parameterName"), - node.typeAnnotation - ? [" is ", printTypeAnnotationProperty(path, print)] - : "", - ]; + return printTypePredicate(path, print); case "TSNonNullExpression": return [print("expression"), "!"]; case "TSImportType": diff --git a/src/language-js/traverse/visitor-keys.evaluate.js b/src/language-js/traverse/visitor-keys.evaluate.js index 3ef3a9034..3c079f7d7 100644 --- a/src/language-js/traverse/visitor-keys.evaluate.js +++ b/src/language-js/traverse/visitor-keys.evaluate.js @@ -49,6 +49,7 @@ const additionalVisitorKeys = { TupleTypeAnnotation: ["elementTypes"], TupleTypeSpreadElement: ["label", "typeAnnotation"], TupleTypeLabeledElement: ["label", "elementType", "variance"], + TypePredicate: ["parameterName", "typeAnnotation", "asserts"], NeverTypeAnnotation: [], UndefinedTypeAnnotation: [], UnknownTypeAnnotation: [], diff --git a/tests/format/flow-repo/type-guards/__snapshots__/jsfmt.spec.js.snap b/tests/format/flow-repo/type-guards/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..d16099ed4 --- /dev/null +++ b/tests/format/flow-repo/type-guards/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`comments-in-type-annotations.js [babel-flow] format 1`] = ` +"Unexpected token, expected "{" (3:51) + 1 | type F = (x: mixed) /* C1 */ => /* C2 */ x /* C3 */ is /* C4 */ number; + 2 | +> 3 | function f(x: any) /* C1 */ : /* C2 */ x /* C3 */ is /* C4 */ number /* C5 */ { return true; } + | ^ + 4 | + 5 | const arrow = (x: any): /* C1 */ x is (number /* C2 */ => /* C3 */ x /* C4 */ is /* C5 */ string /* C6 */ => /* C7 */ x /* C8 */ is /* C9 */ boolean) => true; + 6 |" +`; + +exports[`comments-in-type-annotations.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +type F = (x: mixed) /* C1 */ => /* C2 */ x /* C3 */ is /* C4 */ number; + +function f(x: any) /* C1 */ : /* C2 */ x /* C3 */ is /* C4 */ number /* C5 */ { return true; } + +const arrow = (x: any): /* C1 */ x is (number /* C2 */ => /* C3 */ x /* C4 */ is /* C5 */ string /* C6 */ => /* C7 */ x /* C8 */ is /* C9 */ boolean) => true; + +=====================================output===================================== +type F = (x: mixed /* C1 */) => /* C2 */ x /* C3 */ is /* C4 */ number; + +function f(x: any) /* C1 */ : /* C2 */ x /* C3 */ is /* C4 */ number /* C5 */ { + return true; +} + +const arrow = ( + x: any, +): /* C1 */ x is (( + number /* C2 */, +) => /* C3 */ x /* C4 */ is /* C5 */ ( + string /* C6 */, +) => /* C7 */ x /* C8 */ is /* C9 */ boolean) => true; + +================================================================================ +`; + +exports[`passing.js [babel-flow] format 1`] = ` +"Unexpected token, expected "{" (5:20) + 3 | type F = (x: mixed) => x is A; + 4 | +> 5 | function f(any): x is A { return true; } + | ^ + 6 | + 7 | const arrow0 = (x: any): x is A => true; + 8 | const arrow1 = (x: any): x is (y: A) => B => true;" +`; + +exports[`passing.js format 1`] = ` +====================================options===================================== +parsers: ["flow"] +printWidth: 80 + | printWidth +=====================================input====================================== +// @flow + +type F = (x: mixed) => x is A; + +function f(any): x is A { return true; } + +const arrow0 = (x: any): x is A => true; +const arrow1 = (x: any): x is (y: A) => B => true; +const arrow3 = (x: any): x is (y: A) => y is B => true; + +const needs_parens_1 = (x: any): x is (A => B) => true; +const needs_parens_2 = (x: any): x is ((A) => B) => true; +const needs_parens_3 = (x: any): x is (A => x is B) => true; +const needs_parens_4 = (x: any): x is (A => x is B => x is C) => true; + +const needs_parens_5 = (x: any): x is (y: A) => (B => C) => true; +const needs_parens_6 = (x: any): x is (y: A) => y is (B => C) => true; + +class C { + m(): this is D {} + f = (): this is D => {} +} + +function asserts_1(x: any): asserts x {} +function asserts_2(x: any): asserts x is A {} + +=====================================output===================================== +// @flow + +type F = (x: mixed) => x is A; + +function f(any): x is A { + return true; +} + +const arrow0 = (x: any): x is A => true; +const arrow1 = (x: any): x is ((y: A) => B) => true; +const arrow3 = (x: any): x is ((y: A) => y is B) => true; + +const needs_parens_1 = (x: any): x is ((A) => B) => true; +const needs_parens_2 = (x: any): x is ((A) => B) => true; +const needs_parens_3 = (x: any): x is ((A) => x is B) => true; +const needs_parens_4 = (x: any): x is ((A) => x is (B) => x is C) => true; + +const needs_parens_5 = (x: any): x is ((y: A) => (B) => C) => true; +const needs_parens_6 = (x: any): x is ((y: A) => y is (B) => C) => true; + +class C { + m(): this is D {} + f = (): this is D => {}; +} + +function asserts_1(x: any): asserts x {} +function asserts_2(x: any): asserts x is A {} + +================================================================================ +`; diff --git a/tests/format/flow-repo/type-guards/comments-in-type-annotations.js b/tests/format/flow-repo/type-guards/comments-in-type-annotations.js new file mode 100644 index 000000000..ee2a15d38 --- /dev/null +++ b/tests/format/flow-repo/type-guards/comments-in-type-annotations.js @@ -0,0 +1,5 @@ +type F = (x: mixed) /* C1 */ => /* C2 */ x /* C3 */ is /* C4 */ number; + +function f(x: any) /* C1 */ : /* C2 */ x /* C3 */ is /* C4 */ number /* C5 */ { return true; } + +const arrow = (x: any): /* C1 */ x is (number /* C2 */ => /* C3 */ x /* C4 */ is /* C5 */ string /* C6 */ => /* C7 */ x /* C8 */ is /* C9 */ boolean) => true; diff --git a/tests/format/flow-repo/type-guards/jsfmt.spec.js b/tests/format/flow-repo/type-guards/jsfmt.spec.js new file mode 100644 index 000000000..43cc8b318 --- /dev/null +++ b/tests/format/flow-repo/type-guards/jsfmt.spec.js @@ -0,0 +1,5 @@ +run_spec(import.meta, ["flow"], { + errors: { + "babel-flow": ["passing.js", "comments-in-type-annotations.js"], + }, +}); diff --git a/tests/format/flow-repo/type-guards/passing.js b/tests/format/flow-repo/type-guards/passing.js new file mode 100644 index 000000000..377287554 --- /dev/null +++ b/tests/format/flow-repo/type-guards/passing.js @@ -0,0 +1,25 @@ +// @flow + +type F = (x: mixed) => x is A; + +function f(any): x is A { return true; } + +const arrow0 = (x: any): x is A => true; +const arrow1 = (x: any): x is (y: A) => B => true; +const arrow3 = (x: any): x is (y: A) => y is B => true; + +const needs_parens_1 = (x: any): x is (A => B) => true; +const needs_parens_2 = (x: any): x is ((A) => B) => true; +const needs_parens_3 = (x: any): x is (A => x is B) => true; +const needs_parens_4 = (x: any): x is (A => x is B => x is C) => true; + +const needs_parens_5 = (x: any): x is (y: A) => (B => C) => true; +const needs_parens_6 = (x: any): x is (y: A) => y is (B => C) => true; + +class C { + m(): this is D {} + f = (): this is D => {} +} + +function asserts_1(x: any): asserts x {} +function asserts_2(x: any): asserts x is A {} diff --git a/tests/unit/__snapshots__/visitor-keys.js.snap b/tests/unit/__snapshots__/visitor-keys.js.snap index 2652e35e0..54bb985ae 100644 --- a/tests/unit/__snapshots__/visitor-keys.js.snap +++ b/tests/unit/__snapshots__/visitor-keys.js.snap @@ -1010,6 +1010,11 @@ exports[`visitor keys estree 1`] = ` "TypeParameterInstantiation": [ "params", ], + "TypePredicate": [ + "parameterName", + "typeAnnotation", + "asserts", + ], "TypeofTypeAnnotation": [ "argument", ], diff --git a/yarn.lock b/yarn.lock index 3c8e22a94..f1f78b6e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3989,10 +3989,10 @@ __metadata: languageName: node linkType: hard -"flow-parser@npm:0.204.0": - version: 0.204.0 - resolution: "flow-parser@npm:0.204.0" - checksum: 295a85bbb3e9f6f46766d751ab9af9977b278b3a41648a4f1a5a8e123b5388822fd120bb16b2ee68a43199b309e0472732f1f2ff04cc841d4472aab3a67d8848 +"flow-parser@npm:0.205.0": + version: 0.205.0 + resolution: "flow-parser@npm:0.205.0" + checksum: 84e79867128c414a332e506b43d436baca15eea86100fcc9fb0791dabefd02e9ba72feb1706262378950a10fd97340d8f30363b4fe798ae6c86d74bb255ee0b6 languageName: node linkType: hard @@ -6762,7 +6762,7 @@ __metadata: file-entry-cache: 6.0.1 find-cache-dir: 4.0.0 find-parent-dir: 0.3.1 - flow-parser: 0.204.0 + flow-parser: 0.205.0 get-stdin: 9.0.0 graphql: 16.6.0 hermes-parser: 0.10.1