diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8f519bca2..38c9cd834a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## Unreleased + +* Add support for parsing "optional variance annotations" from TypeScript 4.7 ([#2102](https://github.com/evanw/esbuild/pull/2102)) + + The upcoming version of TypeScript now lets you specify `in` and/or `out` on certain type parameters (specifically only on a type alias, an interface declaration, or a class declaration). These modifiers control type paramemter covariance and contravariance: + + ```ts + type Provider = () => T; + type Consumer = (x: T) => void; + type Mapper = (x: T) => U; + type Processor = (x: T) => T; + ``` + + With this release, esbuild can now parse these new type parameter modifiers. This feature was contributed by [@magic-akari](https://github.com/magic-akari). + ## 0.14.30 * Change the context of TypeScript parameter decorators ([#2147](https://github.com/evanw/esbuild/issues/2147)) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index a23fa731157..0acad6535a7 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -1928,7 +1928,7 @@ func (p *parser) parseProperty(kind js_ast.PropertyKind, opts propertyOpts, erro // "class X { foo?(): T }" // "const x = { foo(): T {} }" - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersNormal) } // Parse a class field with an optional initial value @@ -2490,7 +2490,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() + p.skipTypeScriptTypeParameters(typeParametersNormal) } await := allowIdent @@ -3139,7 +3139,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() + p.skipTypeScriptTypeParameters(typeParametersNormal) } class := p.parseClass(classKeyword, name, parseClassOpts{}) @@ -3347,7 +3347,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() + p.skipTypeScriptTypeParameters(typeParametersNormal) p.lexer.Expect(js_lexer.TOpenParen) return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true}) } @@ -5387,7 +5387,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() + p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) } classOpts := parseClassOpts{ @@ -5647,7 +5647,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() + p.skipTypeScriptTypeParameters(typeParametersNormal) } // 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 5064fb1a6b5..15e36a1cc41 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -277,12 +277,12 @@ func (p *parser) skipTypeScriptTypeWithOpts(level js_ast.L, opts skipTypeOpts) { return } - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersNormal) p.skipTypeScriptParenOrFnType() case js_lexer.TLessThan: // "() => Foo" - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersNormal) p.skipTypeScriptParenOrFnType() case js_lexer.TOpenParen: @@ -562,7 +562,7 @@ func (p *parser) skipTypeScriptObjectType() { } // Type parameters come right after the optional mark - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersNormal) switch p.lexer.Token { case js_lexer.TColon: @@ -603,14 +603,79 @@ func (p *parser) skipTypeScriptObjectType() { p.lexer.Expect(js_lexer.TCloseBrace) } +type typeParameters uint8 + +const ( + typeParametersNormal typeParameters = iota + typeParametersWithInOutVarianceAnnotations +) + // This is the type parameter declarations that go with other symbol // declarations (class, function, type, etc.) -func (p *parser) skipTypeScriptTypeParameters() { +func (p *parser) skipTypeScriptTypeParameters(mode typeParameters) { if p.lexer.Token == js_lexer.TLessThan { p.lexer.Next() for { - p.lexer.Expect(js_lexer.TIdentifier) + hasIn := false + hasOut := false + expectIdentifier := true + invalidModifierRange := logger.Range{} + + // Scan over a sequence of "in" and "out" modifiers (a.k.a. optional variance annotations) + for { + if p.lexer.Token == js_lexer.TIn { + if invalidModifierRange.Len == 0 && (mode != typeParametersWithInOutVarianceAnnotations || hasIn || hasOut) { + // Valid: + // "type Foo = T" + // Invalid: + // "type Foo = T" + // "type Foo = T" + invalidModifierRange = p.lexer.Range() + } + p.lexer.Next() + hasIn = true + expectIdentifier = true + continue + } + + if p.lexer.IsContextualKeyword("out") { + r := p.lexer.Range() + if invalidModifierRange.Len == 0 && mode != typeParametersWithInOutVarianceAnnotations { + invalidModifierRange = r + } + p.lexer.Next() + if invalidModifierRange.Len == 0 && hasOut && (p.lexer.Token == js_lexer.TIn || p.lexer.Token == js_lexer.TIdentifier) { + // Valid: + // "type Foo = T" + // "type Foo = T" + // "type Foo = T" + // "type Foo = T" + // "type Foo = T" + // Invalid: + // "type Foo = T" + // "type Foo = T" + invalidModifierRange = r + } + hasOut = true + expectIdentifier = false + continue + } + + break + } + + // Only report an error for the first invalid modifier + if invalidModifierRange.Len > 0 { + p.log.Add(logger.Error, &p.tracker, invalidModifierRange, fmt.Sprintf( + "The modifier %q is not valid here:", p.source.TextForRange(invalidModifierRange))) + } + + // expectIdentifier => Mandatory identifier (e.g. after "type Foo Optional identifier (e.g. after "type Foo {}" if p.lexer.Token == js_lexer.TExtends { @@ -701,7 +766,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() } }() - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersNormal) if p.lexer.Token != js_lexer.TOpenParen { p.lexer.Unexpected() } @@ -836,7 +901,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) if p.lexer.Token == js_lexer.TExtends { p.lexer.Next() @@ -883,7 +948,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters() + p.skipTypeScriptTypeParameters(typeParametersWithInOutVarianceAnnotations) 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 891b0e7b3b1..dff839a2857 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -287,6 +287,84 @@ func TestTSTypes(t *testing.T) { expectParseErrorTS(t, "let x: abstract () => void = Foo", ": ERROR: Expected \";\" but found \"(\"\n") expectParseErrorTS(t, "let x: abstract () => Foo", ": ERROR: Expected \";\" but found \"(\"\n") expectParseErrorTS(t, "let x: abstract () => Foo", ": ERROR: Expected \"?\" but found \">\"\n") + + // TypeScript 4.7 + jsxErrorArrow := ": ERROR: The character \">\" is not valid inside a JSX element\n" + + "NOTE: Did you mean to escape it as \"{'>'}\" instead?\n" + + ": ERROR: Unexpected end of file\n" + expectPrintedTS(t, "type Foo = T", "") + expectPrintedTS(t, "type Foo = T", "") + expectPrintedTS(t, "type Foo = T", "") + expectPrintedTS(t, "type Foo = T", "") + expectPrintedTS(t, "type Foo = T", "") + expectPrintedTS(t, "type Foo = [X, Y]", "") + expectPrintedTS(t, "type Foo = [X, Y]", "") + expectPrintedTS(t, "type Foo = [X, Y]", "") + expectParseErrorTS(t, "type Foo = T", ": ERROR: Expected identifier but found \"i\\\\u006E\"\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: Expected \">\" but found \"T\"\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: Expected \">\" but found \"T\"\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "type Foo = T", ": ERROR: The modifier \"out\" is not valid here:\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "class Foo {}", "class Foo {\n}\n") + expectPrintedTS(t, "export default class Foo {}", "export default class Foo {\n}\n") + expectPrintedTS(t, "export default class Foo {}", "export default class Foo {\n}\n") + expectPrintedTS(t, "export default class {}", "export default class {\n}\n") + expectPrintedTS(t, "export default class {}", "export default class {\n}\n") + expectPrintedTS(t, "interface Foo {}", "") + expectPrintedTS(t, "interface Foo {}", "") + expectPrintedTS(t, "declare class Foo {}", "") + expectPrintedTS(t, "declare class Foo {}", "") + expectPrintedTS(t, "declare interface Foo {}", "") + expectPrintedTS(t, "declare interface Foo {}", "") + expectParseErrorTS(t, "function foo() {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "function foo() {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "export default function foo() {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "export default function foo() {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "export default function () {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "export default function () {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let foo: Foo", ": ERROR: Unexpected \"in\"\n") + expectParseErrorTS(t, "let foo: Foo", ": ERROR: Expected \">\" but found \"T\"\n") + expectParseErrorTS(t, "declare function foo()", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "declare function foo()", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "declare let foo: Foo", ": ERROR: Unexpected \"in\"\n") + expectParseErrorTS(t, "declare let foo: Foo", ": ERROR: Expected \">\" but found \"T\"\n") + expectParseErrorTS(t, "Foo = class {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "Foo = class {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "foo = function () {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "foo = function () {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "class Foo { foo(): T {} }", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "class Foo { foo(): T {} }", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "foo = { foo(): T {} }", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "foo = { foo(): T {} }", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "() => {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "() => {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "() => {}", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: () => {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "let x: () => {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: () => {}", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: new () => {}", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "let x: new () => {}", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: new () => {}", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: { y(): any }", ": ERROR: The modifier \"in\" is not valid here:\n") + expectParseErrorTS(t, "let x: { y(): any }", ": ERROR: The modifier \"out\" is not valid here:\n") + expectParseErrorTS(t, "let x: { y(): any }", ": ERROR: The modifier \"in\" is not valid here:\n: ERROR: The modifier \"out\" is not valid here:\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"in\", {\n T: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"out\", {\n T: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"in\", {\n out: true,\n T: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"out\", {\n in: true,\n T: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"in\", {\n T: true,\n extends: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"out\", {\n T: true,\n extends: true\n});\n") + expectPrintedTSX(t, "", "/* @__PURE__ */ React.createElement(\"in\", {\n out: true,\n T: true,\n extends: true\n});\n") + expectParseErrorTSX(t, "() => {}", ": ERROR: Expected \">\" but found \",\"\n") + expectParseErrorTSX(t, "() => {}", ": ERROR: Expected \">\" but found \",\"\n") + expectParseErrorTSX(t, "() => {}", ": ERROR: Expected \">\" but found \",\"\n") + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) + expectParseErrorTSX(t, "() => {}", jsxErrorArrow) } func TestTSAsCast(t *testing.T) {