Skip to content

Commit 08b0ed8

Browse files
committed
parens: Emit parens where needed throughout Flow types; add tests
There were a couple of particular cases that were already covered, but many others that were not. I ran into one of them in practice (`A & (B | C)` was getting printed as `A & B | C`, which means something different) and looked into it, and decided to go about just fixing this whole class of issues. I started by scanning through all the different kinds of FlowType node, looking for any that could need parens around them; then I wrote up a comprehensive list of test cases. Once that was done, the whole thing turned out to be doable with a pretty reasonable amount of code! And given the tests, I'm hopeful that this logic is pretty much complete. Some of the added tests are skipped for now, because they involve function types and those currently have other bugs that cause those test cases to break. They start passing when this branch is merged with benjamn#1089, which fixes those other bugs.
1 parent 8a33061 commit 08b0ed8

File tree

2 files changed

+182
-8
lines changed

2 files changed

+182
-8
lines changed

lib/fast-path.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,11 +532,81 @@ FPp.needsParens = function (assumeExpressionContext) {
532532
// parentheses explicitly in the AST, with TSParenthesizedType.)
533533

534534
case "OptionalIndexedAccessType":
535-
return parent.type === "IndexedAccessType";
535+
switch (parent.type) {
536+
case "IndexedAccessType":
537+
// `(O?.['x'])['y']` is distinct from `O?.['x']['y']`.
538+
return name === "objectType" && parent.objectType === node;
539+
default:
540+
return false;
541+
}
542+
543+
case "IndexedAccessType":
544+
case "ArrayTypeAnnotation":
545+
return false;
546+
547+
case "NullableTypeAnnotation":
548+
switch (parent.type) {
549+
case "OptionalIndexedAccessType":
550+
case "IndexedAccessType":
551+
return name === "objectType" && parent.objectType === node;
552+
case "ArrayTypeAnnotation":
553+
return true;
554+
default:
555+
return false;
556+
}
536557

537558
case "IntersectionTypeAnnotation":
559+
switch (parent.type) {
560+
case "OptionalIndexedAccessType":
561+
case "IndexedAccessType":
562+
return name === "objectType" && parent.objectType === node;
563+
case "ArrayTypeAnnotation":
564+
case "NullableTypeAnnotation":
565+
return true;
566+
default:
567+
return false;
568+
}
569+
538570
case "UnionTypeAnnotation":
539-
return parent.type === "NullableTypeAnnotation";
571+
switch (parent.type) {
572+
case "OptionalIndexedAccessType":
573+
case "IndexedAccessType":
574+
return name === "objectType" && parent.objectType === node;
575+
case "ArrayTypeAnnotation":
576+
case "NullableTypeAnnotation":
577+
case "IntersectionTypeAnnotation":
578+
return true;
579+
default:
580+
return false;
581+
}
582+
583+
case "FunctionTypeAnnotation":
584+
switch (parent.type) {
585+
case "OptionalIndexedAccessType":
586+
case "IndexedAccessType":
587+
return name === "objectType" && parent.objectType === node;
588+
589+
case "ArrayTypeAnnotation":
590+
// We need parens.
591+
592+
// fallthrough
593+
case "NullableTypeAnnotation":
594+
// We don't *need* any parens here… unless some ancestor
595+
// means we do, by putting a `&` or `|` on the right.
596+
// Just use parens; probably more readable that way anyway.
597+
// (FWIW, this agrees with Prettier's behavior.)
598+
599+
// fallthrough
600+
case "IntersectionTypeAnnotation":
601+
case "UnionTypeAnnotation":
602+
// We need parens if there's another `&` or `|` after this node.
603+
// For consistency, just always use parens.
604+
// (FWIW, this agrees with Prettier's behavior.)
605+
return true;
606+
607+
default:
608+
return false;
609+
}
540610
}
541611

