diff --git a/src/parser/plugins/typescript.ts b/src/parser/plugins/typescript.ts index bfc274c7..edf42389 100644 --- a/src/parser/plugins/typescript.ts +++ b/src/parser/plugins/typescript.ts @@ -4,6 +4,7 @@ import { lookaheadTypeAndKeyword, match, next, + nextTemplateToken, popTypeContext, pushTypeContext, } from "../tokenizer/index"; @@ -59,6 +60,17 @@ function tsIsIdentifier(): boolean { return match(tt.name); } +function isLiteralPropertyName(): boolean { + return ( + match(tt.name) || + Boolean(state.type & TokenType.IS_KEYWORD) || + match(tt.string) || + match(tt.num) || + match(tt.bigint) || + match(tt.decimal) + ); +} + function tsNextTokenCanFollowModifier(): boolean { // Note: TypeScript's implementation is much more complicated because // more things are considered modifiers there. @@ -68,13 +80,13 @@ function tsNextTokenCanFollowModifier(): boolean { next(); const canFollowModifier = - !hasPrecedingLineBreak() && - !match(tt.parenL) && - !match(tt.parenR) && - !match(tt.colon) && - !match(tt.eq) && - !match(tt.question) && - !match(tt.bang); + (match(tt.bracketL) || + match(tt.braceL) || + match(tt.star) || + match(tt.ellipsis) || + match(tt.hash) || + isLiteralPropertyName()) && + !hasPrecedingLineBreak(); if (canFollowModifier) { return true; @@ -352,6 +364,9 @@ function tsParseMappedType(): void { } expect(tt.bracketL); tsParseMappedTypeParameter(); + if (eatContextual(ContextualKeyword._as)) { + tsParseType(); + } expect(tt.bracketR); if (match(tt.plus) || match(tt.minus)) { next(); @@ -396,6 +411,22 @@ function tsParseParenthesizedType(): void { expect(tt.parenR); } +function tsParseTemplateLiteralType(): void { + // Finish `, read quasi + nextTemplateToken(); + // Finish quasi, read ${ + nextTemplateToken(); + while (!match(tt.backQuote) && !state.error) { + expect(tt.dollarBraceL); + tsParseType(); + // Finish }, read quasi + nextTemplateToken(); + // Finish quasi, read either ${ or ` + nextTemplateToken(); + } + next(); +} + enum FunctionType { TSFunctionType, TSConstructorType, @@ -456,7 +487,7 @@ function tsParseNonArrayType(): void { tsParseParenthesizedType(); return; case tt.backQuote: - parseTemplate(); + tsParseTemplateLiteralType(); return; default: if (state.type & TokenType.IS_KEYWORD) { diff --git a/src/parser/tokenizer/index.ts b/src/parser/tokenizer/index.ts index 1ccce962..b6ed8177 100644 --- a/src/parser/tokenizer/index.ts +++ b/src/parser/tokenizer/index.ts @@ -476,11 +476,6 @@ function readToken_plus_min(code: number): void { // '<>' function readToken_lt_gt(code: number): void { - // Avoid right-shift for things like Array>. - if (code === charCodes.greaterThan && state.isType) { - finishOp(tt.greaterThan, 1); - return; - } const nextChar = input.charCodeAt(state.pos + 1); if (nextChar === code) { @@ -492,6 +487,11 @@ function readToken_lt_gt(code: number): void { finishOp(tt.assign, size + 1); return; } + // Avoid right-shift for things like Array>. + if (code === charCodes.greaterThan && state.isType) { + finishOp(tt.greaterThan, 1); + return; + } finishOp(tt.bitShift, size); return; } diff --git a/src/parser/traverser/expression.ts b/src/parser/traverser/expression.ts index 9421316e..9231035b 100644 --- a/src/parser/traverser/expression.ts +++ b/src/parser/traverser/expression.ts @@ -521,7 +521,7 @@ export function parseExprAtom(): boolean { case tt._do: { next(); - parseBlock(false); + parseBlock(); return false; } @@ -933,7 +933,7 @@ export function parseFunctionBody(allowExpression: boolean, funcContextId: numbe if (isExpression) { parseMaybeAssign(); } else { - parseBlock(true /* allowDirectives */, true /* isFunctionScope */, funcContextId); + parseBlock(true /* isFunctionScope */, funcContextId); } } diff --git a/src/parser/traverser/statement.ts b/src/parser/traverser/statement.ts index 46386a2a..214ea8f4 100644 --- a/src/parser/traverser/statement.ts +++ b/src/parser/traverser/statement.ts @@ -481,15 +481,8 @@ function parseIdentifierStatement(contextualKeyword: ContextualKeyword): void { } } -// Parse a semicolon-enclosed block of statements, handling `"use -// strict"` declarations when `allowStrict` is true (used for -// function bodies). - -export function parseBlock( - allowDirectives: boolean = false, - isFunctionScope: boolean = false, - contextId: number = 0, -): void { +// Parse a semicolon-enclosed block of statements. +export function parseBlock(isFunctionScope: boolean = false, contextId: number = 0): void { const startTokenIndex = state.tokens.length; state.scopeDepth++; expect(tt.braceL); @@ -716,6 +709,14 @@ function parseClassMember(memberStart: number, classContextId: number): void { // otherwise something static state.tokens[state.tokens.length - 1].type = tt._static; isStatic = true; + + if (match(tt.braceL)) { + // This is a static block. Mark the word "static" with the class context ID for class element + // detection and parse as a regular block. + state.tokens[state.tokens.length - 1].contextId = classContextId; + parseBlock(); + return; + } } parseClassMemberWithIsStatic(memberStart, isStatic, classContextId); diff --git a/src/util/getClassInfo.ts b/src/util/getClassInfo.ts index 412006a5..59799bdc 100644 --- a/src/util/getClassInfo.ts +++ b/src/util/getClassInfo.ts @@ -92,12 +92,17 @@ export default function getClassInfo( ({constructorInitializerStatements, constructorInsertPos} = processConstructor(tokens)); continue; } + const isStaticBlock = tokens.matches1(tt.braceL); + const nameStartIndex = tokens.currentIndex(); - skipFieldName(tokens); - if (tokens.matches1(tt.lessThan) || tokens.matches1(tt.parenL)) { - // This is a method, so just skip to the next method/field. To do that, we seek forward to - // the next start of a class name (either an open bracket or an identifier, or the closing - // curly brace), then seek backward to include any access modifiers. + if (!isStaticBlock) { + skipFieldName(tokens); + } + if (isStaticBlock || tokens.matches1(tt.lessThan) || tokens.matches1(tt.parenL)) { + // This is a static block or method, so just skip to the next method/field. To do that, we + // seek forward to the next start of a class name (either an open bracket or an identifier, + // or the closing curly brace), then seek backward to include any access modifiers. + tokens.nextToken(); while (tokens.currentToken().contextId !== classContextId) { tokens.nextToken(); } diff --git a/test/sucrase-test.ts b/test/sucrase-test.ts index d4ed0d57..a9f048b7 100644 --- a/test/sucrase-test.ts +++ b/test/sucrase-test.ts @@ -1179,4 +1179,58 @@ describe("sucrase", () => { {transforms: []}, ); }); + + it("parses and passes through class static blocks", () => { + assertResult( + ` + class A { + static { + console.log("Initialized the class"); + } + static foo() { + return 3; + } + } + `, + ` + class A { + static { + console.log("Initialized the class"); + } + static foo() { + return 3; + } + } + `, + {transforms: []}, + ); + }); + + it("correctly handles scope analysis within static blocks", () => { + assertResult( + ` + import {x, y, z} from './foo'; + + class A { + static { + const x = 3; + console.log(x); + console.log(y); + } + } + `, + ` + import { y,} from './foo'; + + class A { + static { + const x = 3; + console.log(x); + console.log(y); + } + } + `, + {transforms: ["typescript"]}, + ); + }); }); diff --git a/test/typescript-test.ts b/test/typescript-test.ts index 53797ce8..a1b13fca 100644 --- a/test/typescript-test.ts +++ b/test/typescript-test.ts @@ -1859,6 +1859,38 @@ describe("typescript transform", () => { ); }); + it("allows template literal substitutions in literal string types", () => { + assertTypeScriptResult( + ` + type Color = "red" | "blue"; + type Quantity = "one" | "two"; + + type SeussFish = \`\${Quantity | Color} fish\`; + const fish: SeussFish = "blue fish"; + `, + `"use strict"; + + + + + const fish = "blue fish"; + `, + ); + }); + + it("allows complex template literal substitutions in literal string types", () => { + // Uppercase is an example of a type expression that isn't a valid value + // expression, ensuring that we're using a type parser for the substitution. + assertTypeScriptResult( + ` + type EnthusiasticGreeting = \`\${Uppercase}\`; + `, + `"use strict"; + + `, + ); + }); + it("allows bigint literal syntax for type literals", () => { assertTypeScriptResult( ` @@ -2238,6 +2270,106 @@ describe("typescript transform", () => { `"use strict"; function f(args) {} + `, + ); + }); + + it("properly handles a >= symbol after an `as` cast", () => { + assertTypeScriptResult( + ` + const x: string | number = 1; + if (x as number >= 5) {} + `, + `"use strict"; + const x = 1; + if (x >= 5) {} + `, + ); + }); + + it("handles simple template literal interpolations in types", () => { + assertTypeScriptResult( + ` + type A = \`make\${A | B}\`; + `, + `"use strict"; + + `, + ); + }); + + it("handles complex template literal interpolations in types", () => { + assertTypeScriptResult( + ` + type A = \`foo\${{ [k: string]: number}}\`; + `, + `"use strict"; + + `, + ); + }); + + it("handles mapped type `as` clauses", () => { + assertTypeScriptResult( + ` + type MappedTypeWithNewKeys = { + [K in keyof T as NewKeyType]: T[K] + }; + + type RemoveKindField = { + [K in keyof T as Exclude]: T[K] + }; + + type PickByValueType = { + [K in keyof T as T[K] extends U ? K : never]: T[K] + }; + `, + `"use strict"; + + + + + + + + + + + + `, + ); + }); + + it("handles an arrow function with typed destructured params", () => { + assertTypeScriptResult( + ` + ( + { a, b }: T, + ): T => {}; + `, + `"use strict"; + ( + { a, b }, + ) => {}; + `, + ); + }); + + it("handles various forms of optional parameters in an interface", () => { + assertTypeScriptResult( + ` + interface B { + foo([]?): void; + bar({}, []?): any; + baz(a: string, b: number, []?): void; + } + `, + `"use strict"; + + + + + `, ); });