Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support of TS Optional variance annotations #2102

Merged
merged 8 commits into from Apr 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 15 additions & 0 deletions 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<out T> = () => T;
type Consumer<in T> = (x: T) => void;
type Mapper<in T, out U> = (x: T) => U;
type Processor<in out T> = (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))
Expand Down
12 changes: 6 additions & 6 deletions internal/js_parser/js_parser.go
Expand Up @@ -1928,7 +1928,7 @@ func (p *parser) parseProperty(kind js_ast.PropertyKind, opts propertyOpts, erro

// "class X { foo?<T>(): T }"
// "const x = { foo<T>(): T {} }"
p.skipTypeScriptTypeParameters()
p.skipTypeScriptTypeParameters(typeParametersNormal)
}

// Parse a class field with an optional initial value
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{})
Expand Down Expand Up @@ -3347,7 +3347,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
// <A = B>(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})
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down
81 changes: 73 additions & 8 deletions internal/js_parser/ts_parser.go
Expand Up @@ -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:
// "<T>() => Foo<T>"
p.skipTypeScriptTypeParameters()
p.skipTypeScriptTypeParameters(typeParametersNormal)
p.skipTypeScriptParenOrFnType()

case js_lexer.TOpenParen:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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<in T> = T"
// Invalid:
// "type Foo<in in T> = T"
// "type Foo<out in T> = 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<out T> = T"
// "type Foo<out out> = T"
// "type Foo<out out, T> = T"
// "type Foo<out out = T> = T"
// "type Foo<out out extends T> = T"
// Invalid:
// "type Foo<out out in T> = T"
// "type Foo<out out T> = 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 <in ___")
// !expectIdentifier => Optional identifier (e.g. after "type Foo <out ___" since "out" may be the identifier)
if expectIdentifier || p.lexer.Token == js_lexer.TIdentifier {
p.lexer.Expect(js_lexer.TIdentifier)
}

// "class Foo<T extends number> {}"
if p.lexer.Token == js_lexer.TExtends {
Expand Down Expand Up @@ -701,7 +766,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking()
}
}()

