Skip to content

Commit

Permalink
Support type guards in Flow
Browse files Browse the repository at this point in the history
  • Loading branch information
panagosg7 committed Apr 27, 2023
1 parent 1107ae6 commit 826f356
Show file tree
Hide file tree
Showing 11 changed files with 209 additions and 6 deletions.
15 changes: 15 additions & 0 deletions changelog_unreleased/flow/14767.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#### Support type guards (#14767 by @panagosg7)

<!-- prettier-ignore -->
```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';
}
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
21 changes: 21 additions & 0 deletions src/language-js/needs-parens.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
9 changes: 9 additions & 0 deletions src/language-js/print/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,15 @@ function printFlow(path, options, print) {
")",
];

case "TypePredicate":
return [
node.asserts ? "asserts " : "",
print("parameterName"),
node.typeAnnotation
? [" is ", printTypeAnnotationProperty(path, print)]
: "",
];

case "TypeParameterDeclaration":
case "TypeParameterInstantiation":
return printTypeParameters(path, options, print, "params");
Expand Down
1 change: 1 addition & 0 deletions src/language-js/traverse/visitor-keys.evaluate.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const additionalVisitorKeys = {
TupleTypeAnnotation: ["elementTypes"],
TupleTypeSpreadElement: ["label", "typeAnnotation"],
TupleTypeLabeledElement: ["label", "elementType", "variance"],
TypePredicate: ["parameterName", "typeAnnotation", "asserts"],
NeverTypeAnnotation: [],
UndefinedTypeAnnotation: [],
UnknownTypeAnnotation: [],
Expand Down
117 changes: 117 additions & 0 deletions tests/format/flow-repo/type-guards/__snapshots__/jsfmt.spec.js.snap
Original file line number Diff line number Diff line change
@@ -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 {}
================================================================================
`;
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 5 additions & 0 deletions tests/format/flow-repo/type-guards/jsfmt.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
run_spec(import.meta, ["flow"], {
errors: {
"babel-flow": ["passing.js", "comments-in-type-annotations.js"],
},
});
25 changes: 25 additions & 0 deletions tests/format/flow-repo/type-guards/passing.js
Original file line number Diff line number Diff line change
@@ -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 {}
5 changes: 5 additions & 0 deletions tests/unit/__snapshots__/visitor-keys.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,11 @@ exports[`visitor keys estree 1`] = `
"TypeParameterInstantiation": [
"params",
],
"TypePredicate": [
"parameterName",
"typeAnnotation",
"asserts",
],
"TypeofTypeAnnotation": [
"argument",
],
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 826f356

Please sign in to comment.