From 6ccb2a5ef21fb9962114eb4fb91edec99d42cf64 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Fri, 16 Oct 2015 13:47:57 -0700 Subject: [PATCH] Better error recovery for adjacent JSX elements in expression positions Fixes #5286 --- src/compiler/checker.ts | 3 -- src/compiler/diagnosticMessages.json | 4 +++ src/compiler/parser.ts | 28 +++++++++++++++-- .../jsxEsprimaFbTestSuite.errors.txt | 9 ++---- .../reference/jsxEsprimaFbTestSuite.js | 4 +-- .../jsxInvalidEsprimaTestSuite.errors.txt | 30 ++++--------------- .../reference/jsxInvalidEsprimaTestSuite.js | 10 +++---- .../reference/tsxErrorRecovery2.errors.txt | 18 +++++++++++ .../baselines/reference/tsxErrorRecovery2.js | 19 ++++++++++++ .../reference/tsxErrorRecovery3.errors.txt | 30 +++++++++++++++++++ .../baselines/reference/tsxErrorRecovery3.js | 19 ++++++++++++ .../conformance/jsx/tsxErrorRecovery2.tsx | 10 +++++++ .../conformance/jsx/tsxErrorRecovery3.tsx | 10 +++++++ 13 files changed, 151 insertions(+), 43 deletions(-) create mode 100644 tests/baselines/reference/tsxErrorRecovery2.errors.txt create mode 100644 tests/baselines/reference/tsxErrorRecovery2.js create mode 100644 tests/baselines/reference/tsxErrorRecovery3.errors.txt create mode 100644 tests/baselines/reference/tsxErrorRecovery3.js create mode 100644 tests/cases/conformance/jsx/tsxErrorRecovery2.tsx create mode 100644 tests/cases/conformance/jsx/tsxErrorRecovery3.tsx diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b13520cee2d..85817734fd7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7510,9 +7510,6 @@ namespace ts { case SyntaxKind.JsxSelfClosingElement: checkJsxSelfClosingElement(child); break; - default: - // No checks for JSX Text - Debug.assert(child.kind === SyntaxKind.JsxText); } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index a8ed6ac6127..9ec439d96fe 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -1724,6 +1724,10 @@ "category": "Error", "code": 2656 }, + "JSX expressions must have one parent element": { + "category": "Error", + "code": 2657 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", "code": 4000 diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 1551706a337..f021251f7d9 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -3454,19 +3454,43 @@ namespace ts { function parseJsxElementOrSelfClosingElement(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement { let opening = parseJsxOpeningOrSelfClosingElement(inExpressionContext); + let result: JsxElement | JsxSelfClosingElement; if (opening.kind === SyntaxKind.JsxOpeningElement) { let node = createNode(SyntaxKind.JsxElement, opening.pos); node.openingElement = opening; node.children = parseJsxChildren(node.openingElement.tagName); node.closingElement = parseJsxClosingElement(inExpressionContext); - return finishNode(node); + result = finishNode(node); } else { Debug.assert(opening.kind === SyntaxKind.JsxSelfClosingElement); // Nothing else to do for self-closing elements - return opening; + result = opening; } + + // If the user writes the invalid code '
' in an expression context (i.e. not wrapped in + // an enclosing tag), we'll naively try to parse ^ this as a 'less than' operator and the remainder of the tag + // as garbage, which will cause the formatter to badly mangle the JSX. Perform a speculative parse of a JSX + // element if we see a < token so that we can wrap it in a synthetic binary expression so the formatter + // does less damage and we can report a better error. + // Since JSX elements are invalid < operands anyway, this lookahead parse will only occur in error scenarios + // of one sort or another. + if (inExpressionContext && token === SyntaxKind.LessThanToken) { + let invalidElement = tryParse(() => parseJsxElementOrSelfClosingElement(/*inExpressionContext*/true)); + if (invalidElement) { + parseErrorAtCurrentToken(Diagnostics.JSX_expressions_must_have_one_parent_element); + let badNode = createNode(SyntaxKind.BinaryExpression, result.pos); + badNode.end = invalidElement.end; + badNode.left = result; + badNode.right = invalidElement; + badNode.operatorToken = createMissingNode(SyntaxKind.CommaToken, /*reportAtCurrentPosition*/ false, /*diagnosticMessage*/ undefined); + badNode.operatorToken.pos = badNode.operatorToken.end = badNode.right.pos; + return badNode; + } + } + + return result; } function parseJsxText(): JsxText { diff --git a/tests/baselines/reference/jsxEsprimaFbTestSuite.errors.txt b/tests/baselines/reference/jsxEsprimaFbTestSuite.errors.txt index 5758db36960..3a6059d8d07 100644 --- a/tests/baselines/reference/jsxEsprimaFbTestSuite.errors.txt +++ b/tests/baselines/reference/jsxEsprimaFbTestSuite.errors.txt @@ -4,11 +4,10 @@ tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,29): error TS1005: '{' tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,57): error TS1109: Expression expected. tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(39,58): error TS1109: Expression expected. tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,1): error TS1003: Identifier expected. -tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,6): error TS1109: Expression expected. -tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS1109: Expression expected. +tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS2657: JSX expressions must have one parent element -==== tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx (8 errors) ==== +==== tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx (7 errors) ==== declare var React: any; declare var 日本語; declare var AbC_def; @@ -62,10 +61,8 @@ tests/cases/conformance/jsx/jsxEsprimaFbTestSuite.tsx(41,12): error TS1109: Expr ; ~ !!! error TS1003: Identifier expected. - ~~ -!!! error TS1109: Expression expected. ~ -!!! error TS1109: Expression expected. +!!! error TS2657: JSX expressions must have one parent element ; diff --git a/tests/baselines/reference/jsxEsprimaFbTestSuite.js b/tests/baselines/reference/jsxEsprimaFbTestSuite.js index 1e22826f440..465d265efb2 100644 --- a/tests/baselines/reference/jsxEsprimaFbTestSuite.js +++ b/tests/baselines/reference/jsxEsprimaFbTestSuite.js @@ -72,8 +72,8 @@ baz
@test content
;

