From 473a09ecce52cfd40ed1a7481b7be470c48a6054 Mon Sep 17 00:00:00 2001 From: Alan Pierce Date: Sun, 27 Nov 2022 22:02:47 -0800 Subject: [PATCH] Fix parsing of << within a type (#769) Fixes #758 There was already some logic from #716 to handle cases like `f<() => void>`, where we're transitioning from a non-type context to a type context and need to detect if `<<` is actually two open-type-parameter/argument tokens. There was a simpler missing case, though, which is that `<<` is never allowed within a type, so we should tokenize as a simple `<` in a type context. To handle the various cases correctly, this extra logic needs to only happen for `<<`, and this PR also adds some additional comments explaining the nuances. --- src/parser/plugins/typescript.ts | 4 +--- src/parser/tokenizer/index.ts | 33 ++++++++++++++++++++++++++------ test/flow-test.ts | 11 +++++++++++ test/typescript-test.ts | 27 +++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/parser/plugins/typescript.ts b/src/parser/plugins/typescript.ts index cf57085b..0a9eabfc 100644 --- a/src/parser/plugins/typescript.ts +++ b/src/parser/plugins/typescript.ts @@ -1138,9 +1138,7 @@ function tsTryParseGenericAsyncArrowFunction(): boolean { * where bitshift would be illegal anyway (e.g. in a class "extends" clause). * * This hack is useful to handle situations like foo<() => void>() where - * there can legitimately be two open-angle-brackets in a row in TS. This - * situation is very obscure and (as of this writing) is handled by Babel but - * not TypeScript itself, so it may be fine in the future to remove this case. + * there can legitimately be two open-angle-brackets in a row in TS. */ function tsParseTypeArgumentsWithPossibleBitshift(): void { if (state.type === tt.bitShiftL) { diff --git a/src/parser/tokenizer/index.ts b/src/parser/tokenizer/index.ts index 316f7828..953fd7a5 100644 --- a/src/parser/tokenizer/index.ts +++ b/src/parser/tokenizer/index.ts @@ -512,11 +512,25 @@ function readToken_lt(): void { finishOp(tt.assign, 3); return; } - // This still might be two instances of <, e.g. the TS type argument - // expression f<() => void>() , but parse as left shift for now and we'll - // retokenize if necessary. We can't use isType for this case because we - // don't know yet if we're in a type. - finishOp(tt.bitShiftL, 2); + // We see <<, but need to be really careful about whether to treat it as a + // true left-shift or as two < tokens. + if (state.isType) { + // Within a type, << might come up in a snippet like `Array<() => void>`, + // so treat it as two < tokens. Importantly, this should only override << + // rather than other tokens like <= . If we treated <= as < in a type + // context, then the snippet `a as T <= 1` would incorrectly start parsing + // a type argument on T. We don't need to worry about `a as T << 1` + // because TypeScript disallows that syntax. + finishOp(tt.lessThan, 1); + } else { + // Outside a type, this might be a true left-shift operator, or it might + // still be two open-type-arg tokens, such as in `f<() => void>()`. We + // look at the token while considering the `f`, so we don't yet know that + // we're in a type context. In this case, we initially tokenize as a + // left-shift and correct after-the-fact as necessary in + // tsParseTypeArgumentsWithPossibleBitshift . + finishOp(tt.bitShiftL, 2); + } return; } @@ -558,7 +572,14 @@ function readToken_gt(): void { /** * Called after `as` expressions in TS; we're switching from a type to a - * non-type context, so a > token may actually be >= . + * non-type context, so a > token may actually be >= . This is needed because >= + * must be tokenized as a > in a type context because of code like + * `const x: Array=[];`, but `a as T >= 1` is a code example where it must be + * treated as >=. + * + * Notably, this only applies to >, not <. In a code snippet like `a as T <= 1`, + * we must NOT tokenize as <, or else the type parser will start parsing a type + * argument and fail. */ export function rescan_gt(): void { if (state.type === tt.greaterThan) { diff --git a/test/flow-test.ts b/test/flow-test.ts index b919f2d8..e005af78 100644 --- a/test/flow-test.ts +++ b/test/flow-test.ts @@ -699,4 +699,15 @@ describe("transform flow", () => { `, ); }); + + it("properly parses flow type args that look like left shift", () => { + assertFlowResult( + ` + type A = B<() => void>; + `, + `"use strict"; + + `, + ); + }); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index 1d3f2b3f..7d661143 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -2500,6 +2500,19 @@ describe("typescript transform", () => { ); }); + it("properly handles <= after `as` and `satisfies`", () => { + assertTypeScriptResult( + ` + if (x as number <= 5) {} + if (x satisfies number <= 5) {} + `, + `"use strict"; + if (x <= 5) {} + if (x <= 5) {} + `, + ); + }); + it("handles simple template literal interpolations in types", () => { assertTypeScriptResult( ` @@ -3161,7 +3174,7 @@ describe("typescript transform", () => { ); }); - it("properly parses TS angle brackets that look like left shift", () => { + it("properly parses TS function type args that look like left shift", () => { assertResult( ` f<(value: T) => void>(g); @@ -3173,6 +3186,18 @@ describe("typescript transform", () => { ); }); + it("properly parses TS type args that look like left shift", () => { + assertResult( + ` + type A = B<() => void>; + `, + ` + + `, + {transforms: ["typescript"]}, + ); + }); + it("properly parses actual left shift", () => { assertResult( `