From 083017535bd109c317e19972522987307c28ea6b Mon Sep 17 00:00:00 2001 From: Alan Pierce Date: Sun, 17 Oct 2021 22:06:04 -0700 Subject: [PATCH] Properly remove `!` from definite assignment assertions Fixes #639 Previously, the `!` operator for class field declarations was not treated as a type token, so it wasn't automatically removed at transpile. In most cases, this wasn't relevant because the class field transform removes uninitialized fields completely. However, there are two cases where it causes an issue: * `disableESTransforms: true`, which disables the class field transform. * Private fields, which are skipped by the class field transform. In both cases, we can fix the issue by just setting the `!` as a type token so that it will naturally get removed by the TS transformer. I also did a little refactoring to pull out the logic for handling individual type tokens. --- src/parser/plugins/types.ts | 8 ++---- src/parser/tokenizer/index.ts | 7 +++++ src/parser/traverser/statement.ts | 3 +- test/typescript-test.ts | 47 +++++++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/parser/plugins/types.ts b/src/parser/plugins/types.ts index 935c289a..4778fc31 100644 --- a/src/parser/plugins/types.ts +++ b/src/parser/plugins/types.ts @@ -1,6 +1,6 @@ -import {eat, lookaheadType, match} from "../tokenizer/index"; +import {eatTypeToken, lookaheadType, match} from "../tokenizer/index"; import {TokenType as tt} from "../tokenizer/types"; -import {isFlowEnabled, isTypeScriptEnabled, state} from "../traverser/base"; +import {isFlowEnabled, isTypeScriptEnabled} from "../traverser/base"; import {baseParseConditional} from "../traverser/expression"; import {flowParseTypeAnnotation} from "./flow"; import {tsParseTypeAnnotation} from "./typescript"; @@ -26,9 +26,7 @@ export function typedParseConditional(noIn: boolean): void { // Note: These "type casts" are *not* valid TS expressions. // But we parse them here and change them when completing the arrow function. export function typedParseParenItem(): void { - if (eat(tt.question)) { - state.tokens[state.tokens.length - 1].isType = true; - } + eatTypeToken(tt.question); if (match(tt.colon)) { if (isTypeScriptEnabled) { tsParseTypeAnnotation(); diff --git a/src/parser/tokenizer/index.ts b/src/parser/tokenizer/index.ts index 74efa955..63bdc423 100644 --- a/src/parser/tokenizer/index.ts +++ b/src/parser/tokenizer/index.ts @@ -189,6 +189,13 @@ export function eat(type: TokenType): boolean { } } +export function eatTypeToken(tokenType: TokenType): void { + const oldIsType = state.isType; + state.isType = true; + eat(tokenType); + state.isType = oldIsType; +} + export function match(type: TokenType): boolean { return state.type === type; } diff --git a/src/parser/traverser/statement.ts b/src/parser/traverser/statement.ts index ebe939f0..4b7adacb 100644 --- a/src/parser/traverser/statement.ts +++ b/src/parser/traverser/statement.ts @@ -36,6 +36,7 @@ import { } from "../plugins/typescript"; import { eat, + eatTypeToken, IdentifierRole, lookaheadType, lookaheadTypeAndKeyword, @@ -819,7 +820,7 @@ export function parsePostMemberNameModifiers(): void { export function parseClassProperty(): void { if (isTypeScriptEnabled) { - eat(tt.bang); + eatTypeToken(tt.bang); tsTryParseTypeAnnotation(); } else if (isFlowEnabled) { if (match(tt.colon)) { diff --git a/test/typescript-test.ts b/test/typescript-test.ts index e4d4ccfc..087b2797 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1074,6 +1074,22 @@ describe("typescript transform", () => { ); }); + it("handles definite assignment assertions in classes with disableESTransforms", () => { + assertTypeScriptResult( + ` + class A { + foo!: number; + } + `, + `"use strict"; + class A { + foo; + } + `, + {disableESTransforms: true}, + ); + }); + it("handles definite assignment assertions on variables", () => { assertTypeScriptResult( ` @@ -1089,6 +1105,37 @@ describe("typescript transform", () => { ); }); + it("handles definite assignment assertions on private fields in classes", () => { + assertTypeScriptResult( + ` + class A { + #a!: number; + } + `, + `"use strict"; + class A { + #a; + } + `, + ); + }); + + it("handles definite assignment assertions on private fields in classes with disableESTransforms", () => { + assertTypeScriptResult( + ` + class A { + #a!: number; + } + `, + `"use strict"; + class A { + #a; + } + `, + {disableESTransforms: true}, + ); + }); + it("handles mapped type modifiers", () => { assertTypeScriptResult( `