7x invalid-js-identifier
; } right={monkeys /> gorillas / > }/> - < a.b > ; -a.b > ; + , + ; ; (
) < x;
; diff --git a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.errors.txt b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.errors.txt index 02bc56e823e..578159dc651 100644 --- a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.errors.txt +++ b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.errors.txt @@ -12,17 +12,11 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,6): error TS2304: C tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,9): error TS1109: Expression expected. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(6,10): error TS1109: Expression expected. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,1): error TS1003: Identifier expected. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,2): error TS2304: Cannot find name 'a'. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(7,4): error TS1109: Expression expected. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(8,4): error TS17002: Expected corresponding JSX closing tag for 'a'. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(9,13): error TS1002: Unterminated string literal. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,1): error TS1003: Identifier expected. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,2): error TS2304: Cannot find name 'a'. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,3): error TS1005: ';' expected. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,4): error TS2304: Cannot find name 'b'. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,6): error TS1109: Expression expected. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,8): error TS2304: Cannot find name 'b'. -tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,10): error TS1109: Expression expected. +tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,6): error TS17002: Expected corresponding JSX closing tag for 'a'. +tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(10,10): error TS2657: JSX expressions must have one parent element tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(11,3): error TS1003: Identifier expected. tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(11,5): error TS1003: Identifier expected. 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: tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002: Expected corresponding JSX closing tag for 'a'. -==== tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx (71 errors) ==== +==== tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx (65 errors) ==== declare var React: any; ; @@ -107,10 +101,6 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002 ; ~ !!! error TS1003: Identifier expected. - ~ -!!! error TS2304: Cannot find name 'a'. - ~ -!!! error TS1109: Expression expected. ; ~~~~ !!! error TS17002: Expected corresponding JSX closing tag for 'a'. @@ -120,18 +110,10 @@ tests/cases/conformance/jsx/jsxInvalidEsprimaTestSuite.tsx(35,21): error TS17002 ; ~ !!! error TS1003: Identifier expected. - ~ -!!! error TS2304: Cannot find name 'a'. - ~ -!!! error TS1005: ';' expected. - ~ -!!! error TS2304: Cannot find name 'b'. - ~~ -!!! error TS1109: Expression expected. - ~ -!!! error TS2304: Cannot find name 'b'. + ~~~~ +!!! error TS17002: Expected corresponding JSX closing tag for 'a'. ~ -!!! error TS1109: Expression expected. +!!! error TS2657: JSX expressions must have one parent element ; ~ !!! error TS1003: Identifier expected. diff --git a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js index d623e127be7..7f01666bd78 100644 --- a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js +++ b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js @@ -41,12 +41,10 @@ var x =
one
/* intervening comment */
two
;; < ; a / > ;
}/> - < a > ; -; -a:b>; ; b.c > ; ; diff --git a/tests/baselines/reference/tsxErrorRecovery2.errors.txt b/tests/baselines/reference/tsxErrorRecovery2.errors.txt new file mode 100644 index 00000000000..9a0b5790b2c --- /dev/null +++ b/tests/baselines/reference/tsxErrorRecovery2.errors.txt @@ -0,0 +1,18 @@ +tests/cases/conformance/jsx/file1.tsx(6,1): error TS2657: JSX expressions must have one parent element +tests/cases/conformance/jsx/file2.tsx(2,1): error TS2657: JSX expressions must have one parent element + + +==== tests/cases/conformance/jsx/file1.tsx (1 errors) ==== + + declare namespace JSX { interface Element { } } + +
+
+ + +!!! error TS2657: JSX expressions must have one parent element +==== tests/cases/conformance/jsx/file2.tsx (1 errors) ==== + var x =
+ + +!!! error TS2657: JSX expressions must have one parent element \ No newline at end of file diff --git a/tests/baselines/reference/tsxErrorRecovery2.js b/tests/baselines/reference/tsxErrorRecovery2.js new file mode 100644 index 00000000000..be5b6c73424 --- /dev/null +++ b/tests/baselines/reference/tsxErrorRecovery2.js @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/jsx/tsxErrorRecovery2.tsx] //// + +//// [file1.tsx] + +declare namespace JSX { interface Element { } } + +
+
+ +//// [file2.tsx] +var x =
+ + +//// [file1.jsx] +
+ , +
; +//// [file2.jsx] +var x =
,
; diff --git a/tests/baselines/reference/tsxErrorRecovery3.errors.txt b/tests/baselines/reference/tsxErrorRecovery3.errors.txt new file mode 100644 index 00000000000..f7e4bc170a9 --- /dev/null +++ b/tests/baselines/reference/tsxErrorRecovery3.errors.txt @@ -0,0 +1,30 @@ +tests/cases/conformance/jsx/file1.tsx(4,2): error TS2304: Cannot find name 'React'. +tests/cases/conformance/jsx/file1.tsx(5,2): error TS2304: Cannot find name 'React'. +tests/cases/conformance/jsx/file1.tsx(6,1): error TS2657: JSX expressions must have one parent element +tests/cases/conformance/jsx/file2.tsx(1,10): error TS2304: Cannot find name 'React'. +tests/cases/conformance/jsx/file2.tsx(1,21): error TS2304: Cannot find name 'React'. +tests/cases/conformance/jsx/file2.tsx(2,1): error TS2657: JSX expressions must have one parent element + + +==== tests/cases/conformance/jsx/file1.tsx (3 errors) ==== + + declare namespace JSX { interface Element { } } + +
+ ~~~ +!!! error TS2304: Cannot find name 'React'. +
+ ~~~ +!!! error TS2304: Cannot find name 'React'. + + +!!! error TS2657: JSX expressions must have one parent element +==== tests/cases/conformance/jsx/file2.tsx (3 errors) ==== + var x =
+ ~~~ +!!! error TS2304: Cannot find name 'React'. + ~~~ +!!! error TS2304: Cannot find name 'React'. + + +!!! error TS2657: JSX expressions must have one parent element \ No newline at end of file diff --git a/tests/baselines/reference/tsxErrorRecovery3.js b/tests/baselines/reference/tsxErrorRecovery3.js new file mode 100644 index 00000000000..93b34a14c19 --- /dev/null +++ b/tests/baselines/reference/tsxErrorRecovery3.js @@ -0,0 +1,19 @@ +//// [tests/cases/conformance/jsx/tsxErrorRecovery3.tsx] //// + +//// [file1.tsx] + +declare namespace JSX { interface Element { } } + +
+
+ +//// [file2.tsx] +var x =
+ + +//// [file1.js] +React.createElement("div", null) + , + React.createElement("div", null); +//// [file2.js] +var x = React.createElement("div", null), React.createElement("div", null); diff --git a/tests/cases/conformance/jsx/tsxErrorRecovery2.tsx b/tests/cases/conformance/jsx/tsxErrorRecovery2.tsx new file mode 100644 index 00000000000..e189e0e7791 --- /dev/null +++ b/tests/cases/conformance/jsx/tsxErrorRecovery2.tsx @@ -0,0 +1,10 @@ +//@jsx: preserve + +//@filename: file1.tsx +declare namespace JSX { interface Element { } } + +
+
+ +//@filename: file2.tsx +var x =
diff --git a/tests/cases/conformance/jsx/tsxErrorRecovery3.tsx b/tests/cases/conformance/jsx/tsxErrorRecovery3.tsx new file mode 100644 index 00000000000..9d444b07cb2 --- /dev/null +++ b/tests/cases/conformance/jsx/tsxErrorRecovery3.tsx @@ -0,0 +1,10 @@ +//@jsx: react + +//@filename: file1.tsx +declare namespace JSX { interface Element { } } + +
+
+ +//@filename: file2.tsx +var x =