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 930e92dbd1d..c50fbc010e5 100644 --- a/internal/js_parser/js_parser.go +++ b/internal/js_parser/js_parser.go @@ -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,6 +3820,7 @@ 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() { @@ -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) 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 27e113ac154..aa721e8aafc 100644 --- a/internal/js_parser/ts_parser.go +++ b/internal/js_parser/ts_parser.go @@ -651,6 +651,9 @@ const ( // TypeScript 5.0 allowConstModifier + + // Allow "<>" without any type parameters + allowEmptyTypeParameters ) type skipTypeScriptTypeParametersResult uint8 @@ -671,6 +674,11 @@ func (p *parser) skipTypeScriptTypeParameters(flags typeParameterFlags) skipType p.lexer.Next() result := couldBeTypeCast + if (flags&allowEmptyTypeParameters) != 0 && p.lexer.Token == js_lexer.TGreaterThan { + p.lexer.Next() + return definitelyTypeParameters + } + for { hasIn := false hasOut := false @@ -1181,7 +1189,7 @@ func (p *parser) skipTypeScriptInterfaceStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) + p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations | allowEmptyTypeParameters) if p.lexer.Token == js_lexer.TExtends { p.lexer.Next() @@ -1256,7 +1264,7 @@ func (p *parser) skipTypeScriptTypeStmt(opts parseStmtOpts) { p.localTypeNames[name] = true } - p.skipTypeScriptTypeParameters(allowInOutVarianceAnnotations) + 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 9ed95429159..11619a80814 100644 --- a/internal/js_parser/ts_parser_test.go +++ b/internal/js_parser/ts_parser_test.go @@ -323,6 +323,23 @@ 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<> = {}", "") + 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") expectPrintedTS(t, "x as 1n < 1", "x < 1;\n")