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

fix TypeScript empty type parameter skipping issue #3513

Merged
merged 2 commits into from
Dec 3, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
23 changes: 19 additions & 4 deletions internal/js_parser/js_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -3795,9 +3795,13 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
// <const A>(x) => {}
// <const A extends B>(x) => {}
//
// A syntax error:
// <>() => {}
//
// TSX:
//
// A JSX element:
// <>() => {}</>
// <A>(x) => {}</A>
// <A extends/>
// <A extends>(x) => {}</A>
Expand All @@ -3816,6 +3820,7 @@ func (p *parser) parsePrefix(level js_ast.L, errors *deferredErrors, flags exprF
// A syntax error:
// <[]>(x)
// <A[]>(x)
// <>() => {}
// <A>(x) => {}

if p.options.ts.Parse && p.options.jsx.Parse && p.isTSArrowFnJSX() {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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)
Expand All @@ -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("</%s>", startText)
p.log.AddMsg(msg)
Expand Down
11 changes: 7 additions & 4 deletions internal/js_parser/js_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5504,11 +5504,14 @@ func TestJSX(t *testing.T) {

expectParseErrorJSX(t, "<a b=true/>", "<stdin>: ERROR: Expected \"{\" but found \"true\"\n")
expectParseErrorJSX(t, "</a>", "<stdin>: ERROR: Expected identifier but found \"/\"\n")
expectParseErrorJSX(t, "<></b>", "<stdin>: ERROR: Expected closing \"b\" tag to match opening \"\" tag\n<stdin>: NOTE: The opening \"\" tag is here:\n")
expectParseErrorJSX(t, "<a></>", "<stdin>: ERROR: Expected closing \"\" tag to match opening \"a\" tag\n<stdin>: NOTE: The opening \"a\" tag is here:\n")
expectParseErrorJSX(t, "<a></b>", "<stdin>: ERROR: Expected closing \"b\" tag to match opening \"a\" tag\n<stdin>: NOTE: The opening \"a\" tag is here:\n")
expectParseErrorJSX(t, "<></b>",
"<stdin>: ERROR: Unexpected closing \"b\" tag does not match opening fragment tag\n<stdin>: NOTE: The opening fragment tag is here:\n")
expectParseErrorJSX(t, "<a></>",
"<stdin>: ERROR: Unexpected closing fragment tag does not match opening \"a\" tag\n<stdin>: NOTE: The opening \"a\" tag is here:\n")
expectParseErrorJSX(t, "<a></b>",
"<stdin>: ERROR: Unexpected closing \"b\" tag does not match opening \"a\" tag\n<stdin>: NOTE: The opening \"a\" tag is here:\n")
expectParseErrorJSX(t, "<\na\n.\nb\n>\n<\n/\nc\n.\nd\n>",
"<stdin>: ERROR: Expected closing \"c.d\" tag to match opening \"a.b\" tag\n<stdin>: NOTE: The opening \"a.b\" tag is here:\n")
"<stdin>: ERROR: Unexpected closing \"c.d\" tag does not match opening \"a.b\" tag\n<stdin>: NOTE: The opening \"a.b\" tag is here:\n")
expectParseErrorJSX(t, "<a-b.c>", "<stdin>: ERROR: Expected \">\" but found \".\"\n")
expectParseErrorJSX(t, "<a.b-c>", "<stdin>: ERROR: Unexpected \"-\"\n")

Expand Down
12 changes: 10 additions & 2 deletions internal/js_parser/ts_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,9 @@ const (

// TypeScript 5.0
allowConstModifier

// Allow "<>" without any type parameters
allowEmptyTypeParameters
)

type skipTypeScriptTypeParametersResult uint8
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
17 changes: 17 additions & 0 deletions internal/js_parser/ts_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,23 @@ func TestTSTypes(t *testing.T) {
expectPrintedTS(t, "type Foo = Array<<T>(x: T) => T>\n x", "x;\n")
expectPrintedTSX(t, "<Foo<<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<> {}", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTSX(t, "class Foo<> {}", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "class Foo { foo<>() {} }", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTSX(t, "class Foo { foo<>() {} }", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "type Foo = { foo<>(): void }", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTSX(t, "type Foo = { foo<>(): void }", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "type Foo = <>() => {}", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTSX(t, "type Foo = <>() => {}", "<stdin>: ERROR: Expected identifier but found \">\"\n")
expectParseErrorTS(t, "let Foo = <>() => {}", "<stdin>: ERROR: Unexpected \">\"\n")
expectParseErrorTSX(t, "let Foo = <>() => {}",
"<stdin>: ERROR: The character \">\" is not valid inside a JSX element\nNOTE: Did you mean to escape it as \"{'>'}\" instead?\n"+
"<stdin>: ERROR: Unexpected end of file before a closing fragment tag\n<stdin>: 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")
Expand Down
Loading