542612
if (

test/flow.ts

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ describe("type syntax", function () {
1414
parser: require("flow-parser"),
1515
};
1616

17+
function checkEquiv(a: string, b: string) {
18+
const aAst = parse(a, flowParserParseOptions);
19+
const bAst = parse(b, flowParserParseOptions);
20+
types.astNodesAreEquivalent.assert(aAst, bAst);
21+
}
22+
1723
function check(source: string, parseOptions?: any) {
1824
parseOptions = parseOptions || flowParserParseOptions;
1925
const ast1 = parse(source, parseOptions);
@@ -212,12 +218,6 @@ describe("type syntax", function () {
212218
check("type H = (Obj?.['bar'])[string][];");
213219
check("type I = Obj?.['bar']?.[string][];");
214220

215-
function checkEquiv(a: string, b: string) {
216-
const aAst = parse(a, flowParserParseOptions);
217-
const bAst = parse(b, flowParserParseOptions);
218-
types.astNodesAreEquivalent.assert(aAst, bAst);
219-
}
220-
221221
// Since FastPath#needsParens does not currently add any parentheses to
222222
// these expressions, make sure they do not matter for parsing the AST.
223223

@@ -231,4 +231,108 @@ describe("type syntax", function () {
231231
"type F = Obj['bar']?.[string][];",
232232
);
233233
});
234+
235+
it("parenthesizes correctly", () => {
236+
// The basic binary operators `&` and `|`.
237+
// `&` binds tighter than `|`
238+
check("type Num = number & (empty | mixed);"); // parens needed
239+
check("type Num = number | empty & mixed;"); // equivalent to `…|(…&…)`
240+
241+
// Unary suffix `[]`, with the above.
242+
// `[]` binds tighter than `&` or `|`
243+
check("type T = (number | string)[];");
244+
check("type T = number | string[];"); // a union
245+
check("type T = (number & mixed)[];");
246+
check("type T = number & mixed[];"); // an intersection
247+
248+
// Unary prefix `?`, with the above.
249+
// `?` binds tighter than `&` or `|`
250+
check("type T = ?(A & B);");
251+
check("type T = ?(A | B);");
252+
// `?` binds less tightly than `[]`
253+
check("type T = (?number)[];"); // array of nullable
254+
check("type T = ?number[];"); // nullable of array
255+
256+
// (Optional) indexed-access types, with the above.
257+
// `[…]` and `?.[…]` bind (their left) tighter than either `&` or `|`
258+
check("type T = (O & P)['x'];");
259+
check("type T = (O | P)['x'];");
260+
check("type T = (O & P)?.['x'];");
261+
check("type T = (O | P)?.['x'];");
262+
// `[…]` and `?.[…]` bind (their left) tighter than `?`
263+
check("type T = (?O)['x'];"); // indexed-access of nullable
264+
check("type T = ?O['x'];"); // nullable of indexed-access
265+
check("type T = (?O)?.['x'];"); // optional-indexed-access of nullable
266+
check("type T = ?O?.['x'];"); // nullable of optional-indexed-access
267+
// `[…]` and `?.[…]` provide brackets on their right, so skip parens:
268+
check("type T = A[B & C];");
269+
check("type T = A[B | C];");
270+
check("type T = A[?B];");
271+
check("type T = A[B[]];");
272+
check("type T = A[B[C]];");
273+
check("type T = A[B?.[C]];");
274+
check("type T = A?.[B & C];");
275+
check("type T = A?.[B | C];");
276+
check("type T = A?.[?B];");
277+
check("type T = A?.[B[]];");
278+
check("type T = A?.[B[C]];");
279+
check("type T = A?.[B?.[C]];");
280+
// `[…]` and `?.[…]` interact in a nonobvious way:
281+
// OptionalIndexedAccessType inside IndexedAccessType.
282+
check("type T = (O?.['x']['y'])['z'];"); // indexed of optional-indexed
283+
check("type T = O?.['x']['y']['z'];"); // optional-indexed throughout
284+
285+
return;
286+
// Skip test cases involving function types, because those are currently
287+
// broken in other ways. Those will be fixed by:
288+
// https://github.com/benjamn/recast/pull/1089
289+
290+
// Function types.
291+
// Function binds less tightly than binary operators at right:
292+
check("type T = (() => number) & O;"); // an intersection
293+
check("type T = (() => number) | void;"); // a union
294+
check("type T = () => number | void;"); // a function
295+
check("type T = (() => void)['x'];");
296+
check("type T = () => void['x'];"); // a function
297+
check("type T = (() => void)?.['x'];");
298+
check("type T = () => void?.['x'];"); // a function
299+
// … and less tightly than suffix operator:
300+
check("type T = (() => void)[];"); // an array
301+
check("type T = () => void[];"); // a function
302+
303+
// Function does bind tighter than prefix operator (how could it not?)
304+
checkEquiv("type T = ?() => void;", "type T = ?(() => void);");
305+
// … and tighter than `&` or `|` at left (ditto):
306+
checkEquiv("type T = A | () => void;", "type T = A | (() => void);");
307+
checkEquiv("type T = A & () => void;", "type T = A & (() => void);");
308+
// … but we choose to insert parens anyway:
309+
check("type T = ?(() => void);");
310+
check("type T = A | (() => void);");
311+
check("type T = A & (() => void);");
312+
// We don't insert parens for the *right* operand of indexed access,
313+
// though, that'd be silly (sillier than writing such a type at all?):
314+
check("type T = A[() => void];");
315+
check("type T = A?.[() => void];");
316+
317+
// Here's one reason we insert those parens we don't strictly have to:
318+
// Even when the parent is something at left so that function binds
319+
// tighter than it, *its* parent (or further ancestor) might be
320+
// something at right that binds tighter than function.
321+
// E.g., union of nullable of function:
322+
check("type T = ?(() => void) | A;");
323+
checkEquiv("type T = ?() => void | A;", "type T = ?() => (void | A);");
324+
// … or intersection of nullable of function:
325+
check("type T = ?(() => void) & A;");
326+
checkEquiv("type T = ?() => void & A;", "type T = ?() => (void & A);");
327+
// … or array or (optional-)indexed-access of nullable of function:
328+
check("type T = ?(() => void)[];");
329+
check("type T = ?(() => void)['x'];");
330+
check("type T = ?(() => void)?.['x'];");
331+
// … or union of intersection:
332+
check("type T = A & (() => void) | B;");
333+
// Or for an example beyond the grandparent: union of cubic nullable:
334+
check("type T = ???(() => void) | B;");
335+
// … or union of intersection of nullable:
336+
check("type T = A & ?(() => void) | B;");
337+
});
234338
});

0 commit comments

Comments
 (0)