Skip to content

Commit 6ccb2a5

Browse files
committed
Better error recovery for adjacent JSX elements in expression positions
Fixes microsoft#5286
1 parent 302db0a commit 6ccb2a5

13 files changed

+151
-43
lines changed

src/compiler/checker.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7510,9 +7510,6 @@ namespace ts {
75107510
case SyntaxKind.JsxSelfClosingElement:
75117511
checkJsxSelfClosingElement(<JsxSelfClosingElement>child);
75127512
break;
7513-
default:
7514-
// No checks for JSX Text
7515-
Debug.assert(child.kind === SyntaxKind.JsxText);
75167513
}
75177514
}
75187515

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,10 @@
17241724
"category": "Error",
17251725
"code": 2656
17261726
},
1727+
"JSX expressions must have one parent element": {
1728+
"category": "Error",
1729+
"code": 2657
1730+
},
17271731
"Import declaration '{0}' is using private name '{1}'.": {
17281732
"category": "Error",
17291733
"code": 4000

src/compiler/parser.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3454,19 +3454,43 @@ namespace ts {
34543454

34553455
function parseJsxElementOrSelfClosingElement(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement {
34563456
let opening = parseJsxOpeningOrSelfClosingElement(inExpressionContext);
3457+
let result: JsxElement | JsxSelfClosingElement;
34573458
if (opening.kind === SyntaxKind.JsxOpeningElement) {
34583459
let node = <JsxElement>createNode(SyntaxKind.JsxElement, opening.pos);
34593460
node.openingElement = opening;
34603461

34613462
node.children = parseJsxChildren(node.openingElement.tagName);
34623463
node.closingElement = parseJsxClosingElement(inExpressionContext);
3463-
return finishNode(node);
3464+
result = finishNode(node);
34643465
}
34653466
else {
34663467
Debug.assert(opening.kind === SyntaxKind.JsxSelfClosingElement);
34673468
// Nothing else to do for self-closing elements
3468-
return <JsxSelfClosingElement>opening;
3469+
result = <JsxSelfClosingElement>opening;
3470+
}
3471+
3472+
// If the user writes the invalid code '<div></div><div></div>' in an expression context (i.e. not wrapped in
3473+
// an enclosing tag), we'll naively try to parse ^ this as a 'less than' operator and the remainder of the tag
3474+
// as garbage, which will cause the formatter to badly mangle the JSX. Perform a speculative parse of a JSX
3475+
// element if we see a < token so that we can wrap it in a synthetic binary expression so the formatter
3476+
// does less damage and we can report a better error.
3477+
// Since JSX elements are invalid < operands anyway, this lookahead parse will only occur in error scenarios
3478+
// of one sort or another.
3479+
if (inExpressionContext && token === SyntaxKind.LessThanToken) {
3480+
let invalidElement = tryParse(() => parseJsxElementOrSelfClosingElement(/*inExpressionContext*/true));
3481+
if (invalidElement) {
3482+
parseErrorAtCurrentToken(Diagnostics.JSX_expressions_must_have_one_parent_element);
3483+
let badNode = <BinaryExpression>createNode(SyntaxKind.BinaryExpression, result.pos);
3484+
badNode.end = invalidElement.end;
3485+
badNode.left = result;
3486+
badNode.right = invalidElement;
3487+
badNode.operatorToken = createMissingNode(SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false, /*diagnosticMessage*/ undefined);
3488+
badNode.operatorToken.pos = badNode.operatorToken.end = badNode.right.pos;
3489+
return <JsxElement><Node>badNode;
3490+
}
34693491
}
3492+
3493+
return result;
34703494
}
34713495

34723496
function parseJsxText(): JsxText {

tests/baselines/reference/jsxEsprimaFbTestSuite.errors.txt

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,29): error TS1005: '{'
44
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,57): error TS1109: Expression expected.
55
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,58): error TS1109: Expression expected.
66
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,1): error TS1003: Identifier expected.
7-
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,6): error TS1109: Expression expected.
8-
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS1109: Expression expected.
7+
tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS2657: JSX expressions must have one parent element
98

109

