From 56bfe1be55d1a1b14bab427547d9bf3c114fcbd1 Mon Sep 17 00:00:00 2001 From: magic-akari Date: Wed, 29 Nov 2023 16:56:50 +0800 Subject: [PATCH 1/2] fix TypeScript empty type parameter skipping issue --- internal/js_parser/js_parser.go | 12 ++++++------ internal/js_parser/ts_parser.go | 19 ++++++++++++------- internal/js_parser/ts_parser_test.go | 6 ++++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 930e92dbd1d..93cf369d158 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -2316,7 +2316,7 @@ func (p *parser) parseProperty(startLoc logger.Loc, kind js_ast.PropertyKind, op // "class X { foo?(): T }" // "const x = { foo(): T {} }" if !hasDefiniteAssignmentAssertionOperator && kind != js_ast.PropertyAutoAccessor { - hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier) != didNotSkipAnything + hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier, false) != didNotSkipAnything } } @@ -2939,7 +2939,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(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) } await := allowIdent @@ -3819,7 +3819,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(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) p.lexer.Expect(js_lexer.TOpenParen) return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true}) } @@ -6122,7 +6122,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(allowInOutVarianceAnnotations | allowConstModifier) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations|allowConstModifier, false) } classOpts := parseClassOpts{ @@ -6174,7 +6174,7 @@ func (p *parser) parseClassExpr(decorators []js_ast.Decorator) js_ast.Expr { // Even anonymous classes can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations|allowConstModifier, false) } class := p.parseClass(classKeyword, name, opts) @@ -6470,7 +6470,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(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) } // 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 27e113ac154..0ff497b9fe0 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -292,12 +292,12 @@ loop: return } - p.skipTypeScriptTypeParameters(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) p.skipTypeScriptParenOrFnType() case js_lexer.TLessThan: // "() => Foo" - p.skipTypeScriptTypeParameters(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) p.skipTypeScriptParenOrFnType() case js_lexer.TOpenParen: @@ -602,7 +602,7 @@ func (p *parser) skipTypeScriptObjectType() { } // Type parameters come right after the optional mark - p.skipTypeScriptTypeParameters(allowConstModifier) + p.skipTypeScriptTypeParameters(allowConstModifier, false) switch p.lexer.Token { case js_lexer.TColon: @@ -663,7 +663,7 @@ const ( // This is the type parameter declarations that go with other symbol // declarations (class, function, type, etc.) -func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipTypeScriptTypeParametersResult { +func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags, allowEmptyTypeList bool) skipTypeScriptTypeParametersResult { if p.lexer.Token != js_lexer.TLessThan { return didNotSkipAnything } @@ -671,6 +671,11 @@ func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipType p.lexer.Next() result := couldBeTypeCast + if allowEmptyTypeList && p.lexer.Token == js_lexer.TGreaterThan { + p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */) + return definitelyTypeParameters + } + for { hasIn := false hasOut := false @@ -864,7 +869,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() } }() - result := p.skipTypeScriptTypeParameters(allowConstModifier) + result := p.skipTypeScriptTypeParameters(allowConstModifier, false) if p.lexer.Token != js_lexer.TOpenParen { p.lexer.Unexpected() } @@ -1181,7 +1186,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations, true) if p.lexer.Token == js_lexer.TExtends { p.lexer.Next() @@ -1256,7 +1261,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations, true) 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 9ed95429159..be03b9aa67a 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -323,6 +323,12 @@ func TestTSTypes(t *testing.T) { expectPrintedTS(t, "type Foo = Array<(x: T) => T>\n x", "x;\n") expectPrintedTSX(t, "(x: T) => T>/>", "/* @__PURE__ */ React.createElement(Foo, null);\n") + expectPrintedTS(t, "interface Foo<> {}", "") + expectPrintedTSX(t, "interface Foo<> {}", "") + + expectPrintedTS(t, "type Foo<> = {}", "") + expectPrintedTSX(t, "type Foo<> = {}", "") + // Certain built-in types do not accept type parameters expectPrintedTS(t, "x as 1 < 1", "x < 1;\n") expectPrintedTS(t, "x as 1n < 1", "x < 1;\n") From 09d5a4a28e72d54fa8f4529d57bacd1fb6bae0b4 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sun, 3 Dec 2023 13:33:50 -0500 Subject: [PATCH 2/2] use a bit flag instead of a boolean flag --- CHANGELOG.md | 13 +++++++++++ internal/js_parser/js_parser.go | 35 ++++++++++++++++++++-------- internal/js_parser/js_parser_test.go | 11 +++++---- internal/js_parser/ts_parser.go | 21 ++++++++++------- internal/js_parser/ts_parser_test.go | 13 ++++++++++- 5 files changed, 69 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d0a6e4e06c..7ffb9dc5961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## Unreleased + +* Allow empty type parameter lists in certain cases ([#3512](https://github.com/evanw/esbuild/issues/3512)) + + TypeScript allows interface declarations and type aliases to have empty type parameter lists. Previously esbuild didn't handle this edge case but with this release, esbuild will now parse this syntax: + + ```ts + interface Foo<> {} + type Bar<> = {} + ``` + + This fix was contributed by [@magic-akari](https://github.com/magic-akari). + ## 0.19.8 * Add a treemap chart to esbuild's bundle analyzer ([#2848](https://github.com/evanw/esbuild/issues/2848)) diff --git a/internal/js_parser/js_parser.go b/internal/js_parser/js_parser.go index 93cf369d158..c50fbc010e5 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -2316,7 +2316,7 @@ func (p *parser) parseProperty(startLoc logger.Loc, kind js_ast.PropertyKind, op // "class X { foo?(): T }" // "const x = { foo(): T {} }" if !hasDefiniteAssignmentAssertionOperator && kind != js_ast.PropertyAutoAccessor { - hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier, false) != didNotSkipAnything + hasTypeParameters = p.skipTypeScriptTypeParameters(allowConstModifier) != didNotSkipAnything } } @@ -2939,7 +2939,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(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) } await := allowIdent @@ -3795,9 +3795,13 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // (x) => {} // (x) => {} // + // A syntax error: + // <>() => {} + // // TSX: // // A JSX element: + // <>() => {} // (x) => {} // // (x) => {} @@ -3816,10 +3820,11 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF // A syntax error: // <[]>(x) // (x) + // <>() => {} // (x) => {} if p.options.ts.Parse && p.options.jsx.Parse && p.isTSArrowFnJSX() { - p.skipTypeScriptTypeParameters(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) p.lexer.Expect(js_lexer.TOpenParen) return p.parseParenExpr(loc, level, parenExprOpts{forceArrowFn: true}) } @@ -4933,6 +4938,13 @@ func (p *parser) parseJSXNamespacedName() (logger.Range, js_lexer.MaybeSubstring return nameRange, name } +func tagOrFragmentHelpText(tag string) string { + if tag == "" { + return "fragment tag" + } + return fmt.Sprintf("%q tag", tag) +} + func (p *parser) parseJSXTag() (logger.Range, string, js_ast.Expr) { loc := p.lexer.Loc() @@ -5226,10 +5238,12 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr { p.lexer.NextInsideJSXElement() endRange, endText, _ := p.parseJSXTag() if startText != endText { + startTag := tagOrFragmentHelpText(startText) + endTag := tagOrFragmentHelpText(endText) msg := logger.Msg{ Kind: logger.Error, - Data: p.tracker.MsgData(endRange, fmt.Sprintf("Expected closing %q tag to match opening %q tag", endText, startText)), - Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %q tag is here:", startText))}, + Data: p.tracker.MsgData(endRange, fmt.Sprintf("Unexpected closing %s does not match opening %s", endTag, startTag)), + Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %s is here:", startTag))}, } msg.Data.Location.Suggestion = startText p.log.AddMsg(msg) @@ -5247,10 +5261,11 @@ func (p *parser) parseJSXElement(loc logger.Loc) js_ast.Expr { }} case js_lexer.TEndOfFile: + startTag := tagOrFragmentHelpText(startText) msg := logger.Msg{ Kind: logger.Error, - Data: p.tracker.MsgData(p.lexer.Range(), fmt.Sprintf("Unexpected end of file before a closing %q tag", startText)), - Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %q tag is here:", startText))}, + Data: p.tracker.MsgData(p.lexer.Range(), fmt.Sprintf("Unexpected end of file before a closing %s", startTag)), + Notes: []logger.MsgData{p.tracker.MsgData(startRange, fmt.Sprintf("The opening %s is here:", startTag))}, } msg.Data.Location.Suggestion = fmt.Sprintf("", startText) p.log.AddMsg(msg) @@ -6122,7 +6137,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(allowInOutVarianceAnnotations|allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier) } classOpts := parseClassOpts{ @@ -6174,7 +6189,7 @@ func (p *parser) parseClassExpr(decorators []js_ast.Decorator) js_ast.Expr { // Even anonymous classes can have TypeScript type parameters if p.options.ts.Parse { - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations|allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowConstModifier) } class := p.parseClass(classKeyword, name, opts) @@ -6470,7 +6485,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(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) } // Introduce a fake block scope for function declarations inside if statements diff --git a/internal/js_parser/js_parser_test.go b/internal/js_parser/js_parser_test.go index bcb09f0f8cf..04b5aaf889e 100644 --- a/internal/js_parser/js_parser_test.go +++ b/internal/js_parser/js_parser_test.go @@ -5504,11 +5504,14 @@ func TestJSX(t *testing.T) { expectParseErrorJSX(t, "", ": ERROR: Expected \"{\" but found \"true\"\n") expectParseErrorJSX(t, "", ": ERROR: Expected identifier but found \"/\"\n") - expectParseErrorJSX(t, "<>", ": ERROR: Expected closing \"b\" tag to match opening \"\" tag\n: NOTE: The opening \"\" tag is here:\n") - expectParseErrorJSX(t, "", ": ERROR: Expected closing \"\" tag to match opening \"a\" tag\n: NOTE: The opening \"a\" tag is here:\n") - expectParseErrorJSX(t, "", ": ERROR: Expected closing \"b\" tag to match opening \"a\" tag\n: NOTE: The opening \"a\" tag is here:\n") + expectParseErrorJSX(t, "<>", + ": ERROR: Unexpected closing \"b\" tag does not match opening fragment tag\n: NOTE: The opening fragment tag is here:\n") + expectParseErrorJSX(t, "", + ": ERROR: Unexpected closing fragment tag does not match opening \"a\" tag\n: NOTE: The opening \"a\" tag is here:\n") + expectParseErrorJSX(t, "", + ": ERROR: Unexpected closing \"b\" tag does not match opening \"a\" tag\n: NOTE: The opening \"a\" tag is here:\n") expectParseErrorJSX(t, "<\na\n.\nb\n>\n<\n/\nc\n.\nd\n>", - ": ERROR: Expected closing \"c.d\" tag to match opening \"a.b\" tag\n: NOTE: The opening \"a.b\" tag is here:\n") + ": ERROR: Unexpected closing \"c.d\" tag does not match opening \"a.b\" tag\n: NOTE: The opening \"a.b\" tag is here:\n") expectParseErrorJSX(t, "", ": ERROR: Expected \">\" but found \".\"\n") expectParseErrorJSX(t, "", ": ERROR: Unexpected \"-\"\n") diff --git a/internal/js_parser/ts_parser.go b/internal/js_parser/ts_parser.go index 0ff497b9fe0..aa721e8aafc 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -292,12 +292,12 @@ loop: return } - p.skipTypeScriptTypeParameters(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) p.skipTypeScriptParenOrFnType() case js_lexer.TLessThan: // "() => Foo" - p.skipTypeScriptTypeParameters(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) p.skipTypeScriptParenOrFnType() case js_lexer.TOpenParen: @@ -602,7 +602,7 @@ func (p *parser) skipTypeScriptObjectType() { } // Type parameters come right after the optional mark - p.skipTypeScriptTypeParameters(allowConstModifier, false) + p.skipTypeScriptTypeParameters(allowConstModifier) switch p.lexer.Token { case js_lexer.TColon: @@ -651,6 +651,9 @@ const ( // TypeScript 5.0 allowConstModifier + + // Allow "<>" without any type parameters + allowEmptyTypeParameters ) type skipTypeScriptTypeParametersResult uint8 @@ -663,7 +666,7 @@ const ( // This is the type parameter declarations that go with other symbol // declarations (class, function, type, etc.) -func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags, allowEmptyTypeList bool) skipTypeScriptTypeParametersResult { +func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipTypeScriptTypeParametersResult { if p.lexer.Token != js_lexer.TLessThan { return didNotSkipAnything } @@ -671,8 +674,8 @@ func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags, allowEmp p.lexer.Next() result := couldBeTypeCast - if allowEmptyTypeList && p.lexer.Token == js_lexer.TGreaterThan { - p.lexer.ExpectGreaterThan(false /* isInsideJSXElement */) + if (flags&allowEmptyTypeParameters) != 0 && p.lexer.Token == js_lexer.TGreaterThan { + p.lexer.Next() return definitelyTypeParameters } @@ -869,7 +872,7 @@ func (p *parser) trySkipTypeScriptTypeParametersThenOpenParenWithBacktracking() } }() - result := p.skipTypeScriptTypeParameters(allowConstModifier, false) + result := p.skipTypeScriptTypeParameters(allowConstModifier) if p.lexer.Token != js_lexer.TOpenParen { p.lexer.Unexpected() } @@ -1186,7 +1189,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations, true) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowEmptyTypeParameters) if p.lexer.Token == js_lexer.TExtends { p.lexer.Next() @@ -1261,7 +1264,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations, true) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowEmptyTypeParameters) 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 be03b9aa67a..11619a80814 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -325,9 +325,20 @@ func TestTSTypes(t *testing.T) { expectPrintedTS(t, "interface Foo<> {}", "") expectPrintedTSX(t, "interface Foo<> {}", "") - expectPrintedTS(t, "type Foo<> = {}", "") expectPrintedTSX(t, "type Foo<> = {}", "") + expectParseErrorTS(t, "class Foo<> {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTSX(t, "class Foo<> {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "class Foo { foo<>() {} }", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTSX(t, "class Foo { foo<>() {} }", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "type Foo = { foo<>(): void }", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTSX(t, "type Foo = { foo<>(): void }", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "type Foo = <>() => {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTSX(t, "type Foo = <>() => {}", ": ERROR: Expected identifier but found \">\"\n") + expectParseErrorTS(t, "let Foo = <>() => {}", ": ERROR: Unexpected \">\"\n") + expectParseErrorTSX(t, "let Foo = <>() => {}", + ": ERROR: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+ + ": ERROR: Unexpected end of file before a closing fragment tag\n: NOTE: The opening fragment tag is here:\n") // Certain built-in types do not accept type parameters expectPrintedTS(t, "x as 1 < 1", "x < 1;\n")