Skip to content

Commit 78e0808

Browse files
committed
warn about } and > inside JSX elements
1 parent 11ed34e commit 78e0808

File tree

6 files changed

+91
-14
lines changed

6 files changed

+91
-14
lines changed

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@
2626

2727
There's a proposed [CSS syntax for nesting rules](https://drafts.csswg.org/css-nesting/) using the `&` selector, but it's not currently implemented in any browser. Previously esbuild silently passed the syntax through untransformed. With this release, esbuild will now warn when you use nesting syntax with a `--target=` setting that includes a browser.
2828

29+
* Warn about `}` and `>` inside JSX elements
30+
31+
The `}` and `>` characters are invalid inside JSX elements according to [the JSX specification](https://facebook.github.io/jsx/) because they commonly result from typos like these that are hard to catch in code reviews:
32+
33+
```jsx
34+
function F() {
35+
return <div>></div>;
36+
}
37+
function G() {
38+
return <div>{1}}</div>;
39+
}
40+
```
41+
42+
The TypeScript compiler already [treats this as an error](https://github.com/microsoft/TypeScript/issues/36341), so esbuild now treats this as an error in TypeScript files too. That looks like this:
43+
44+
```
45+
✘ [ERROR] The character ">" is not valid inside a JSX element, but can be escaped as "{'>'}" instead
46+
47+
example.tsx:2:14:
48+
2 │ return <div>></div>;
49+
│ ^
50+
╵ {'>'}
51+
52+
✘ [ERROR] The character "}" is not valid inside a JSX element, but can be escaped as "{'}'}" instead
53+
54+
example.tsx:5:17:
55+
5 │ return <div>{1}}</div>;
56+
│ ^
57+
╵ {'}'}
58+
```
59+
60+
Babel doesn't yet treat this as an error, so esbuild only warns about these characters in JavaScript files for now. Babel 8 [treats this as an error](https://github.com/babel/babel/issues/11042) but Babel 8 [hasn't been released yet](https://github.com/babel/babel/issues/10746). If you see this warning, I recommend fixing the invalid JSX syntax because it will become an error in the future.
61+
2962
## 0.14.11
3063

3164
* Fix a bug with enum inlining ([#1903](https://github.com/evanw/esbuild/issues/1903))

internal/js_lexer/js_lexer.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"unicode"
2121
"unicode/utf8"
2222

23+
"github.com/evanw/esbuild/internal/config"
2324
"github.com/evanw/esbuild/internal/helpers"
2425
"github.com/evanw/esbuild/internal/js_ast"
2526
"github.com/evanw/esbuild/internal/logger"
@@ -250,6 +251,7 @@ type Lexer struct {
250251
prevErrorLoc logger.Loc
251252
json json
252253
Token T
254+
ts config.TSOptions
253255
HasNewlineBefore bool
254256
HasPureCommentBefore bool
255257
PreserveAllCommentsBefore bool
@@ -264,13 +266,14 @@ type Lexer struct {
264266

265267
type LexerPanic struct{}
266268

267-
func NewLexer(log logger.Log, source logger.Source) Lexer {
269+
func NewLexer(log logger.Log, source logger.Source, ts config.TSOptions) Lexer {
268270
lexer := Lexer{
269271
log: log,
270272
source: source,
271273
tracker: logger.MakeLineColumnTracker(&source),
272274
prevErrorLoc: logger.Loc{Start: -1},
273275
FnOrArrowStartLoc: logger.Loc{Start: -1},
276+
ts: ts,
274277
}
275278
lexer.step()
276279
lexer.Next()
@@ -898,6 +901,37 @@ func (lexer *Lexer) NextJSXElementChild() {
898901
// Stop when the string ends
899902
break stringLiteral
900903

904+
case '}', '>':
905+
// These technically aren't valid JSX: https://facebook.github.io/jsx/
906+
//
907+
// JSXTextCharacter :
908+
// * SourceCharacter but not one of {, <, > or }
909+
//
910+
var replacement string
911+
if lexer.codePoint == '}' {
912+
replacement = "{'}'}"
913+
} else {
914+
replacement = "{'>'}"
915+
}
916+
msg := logger.Msg{Kind: logger.Error, Data: lexer.tracker.MsgData(logger.Range{Loc: logger.Loc{Start: int32(lexer.end)}, Len: 1},
917+
fmt.Sprintf("The character \"%c\" is not valid inside a JSX element, but can be escaped as %q instead", lexer.codePoint, replacement))}
918+
msg.Data.Location.Suggestion = replacement
919+
if !lexer.ts.Parse {
920+
// TypeScript treats this as an error but Babel doesn't treat this
921+
// as an error yet, so allow this in JS for now. Babel version 8
922+
// was supposed to be released in 2021 but was never released. If
923+
// it's released in the future, this can be changed to an error too.
924+
//
925+
// More context:
926+
// * TypeScript change: https://github.com/microsoft/TypeScript/issues/36341
927+
// * Babel 8 change: https://github.com/babel/babel/issues/11042
928+
// * Babel 8 release: https://github.com/babel/babel/issues/10746
929+
//
930+
msg.Kind = logger.Warning
931+
}
932+
lexer.log.AddMsg(msg)
933+
lexer.step()
934+
901935
default:
902936
// Non-ASCII strings need the slow path
903937
if lexer.codePoint >= 0x80 {

internal/js_lexer/js_lexer_test.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"unicode/utf8"
99

10+
"github.com/evanw/esbuild/internal/config"
1011
"github.com/evanw/esbuild/internal/logger"
1112
"github.com/evanw/esbuild/internal/test"
1213
)
@@ -32,7 +33,7 @@ func assertEqualStrings(t *testing.T, a string, b string) {
3233

3334
func lexToken(t *testing.T, contents string) T {
3435
log := logger.NewDeferLog(logger.DeferLogNoVerboseOrDebug)
35-
lexer := NewLexer(log, test.SourceForTest(contents))
36+
lexer := NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
3637
return lexer.Token
3738
}
3839

@@ -48,7 +49,7 @@ func expectLexerError(t *testing.T, contents string, expected string) {
4849
panic(r)
4950
}
5051
}()
51-
NewLexer(log, test.SourceForTest(contents))
52+
NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
5253
}()
5354
msgs := log.Done()
5455
text := ""
@@ -78,7 +79,7 @@ func expectHashbang(t *testing.T, contents string, expected string) {
7879
panic(r)
7980
}
8081
}()
81-
return NewLexer(log, test.SourceForTest(contents))
82+
return NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
8283
}()
8384
msgs := log.Done()
8485
test.AssertEqual(t, len(msgs), 0)
@@ -106,7 +107,7 @@ func expectIdentifier(t *testing.T, contents string, expected string) {
106107
panic(r)
107108
}
108109
}()
109-
return NewLexer(log, test.SourceForTest(contents))
110+
return NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
110111
}()
111112
msgs := log.Done()
112113
test.AssertEqual(t, len(msgs), 0)
@@ -145,7 +146,7 @@ func expectNumber(t *testing.T, contents string, expected float64) {
145146
panic(r)
146147
}
147148
}()
148-
return NewLexer(log, test.SourceForTest(contents))
149+
return NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
149150
}()
150151
msgs := log.Done()
151152
test.AssertEqual(t, len(msgs), 0)
@@ -352,7 +353,7 @@ func expectBigInteger(t *testing.T, contents string, expected string) {
352353
panic(r)
353354
}
354355
}()
355-
return NewLexer(log, test.SourceForTest(contents))
356+
return NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
356357
}()
357358
msgs := log.Done()
358359
test.AssertEqual(t, len(msgs), 0)
@@ -407,7 +408,7 @@ func expectString(t *testing.T, contents string, expected string) {
407408
panic(r)
408409
}
409410
}()
410-
return NewLexer(log, test.SourceForTest(contents))
411+
return NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
411412
}()
412413
text := lexer.StringLiteral()
413414
msgs := log.Done()
@@ -429,7 +430,7 @@ func expectLexerErrorString(t *testing.T, contents string, expected string) {
429430
panic(r)
430431
}
431432
}()
432-
lexer := NewLexer(log, test.SourceForTest(contents))
433+
lexer := NewLexer(log, test.SourceForTest(contents), config.TSOptions{})
433434
lexer.StringLiteral()
434435
}()
435436
msgs := log.Done()