11-
==== tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx (8 errors) ====
10+
==== tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx (7 errors) ====
1211
declare var React: any;
1312
declare var 日本語;
1413
declare var AbC_def;
@@ -62,10 +61,8 @@ tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS1109: Expr
6261
<a.b></a.b>;
6362
~
6463
!!! error TS1003: Identifier expected.
65-
~~
66-
!!! error TS1109: Expression expected.
6764
~
68-
!!! error TS1109: Expression expected.
65+
!!! error TS2657: JSX expressions must have one parent element
6966

7067
<a.b.c></a.b.c>;
7168

tests/baselines/reference/jsxEsprimaFbTestSuite.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ baz
7272
<div>@test content</div>;
7373
<div><br />7x invalid-js-identifier</div>;
7474
<LeftRight left={<a />} right={<b>monkeys /> gorillas</b> / > }/>
75-
< a.b > ;
76-
a.b > ;
75+
,
76+
<a.b></a.b>;
7777
<a.b.c></a.b.c>;
7878
(<div />) < x;
7979
<div {...props}/>;

tests/baselines/reference/jsxInvalidEsprimaTestSuite.errors.txt

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,11 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,6): error TS2304: C
1212
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,9): error TS1109: Expression expected.
1313
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,10): error TS1109: Expression expected.
1414
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,1): error TS1003: Identifier expected.
15-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,2): error TS2304: Cannot find name 'a'.
16-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,4): error TS1109: Expression expected.
1715
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(8,4): error TS17002: Expected corresponding JSX closing tag for 'a'.
1816
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(9,13): error TS1002: Unterminated string literal.
1917
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,1): error TS1003: Identifier expected.
20-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,2): error TS2304: Cannot find name 'a'.
21-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,3): error TS1005: ';' expected.
22-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,4): error TS2304: Cannot find name 'b'.
23-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,6): error TS1109: Expression expected.
24-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,8): error TS2304: Cannot find name 'b'.
25-
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,10): error TS1109: Expression expected.
18+
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,6): error TS17002: Expected corresponding JSX closing tag for 'a'.
19+
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,10): error TS2657: JSX expressions must have one parent element
2620
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(11,3): error TS1003: Identifier expected.
2721
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(11,5): error TS1003: Identifier expected.
2822
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(11,11): error TS1005: '>' expected.
@@ -71,7 +65,7 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,4): error TS1003:
7165
tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002: Expected corresponding JSX closing tag for 'a'.
7266

7367

74-
==== tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx (71 errors) ====
68+
==== tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx (65 errors) ====
7569
declare var React: any;
7670

7771
</>;
@@ -107,10 +101,6 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002
107101
<a>;
108102
~
109103
!!! error TS1003: Identifier expected.
110-
~
111-
!!! error TS2304: Cannot find name 'a'.
112-
~
113-
!!! error TS1109: Expression expected.
114104
<a></b>;
115105
~~~~
116106
!!! error TS17002: Expected corresponding JSX closing tag for 'a'.
@@ -120,18 +110,10 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002
120110
<a:b></b>;
121111
~
122112
!!! error TS1003: Identifier expected.
123-
~
124-
!!! error TS2304: Cannot find name 'a'.
125-
~
126-
!!! error TS1005: ';' expected.
127-
~
128-
!!! error TS2304: Cannot find name 'b'.
129-
~~
130-
!!! error TS1109: Expression expected.
131-
~
132-
!!! error TS2304: Cannot find name 'b'.
113+
~~~~
114+
!!! error TS17002: Expected corresponding JSX closing tag for 'a'.
133115
~
134-
!!! error TS1109: Expression expected.
116+
!!! error TS2657: JSX expressions must have one parent element
135117
<a:b.c></a:b.c>;
136118
~
137119
!!! error TS1003: Identifier expected.