p.skipTypeScriptTypeParameters()
p.skipTypeScriptTypeParameters(typeParametersNormal)
if p.lexer.Token != js_lexer.TOpenParen {
p.lexer.Unexpected()
}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
78 changes: 78 additions & 0 deletions internal/js_parser/ts_parser_test.go
Expand Up @@ -287,6 +287,84 @@ func TestTSTypes(t *testing.T) {
expectParseErrorTS(t, "let x: abstract () => void = Foo", "<stdin>: ERROR: Expected \";\" but found \"(\"\n")
expectParseErrorTS(t, "let x: abstract <T>() => Foo<T>", "<stdin>: ERROR: Expected \";\" but found \"(\"\n")
expectParseErrorTS(t, "let x: abstract <T extends object>() => Foo<T>", "<stdin>: ERROR: Expected \"?\" but found \">\"\n")

// TypeScript 4.7
jsxErrorArrow := "<stdin>: ERROR: The character \">\" is not valid inside a JSX element\n" +
"NOTE: Did you mean to escape it as \"{'>'}\" instead?\n" +
"<stdin>: ERROR: Unexpected end of file\n"
expectPrintedTS(t, "type Foo<in T> = T", "")
expectPrintedTS(t, "type Foo<out T> = T", "")
expectPrintedTS(t, "type Foo<in out> = T", "")
expectPrintedTS(t, "type Foo<out out> = T", "")
expectPrintedTS(t, "type Foo<in out out> = T", "")
expectPrintedTS(t, "type Foo<in X, out Y> = [X, Y]", "")
expectPrintedTS(t, "type Foo<out X, in Y> = [X, Y]", "")
expectPrintedTS(t, "type Foo<out X, out Y extends keyof X> = [X, Y]", "")
expectParseErrorTS(t, "type Foo<i\\u006E T> = T", "<stdin>: ERROR: Expected identifier but found \"i\\\\u006E\"\n")
expectParseErrorTS(t, "type Foo<ou\\u0074 T> = T", "<stdin>: ERROR: Expected \">\" but found \"T\"\n")
expectParseErrorTS(t, "type Foo<in in> = T", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "type Foo<out in> = T", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "type Foo<out in T> = T", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "type Foo<public T> = T", "<stdin>: ERROR: Expected \">\" but found \"T\"\n")
expectParseErrorTS(t, "type Foo<in out in T> = T", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "type Foo<in out out T> = T", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectPrintedTS(t, "class Foo<in T> {}", "class Foo {\n}\n")
expectPrintedTS(t, "class Foo<out T> {}", "class Foo {\n}\n")
expectPrintedTS(t, "export default class Foo<in T> {}", "export default class Foo {\n}\n")
expectPrintedTS(t, "export default class Foo<out T> {}", "export default class Foo {\n}\n")
expectPrintedTS(t, "export default class <in T> {}", "export default class {\n}\n")
expectPrintedTS(t, "export default class <out T> {}", "export default class {\n}\n")
expectPrintedTS(t, "interface Foo<in T> {}", "")
expectPrintedTS(t, "interface Foo<out T> {}", "")
expectPrintedTS(t, "declare class Foo<in T> {}", "")
expectPrintedTS(t, "declare class Foo<out T> {}", "")
expectPrintedTS(t, "declare interface Foo<in T> {}", "")
expectPrintedTS(t, "declare interface Foo<out T> {}", "")
expectParseErrorTS(t, "function foo<in T>() {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "function foo<out T>() {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "export default function foo<in T>() {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "export default function foo<out T>() {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "export default function <in T>() {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "export default function <out T>() {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let foo: Foo<in T>", "<stdin>: ERROR: Unexpected \"in\"\n")
expectParseErrorTS(t, "let foo: Foo<out T>", "<stdin>: ERROR: Expected \">\" but found \"T\"\n")
expectParseErrorTS(t, "declare function foo<in T>()", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "declare function foo<out T>()", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "declare let foo: Foo<in T>", "<stdin>: ERROR: Unexpected \"in\"\n")
expectParseErrorTS(t, "declare let foo: Foo<out T>", "<stdin>: ERROR: Expected \">\" but found \"T\"\n")
expectParseErrorTS(t, "Foo = class <in T> {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "Foo = class <out T> {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "foo = function <in T>() {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "foo = function <out T>() {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "class Foo { foo<in T>(): T {} }", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "class Foo { foo<out T>(): T {} }", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "foo = { foo<in T>(): T {} }", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "foo = { foo<out T>(): T {} }", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "<in T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "<out T>() => {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "<in T, out T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: <in T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "let x: <out T>() => {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: <in T, out T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: new <in T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "let x: new <out T>() => {}", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: new <in T, out T>() => {}", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: { y<in T>(): any }", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n")
expectParseErrorTS(t, "let x: { y<out T>(): any }", "<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectParseErrorTS(t, "let x: { y<in T, out T>(): any }", "<stdin>: ERROR: The modifier \"in\" is not valid here:\n<stdin>: ERROR: The modifier \"out\" is not valid here:\n")
expectPrintedTSX(t, "<in T></in>", "/* @__PURE__ */ React.createElement(\"in\", {\n T: true\n});\n")
expectPrintedTSX(t, "<out T></out>", "/* @__PURE__ */ React.createElement(\"out\", {\n T: true\n});\n")
expectPrintedTSX(t, "<in out T></in>", "/* @__PURE__ */ React.createElement(\"in\", {\n out: true,\n T: true\n});\n")
expectPrintedTSX(t, "<out in T></out>", "/* @__PURE__ */ React.createElement(\"out\", {\n in: true,\n T: true\n});\n")
expectPrintedTSX(t, "<in T extends={true}></in>", "/* @__PURE__ */ React.createElement(\"in\", {\n T: true,\n extends: true\n});\n")
expectPrintedTSX(t, "<out T extends={true}></out>", "/* @__PURE__ */ React.createElement(\"out\", {\n T: true,\n extends: true\n});\n")
expectPrintedTSX(t, "<in out T extends={true}></in>", "/* @__PURE__ */ React.createElement(\"in\", {\n out: true,\n T: true,\n extends: true\n});\n")
expectParseErrorTSX(t, "<in T,>() => {}", "<stdin>: ERROR: Expected \">\" but found \",\"\n")
expectParseErrorTSX(t, "<out T,>() => {}", "<stdin>: ERROR: Expected \">\" but found \",\"\n")
expectParseErrorTSX(t, "<in out T,>() => {}", "<stdin>: ERROR: Expected \">\" but found \",\"\n")
expectParseErrorTSX(t, "<in T extends any>() => {}", jsxErrorArrow)
expectParseErrorTSX(t, "<out T extends any>() => {}", jsxErrorArrow)
expectParseErrorTSX(t, "<in out T extends any>() => {}", jsxErrorArrow)
}

func TestTSAsCast(t *testing.T) {
Expand Down