internal/js_parser/js_parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14601,7 +14601,7 @@ func Parse(log logger.Log, source logger.Source, options Options) (result js_ast
1460114601
options.unsupportedJSFeatures |= options.tsTarget.UnsupportedJSFeatures
1460214602
}
1460314603

14604-
p := newParser(log, source, js_lexer.NewLexer(log, source), &options)
14604+
p := newParser(log, source, js_lexer.NewLexer(log, source, options.ts), &options)
1460514605

1460614606
// Consume a leading hashbang comment
1460714607
hashbang := ""

internal/js_parser/js_parser_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4242,6 +4242,11 @@ func TestNewTarget(t *testing.T) {
42424242
}
42434243

42444244
func TestJSX(t *testing.T) {
4245+
expectParseErrorJSX(t, "<div>></div>", "<stdin>: WARNING: The character \">\" is not valid inside a JSX element, but can be escaped as \"{'>'}\" instead\n")
4246+
expectParseErrorJSX(t, "<div>{1}}</div>", "<stdin>: WARNING: The character \"}\" is not valid inside a JSX element, but can be escaped as \"{'}'}\" instead\n")
4247+
expectPrintedJSX(t, "<div>></div>", "/* @__PURE__ */ React.createElement(\"div\", null, \">\");\n")
4248+
expectPrintedJSX(t, "<div>{1}}</div>", "/* @__PURE__ */ React.createElement(\"div\", null, 1, \"}\");\n")
4249+
42454250
expectParseError(t, "<a/>", "<stdin>: ERROR: The JSX syntax extension is not currently enabled\n"+
42464251
"NOTE: The esbuild loader for this file is currently set to \"js\" but it must be set to \"jsx\" to be able to parse JSX syntax. "+
42474252
"You can use 'Loader: map[string]api.Loader{\".js\": api.LoaderJSX}' to do that.\n")

internal/js_parser/ts_parser_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,9 @@ func TestTSOptionalChain(t *testing.T) {
17961796
}
17971797

17981798
func TestTSJSX(t *testing.T) {
1799+
expectParseErrorTSX(t, "<div>></div>", "<stdin>: ERROR: The character \">\" is not valid inside a JSX element, but can be escaped as \"{'>'}\" instead\n")
1800+
expectParseErrorTSX(t, "<div>{1}}</div>", "<stdin>: ERROR: The character \"}\" is not valid inside a JSX element, but can be escaped as \"{'}'}\" instead\n")
1801+
17991802
expectPrintedTS(t, "const x = <number>1", "const x = 1;\n")
18001803
expectPrintedTSX(t, "const x = <number>1</number>", "const x = /* @__PURE__ */ React.createElement(\"number\", null, \"1\");\n")
18011804
expectParseErrorTSX(t, "const x = <number>1", "<stdin>: ERROR: Unexpected end of file\n")
@@ -1846,15 +1849,16 @@ func TestTSJSX(t *testing.T) {
18461849
expectPrintedTS(t, "const x = <[]>(y, z)", "const x = (y, z);\n")
18471850
expectPrintedTS(t, "const x = <[]>(y, z) => {}", "const x = (y, z) => {\n};\n")
18481851

1849-
expectPrintedTSX(t, "(<T>(y) => {}</T>)", "/* @__PURE__ */ React.createElement(T, null, \"(y) => \");\n")
1850-
expectPrintedTSX(t, "(<T extends>(y) => {}</T>)", "/* @__PURE__ */ React.createElement(T, {\n extends: true\n}, \"(y) => \");\n")
1851-
expectPrintedTSX(t, "(<T extends={false}>(y) => {}</T>)", "/* @__PURE__ */ React.createElement(T, {\n extends: false\n}, \"(y) => \");\n")
1852+
invalid := "<stdin>: ERROR: The character \">\" is not valid inside a JSX element, but can be escaped as \"{'>'}\" instead\n"
1853+
expectParseErrorTSX(t, "(<T>(y) => {}</T>)", invalid)
1854+
expectParseErrorTSX(t, "(<T extends>(y) => {}</T>)", invalid)
1855+
expectParseErrorTSX(t, "(<T extends={false}>(y) => {}</T>)", invalid)
18521856
expectPrintedTSX(t, "(<T extends X>(y) => {})", "(y) => {\n};\n")
18531857
expectPrintedTSX(t, "(<T extends X = Y>(y) => {})", "(y) => {\n};\n")
18541858
expectPrintedTSX(t, "(<T,>() => {})", "() => {\n};\n")
18551859
expectPrintedTSX(t, "(<T, X>(y) => {})", "(y) => {\n};\n")
18561860
expectPrintedTSX(t, "(<T, X>(y): (() => {}) => {})", "(y) => {\n};\n")
1857-
expectParseErrorTSX(t, "(<T>() => {})", "<stdin>: ERROR: Unexpected end of file\n")
1861+
expectParseErrorTSX(t, "(<T>() => {})", invalid+"<stdin>: ERROR: Unexpected end of file\n")
18581862
expectParseErrorTSX(t, "(<[]>(y))", "<stdin>: ERROR: Expected identifier but found \"[\"\n")
18591863
expectParseErrorTSX(t, "(<T[]>(y))", "<stdin>: ERROR: Expected \">\" but found \"[\"\n")
18601864
expectParseErrorTSX(t, "(<T = X>(y))", "<stdin>: ERROR: Expected \">\" but found \"=\"\n")

0 commit comments

Comments
 (0)