From 73523d9d0c814d70c5697352e4d632f50e558124 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Thu, 26 Jan 2023 21:49:03 -0500 Subject: [PATCH] parse `const` type parameters from TypeScript 5.0 --- CHANGELOG.md | 20 ++++++++ internal/js_parser/js_parser.go | 37 +++++++++----- internal/js_parser/ts_parser.go | 67 +++++++++++++++++++------- internal/js_parser/ts_parser_test.go | 72 +++++++++++++++++++++++++++- 4 files changed, 167 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e268d229e..482f9768d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ ## Unreleased +* Parse `const` type parameters from TypeScript 5.0 + + The TypeScript 5.0 beta announcement adds [`const` type parameters](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#const-type-parameters) to the language. You can now add the `const` modifier on a type parameter of a function, method, or class like this: + + ```ts + type HasNames = { names: readonly string[] }; + const getNamesExactly = (arg: T): T["names"] => arg.names; + const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] }); + ``` + + The type of `names` in the above example is `readonly ["Alice", "Bob", "Eve"]`. Marking the type parameter as `const` behaves as if you had written `as const` at every use instead. The above code is equivalent to the following TypeScript, which was the only option before TypeScript 5.0: + + ```ts + type HasNames = { names: readonly string[] }; + const getNamesExactly = (arg: T): T["names"] => arg.names; + const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] } as const); + ``` + + You can read [the announcement](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#const-type-parameters) for more information. + * Make parsing generic `async` arrow functions more strict in `.tsx` files Previously esbuild's TypeScript parser incorrectly accepted the following code as valid: diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 80979a12bc5..3c417b25392 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -2155,7 +2155,9 @@ func (p *parser) parseProperty(startLoc logger.Loc, kind js_ast.PropertyKind, op // "class X { foo?(): T }" // "const x = { foo(): T {} }" - hasTypeParameters = !hasDefiniteAssignmentAssertionOperator && p.skipTypeScriptTypeParameters(typeParametersNormal) + if !hasDefiniteAssignmentAssertionOperator { + hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier) != didNotSkipAnything + } } // Parse a class field with an optional initial value @@ -2716,9 +2718,14 @@ func (p *parser) parseAsyncPrefixExpr(asyncRange logger.Range, level js_ast.L, f // "async()" // "async () => {}" case js_lexer.TLessThan: - if p.options.ts.Parse && (!p.options.jsx.Parse || p.isTSArrowFnJSX()) && p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() { - p.lexer.Next() - return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{asyncRange: asyncRange}) + if p.options.ts.Parse && (!p.options.jsx.Parse || p.isTSArrowFnJSX()) { + if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything { + p.lexer.Next() + return p.parseParenExpr(asyncRange.Loc, level, parenExprOpts{ + asyncRange: asyncRange, + forceArrowFn: result == definitelyTypeParameters, + }) + } } } } @@ -2761,7 +2768,7 @@ func (p *parser) parseFnExpr(loc logger.Loc, isAsync bool, asyncRange logger.Ran // Even anonymous functions can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(allowConstModifier) } await := allowIdent @@ -3437,7 +3444,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // Even anonymous classes can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier) } class := p.parseClass(classKeyword, name, parseClassOpts{}) @@ -3628,12 +3635,15 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // (x) // <[]>(x) // (x) + // (x) // // An arrow function with type parameters: // (x) => {} // (x) => {} // (x) => {} // (x) => {} + // (x) => {} + // (x) => {} // // TSX: // @@ -3641,10 +3651,13 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // (x) => {} // (x) => {} // (x) => {} + // (x) => {} // // An arrow function with type parameters: // (x) => {} // (x) => {} + // (x) + // (x) => {} // // A syntax error: // <[]>(x) @@ -3653,7 +3666,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // (x) => {} if p.options.ts.Parse && p.options.jsx.Parse && p.isTSArrowFnJSX() { - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(allowConstModifier) p.lexer.Expect(js_lexer.TOpenParen) return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true}) } @@ -3700,9 +3713,11 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // "(x)" // "(x) => {}" - if p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() { + if result := p.trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking(); result != didNotSkipAnything { p.lexer.Expect(js_lexer.TOpenParen) - return p.parseParenExpr(loc, level, parenExprOpts{}) + return p.parseParenExpr(loc, level, parenExprOpts{ + forceArrowFn: result == definitelyTypeParameters, + }) } // "x" @@ -5811,7 +5826,7 @@ func (p *parser) parseClassStmt(loc logger.Loc, opts parseStmtOpts) js_ast.Stmt // Even anonymous classes can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier) } classOpts := parseClassOpts{ @@ -6089,7 +6104,7 @@ func (p *parser) parseFnStmt(loc logger.Loc, opts parseStmtOpts, isAsync bool, a // Even anonymous functions can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(allowConstModifier) } // Introduce a fake block scope for function declarations inside if statements diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index ca41fbe2196..7e742ec824d 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -286,12 +286,12 @@ loop: return } - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(allowConstModifier) p.skipTypeScriptParenOrFnType() case js_lexer.TLessThan: // "() => Foo" - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(allowConstModifier) p.skipTypeScriptParenOrFnType() case js_lexer.TOpenParen: @@ -594,7 +594,7 @@ func (p *parser) skipTypeScriptObjectType() { } // Type parameters come right after the optional mark - p.skipTypeScriptTypeParameters(typeParametersNormal) + p.skipTypeScriptTypeParameters(0) switch p.lexer.Token { case js_lexer.TColon: @@ -635,21 +635,33 @@ func (p *parser) skipTypeScriptObjectType() { p.lexer.Expect(js_lexer.TCloseBrace) } -type typeParameters uint8 +type typeParameterFlags uint8 const ( - typeParametersNormal typeParameters = iota - typeParametersWithInOutVarianceAnnotations + // TypeScript 4.7 + allowInOutVarianceAnnotations typeParameterFlags = 1 << iota + + // TypeScript 5.0 + allowConstModifier +) + +type skipTypeScriptTypeParametersResult uint8 + +const ( + didNotSkipAnything skipTypeScriptTypeParametersResult = iota + couldBeTypeCast + definitelyTypeParameters ) // This is the type parameter declarations that go with other symbol // declarations (class, function, type, etc.) -func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool { +func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipTypeScriptTypeParametersResult { if p.lexer.Token != js_lexer.TLessThan { - return false + return didNotSkipAnything } p.lexer.Next() + result := couldBeTypeCast for { hasIn := false @@ -657,10 +669,25 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool { expectIdentifier := true invalidModifierRange := logger.Range{} - // Scan over a sequence of "in" and "out" modifiers (a.k.a. optional variance annotations) + // Scan over a sequence of "in" and "out" modifiers (a.k.a. optional + // variance annotations) as well as "const" modifiers for { + if p.lexer.Token == js_lexer.TConst { + if invalidModifierRange.Len == 0 && (flags&allowConstModifier) == 0 { + // Valid: + // "class Foo {}" + // Invalid: + // "interface Foo {}" + invalidModifierRange = p.lexer.Range() + } + result = definitelyTypeParameters + p.lexer.Next() + expectIdentifier = true + continue + } + if p.lexer.Token == js_lexer.TIn { - if invalidModifierRange.Len == 0 && (mode != typeParametersWithInOutVarianceAnnotations || hasIn || hasOut) { + if invalidModifierRange.Len == 0 && ((flags&allowInOutVarianceAnnotations) == 0 || hasIn || hasOut) { // Valid: // "type Foo = T" // Invalid: @@ -676,7 +703,7 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool { if p.lexer.IsContextualKeyword("out") { r := p.lexer.Range() - if invalidModifierRange.Len == 0 && mode != typeParametersWithInOutVarianceAnnotations { + if invalidModifierRange.Len == 0 && (flags&allowInOutVarianceAnnotations) == 0 { invalidModifierRange = r } p.lexer.Next() @@ -714,12 +741,14 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool { // "class Foo {}" if p.lexer.Token == js_lexer.TExtends { + result = definitelyTypeParameters p.lexer.Next() p.skipTypeScriptType(js_ast.LLowest) } // "class Foo {}" if p.lexer.Token == js_lexer.TEquals { + result = definitelyTypeParameters p.lexer.Next() p.skipTypeScriptType(js_ast.LLowest) } @@ -729,12 +758,13 @@ func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) bool { } p.lexer.Next() if p.lexer.Token == js_lexer.TGreaterThan { + result = definitelyTypeParameters break } } p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */) - return true + return result } func (p *parser) skipTypeScriptTypeArguments(isInsideJSXElement bool) bool { @@ -787,7 +817,7 @@ func (p *parser) trySkipTypeScriptTypeArgumentsWithBacktracking() bool { return true } -func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() bool { +func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() skipTypeScriptTypeParametersResult { oldLexer := p.lexer p.lexer.IsLogDisabled = true @@ -801,7 +831,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() } }() - p.skipTypeScriptTypeParameters(typeParametersNormal) + result := p.skipTypeScriptTypeParameters(allowConstModifier) if p.lexer.Token != js_lexer.TOpenParen { p.lexer.Unexpected() } @@ -809,7 +839,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() // Restore the log disabled flag. Note that we can't just set it back to false // because it may have been true to start with. p.lexer.IsLogDisabled = oldLexer.IsLogDisabled - return true + return result } func (p *parser) trySkipTypeScriptArrowReturnTypeWithBacktracking() bool { @@ -896,6 +926,9 @@ func (p *parser) isTSArrowFnJSX() (isTSArrowFn bool) { p.lexer.Next() // Look ahead to see if this should be an arrow function instead + if p.lexer.Token == js_lexer.TConst { + p.lexer.Next() + } if p.lexer.Token == js_lexer.TIdentifier { p.lexer.Next() if p.lexer.Token == js_lexer.TComma || p.lexer.Token == js_lexer.TEquals { @@ -1015,7 +1048,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) if p.lexer.Token == js_lexer.TExtends { p.lexer.Next() @@ -1090,7 +1123,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) p.lexer.Expect(js_lexer.TEquals) p.skipTypeScriptType(js_ast.LLowest) p.lexer.ExpectOrInsertSemicolon() diff --git a/internal/js_parser/ts_parser_test.go b/internal/js_parser/ts_parser_test.go index f28b12229a6..f00619142be 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -376,6 +376,76 @@ func TestTSTypes(t *testing.T) { expectPrintedTS(t, "class Container { get data(): typeof this.#data {} }", "class Container {\n get data() {\n }\n}\n") expectPrintedTS(t, "const a: typeof this.#a = 1;", "const a = 1;\n") expectParseErrorTS(t, "const a: typeof #a = 1;", ": ERROR: Expected identifier but found \"#a\"\n") + + // TypeScript 5.0 + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "Foo = class {}", "Foo = class {\n};\n") + expectPrintedTS(t, "Foo = class Bar {}", "Foo = class Bar {\n};\n") + expectPrintedTS(t, "function foo() {}", "function foo() {\n}\n") + expectPrintedTS(t, "foo = function () {}", "foo = function() {\n};\n") + expectPrintedTS(t, "foo = function bar() {}", "foo = function bar() {\n};\n") + expectPrintedTS(t, "class Foo { bar() {} }", "class Foo {\n bar() {\n }\n}\n") + expectPrintedTS(t, "foo = { bar() {} }", "foo = { bar() {\n} };\n") + expectPrintedTS(t, "x = (y)", "x = y;\n") + expectPrintedTS(t, "() => {}", "() => {\n};\n") + expectPrintedTS(t, "() => {}", "() => {\n};\n") + expectPrintedTS(t, "async () => {}", "async () => {\n};\n") + expectPrintedTS(t, "async () => {}", "async () => {\n};\n") + expectPrintedTS(t, "let x: () => T = y", "let x = y;\n") + expectPrintedTS(t, "let x: () => T = y", "let x = y;\n") + expectPrintedTS(t, "let x: new () => T = y", "let x = y;\n") + expectPrintedTS(t, "let x: new () => T = y", "let x = y;\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"const\" is not valid here:\n") + expectParseErrorTS(t, "interface Foo {}", ": ERROR: The modifier \"const\" is not valid here:\n") + expectParseErrorTS(t, "let x: () => {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "let x: new () => {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "let x: Foo", ": ERROR: Expected \">\" but found \"T\"\n") + expectParseErrorTS(t, "x = (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = async (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = async (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = async (y)", ": ERROR: Expected \"=>\" but found end of file\n") + expectParseErrorTS(t, "x = () => {}", ": ERROR: Expected \">\" but found \"const\"\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectParseErrorTS(t, "class Foo {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "class Foo {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "class Foo {}", ": ERROR: Expected identifier but found \">\"\n") + expectPrintedTSX(t, "(x)", "/* @__PURE__ */ React.createElement(\"const\", null, \"(x)\");\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"const\", { const: true });\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"const\", { const: true });\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"const\", { T: true });\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"const\", { T: true });\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"const\", { T: true, extends: true });\n") + expectPrintedTSX(t, "() => {}", "() => {\n};\n") + expectPrintedTSX(t, "() => {}", "() => {\n};\n") + expectPrintedTSX(t, "() => {}", "() => {\n};\n") + expectPrintedTSX(t, "() => {}", "() => {\n};\n") + expectPrintedTSX(t, "() => {}", "() => {\n};\n") + expectPrintedTSX(t, "async () => {}", "async () => {\n};\n") + expectPrintedTSX(t, "async () => {}", "async () => {\n};\n") + expectPrintedTSX(t, "async () => {}", "async () => {\n};\n") + expectPrintedTSX(t, "async () => {}", "async () => {\n};\n") + expectPrintedTSX(t, "async () => {}", "async () => {\n};\n") + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) + expectParseErrorTSX(t, "() => {}", ": ERROR: Expected \">\" but found \",\"\n") + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) + expectParseErrorTSX(t, "async () => {}", ": ERROR: Unexpected \"const\"\n") + expectParseErrorTSX(t, "async () => {}", ": ERROR: Unexpected \"const\"\n") + expectParseErrorTSX(t, "async () => {}", ": ERROR: Unexpected \"const\"\n") + expectParseErrorTSX(t, "async () => {}", ": ERROR: Unexpected \"const\"\n") + + // The TypeScript compiler currently crashes internally while parsing these, + // so it's unclear what the correct output should be. Tracking GitHub issue: + // https://github.com/microsoft/TypeScript/issues/52449 + expectParseErrorTSX(t, "", ": ERROR: Unexpected \"/\"\n") + expectParseErrorTSX(t, "", ": ERROR: Unexpected \"/\"\n") } func TestTSAsCast(t *testing.T) { @@ -2352,7 +2422,7 @@ func TestTSJSX(t *testing.T) { expectPrintedTS(t, "const x = async (y, z) => {}", "const x = async (y, z) => {\n};\n") expectPrintedTS(t, "const x = (async y)", "const x = (async < T, X > y);\n") expectPrintedTS(t, "const x = (async (y))", "const x = async(y);\n") - expectPrintedTS(t, "const x = async (y)", "const x = async(y);\n") + expectParseErrorTS(t, "const x = async (y)", ": ERROR: Expected \"=>\" but found end of file\n") expectParseErrorTS(t, "const x = async (y: T)", ": ERROR: Unexpected \":\"\n") expectParseErrorTS(t, "const x = async\n() => {}", ": ERROR: Expected \";\" but found \"=>\"\n") expectParseErrorTS(t, "const x = async\n(x) => {}", ": ERROR: Expected \";\" but found \"=>\"\n")