tests/baselines/reference/jsxInvalidEsprimaTestSuite.js

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@ var x = <div>one</div> /* intervening comment */ <div>two</div>;;
4141
< ;
4242
a / > ;
4343
<a b={d / > }/>
44-
< a > ;
45-
<a></b>;
46-
<a foo="bar;/>
47-
< a;
48-
b > ;
49-
b > ;
44+
,
45+
<a>;
46+
<a></b>;
47+
<a foo="bar;/>a:b></b>;
5048
<a b c></a>;
5149
b.c > ;
5250
<a.b c></a.b>;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
tests/cases/conformance/jsx/file1.tsx(6,1): error TS2657: JSX expressions must have one parent element
2+
tests/cases/conformance/jsx/file2.tsx(2,1): error TS2657: JSX expressions must have one parent element
3+
4+
5+
==== tests/cases/conformance/jsx/file1.tsx (1 errors) ====
6+
7+
declare namespace JSX { interface Element { } }
8+
9+
<div></div>
10+
<div></div>
11+
12+
13+
!!! error TS2657: JSX expressions must have one parent element
14+
==== tests/cases/conformance/jsx/file2.tsx (1 errors) ====
15+
var x = <div></div><div></div>
16+
17+
18+
!!! error TS2657: JSX expressions must have one parent element
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//// [tests/cases/conformance/jsx/tsxErrorRecovery2.tsx] ////
2+
3+
//// [file1.tsx]
4+
5+
declare namespace JSX { interface Element { } }
6+
7+
<div></div>
8+
<div></div>
9+
10+
//// [file2.tsx]
11+
var x = <div></div><div></div>
12+
13+
14+
//// [file1.jsx]
15+
<div></div>
16+
,
17+
<div></div>;
18+
//// [file2.jsx]
19+
var x = <div></div>, <div></div>;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
tests/cases/conformance/jsx/file1.tsx(4,2): error TS2304: Cannot find name 'React'.
2+
tests/cases/conformance/jsx/file1.tsx(5,2): error TS2304: Cannot find name 'React'.
3+
tests/cases/conformance/jsx/file1.tsx(6,1): error TS2657: JSX expressions must have one parent element
4+
tests/cases/conformance/jsx/file2.tsx(1,10): error TS2304: Cannot find name 'React'.
5+
tests/cases/conformance/jsx/file2.tsx(1,21): error TS2304: Cannot find name 'React'.
6+
tests/cases/conformance/jsx/file2.tsx(2,1): error TS2657: JSX expressions must have one parent element
7+
8+
9+
==== tests/cases/conformance/jsx/file1.tsx (3 errors) ====
10+
11+
declare namespace JSX { interface Element { } }
12+
13+
<div></div>
14+
~~~
15+
!!! error TS2304: Cannot find name 'React'.
16+
<div></div>
17+
~~~
18+
!!! error TS2304: Cannot find name 'React'.
19+
20+
21+
!!! error TS2657: JSX expressions must have one parent element
22+
==== tests/cases/conformance/jsx/file2.tsx (3 errors) ====
23+
var x = <div></div><div></div>
24+
~~~
25+
!!! error TS2304: Cannot find name 'React'.
26+
~~~
27+
!!! error TS2304: Cannot find name 'React'.
28+
29+
30+
!!! error TS2657: JSX expressions must have one parent element
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//// [tests/cases/conformance/jsx/tsxErrorRecovery3.tsx] ////
2+
3+
//// [file1.tsx]
4+
5+
declare namespace JSX { interface Element { } }
6+
7+
<div></div>
8+
<div></div>
9+
10+
//// [file2.tsx]
11+
var x = <div></div><div></div>
12+
13+
14+
//// [file1.js]
15+
React.createElement("div", null)
16+
,
17+
React.createElement("div", null);
18+
//// [file2.js]
19+
var x = React.createElement("div", null), React.createElement("div", null);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//@jsx: preserve
2+
3+
//@filename: file1.tsx
4+
declare namespace JSX { interface Element { } }
5+
6+
<div></div>
7+
<div></div>
8+
9+
//@filename: file2.tsx
10+
var x = <div></div><div></div>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//@jsx: react
2+
3+
//@filename: file1.tsx
4+
declare namespace JSX { interface Element { } }
5+
6+
<div></div>
7+
<div></div>
8+
9+
//@filename: file2.tsx
10+
var x = <div></div><div></div>

0 commit comments

Comments
 (0)