diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 18c1f42f838fb..d0efd490816fd 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -19850,6 +19850,7 @@ namespace ts { } function checkJsxExpression(node: JsxExpression, checkMode?: CheckMode) { + checkGrammarJsxExpression(node); if (node.expression) { const type = checkExpression(node.expression, checkMode); if (node.dotDotDotToken && type !== anyType && !isArrayType(type)) { @@ -31658,6 +31659,12 @@ namespace ts { } } + function checkGrammarJsxExpression(node: JsxExpression) { + if (node.expression && isCommaSequence(node.expression)) { + return grammarErrorOnNode(node.expression, Diagnostics.JSX_expressions_may_not_use_the_comma_operator_Did_you_mean_to_write_an_array); + } + } + function checkGrammarForInOrForOfStatement(forInOrOfStatement: ForInOrOfStatement): boolean { if (checkGrammarStatementInAmbientContext(forInOrOfStatement)) { return true; diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 87101781eadf7..97e80f69b11ff 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -4986,5 +4986,9 @@ "Classes may not have a field named 'constructor'.": { "category": "Error", "code": 18006 + }, + "JSX expressions may not use the comma operator. Did you mean to write an array?": { + "category": "Error", + "code": 18007 } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index c38e4f67fe287..e56ff98846ed6 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -4430,14 +4430,18 @@ namespace ts { if (token() !== SyntaxKind.CloseBraceToken) { node.dotDotDotToken = parseOptionalToken(SyntaxKind.DotDotDotToken); - node.expression = parseAssignmentExpressionOrHigher(); + // Only an AssignmentExpression is valid here per the JSX spec, + // but we can unambiguously parse a comma sequence and provide + // a better error message in grammar checking. + node.expression = parseExpression(); } if (inExpressionContext) { parseExpected(SyntaxKind.CloseBraceToken); } else { - parseExpected(SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false); - scanJsxText(); + if (parseExpected(SyntaxKind.CloseBraceToken, /*message*/ undefined, /*shouldAdvance*/ false)) { + scanJsxText(); + } } return finishNode(node); diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index df7f0051f3f1f..5d0725ea106ee 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -582,6 +582,21 @@ namespace FourSlash { }); } + public verifyErrorExistsAtRange(range: Range, code: number, expectedMessage?: string) { + const span = ts.createTextSpanFromRange(range); + const hasMatchingError = ts.some( + this.getDiagnostics(range.fileName), + ({ code, messageText, start, length }) => + code === code && + (!expectedMessage || expectedMessage === messageText) && + ts.isNumber(start) && ts.isNumber(length) && + ts.textSpansEqual(span, { start, length })); + + if (!hasMatchingError) { + this.raiseError(`No error with code ${code} found at provided range.`); + } + } + public verifyNumberOfErrorsInCurrentFile(expected: number) { const errors = this.getDiagnostics(this.activeFile.fileName); const actual = errors.length; @@ -3968,6 +3983,10 @@ namespace FourSlashInterface { this.state.verifyNoErrors(); } + public errorExistsAtRange(range: FourSlash.Range, code: number, message?: string) { + this.state.verifyErrorExistsAtRange(range, code, message); + } + public numberOfErrorsInCurrentFile(expected: number) { this.state.verifyNumberOfErrorsInCurrentFile(expected); } diff --git a/tests/baselines/reference/jsxAndTypeAssertion.js b/tests/baselines/reference/jsxAndTypeAssertion.js index cf4033f4c577b..fb51acd6fd471 100644 --- a/tests/baselines/reference/jsxAndTypeAssertion.js +++ b/tests/baselines/reference/jsxAndTypeAssertion.js @@ -28,18 +28,21 @@ var foo = /** @class */ (function () { return foo; }()); var x; -x = {test} }; +x = {test}: }; x = ; -x = hello {} } +x = hello {} }; x = }>hello}/> -x = }>hello{}} +x = }>hello{}}; x = x, x = ; {{/foo/.test(x) ? : }} : -}}}/>; +} + + +}}/>; diff --git a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js index bf17ca57c0d5b..b04a03079b717 100644 --- a/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js +++ b/tests/baselines/reference/jsxInvalidEsprimaTestSuite.js @@ -123,7 +123,7 @@ var x =
one
,
two
; var x =
one
/* intervening comment */, /* intervening comment */
two
; ; //// [20.jsx] -{"str"}}; +{"str"};}; //// [21.jsx] ; //// [22.jsx] diff --git a/tests/baselines/reference/jsxParsingError1.errors.txt b/tests/baselines/reference/jsxParsingError1.errors.txt index dec1228fd72e1..a3e4e0b4f25f2 100644 --- a/tests/baselines/reference/jsxParsingError1.errors.txt +++ b/tests/baselines/reference/jsxParsingError1.errors.txt @@ -1,9 +1,8 @@ -tests/cases/conformance/jsx/file.tsx(11,36): error TS1005: '}' expected. -tests/cases/conformance/jsx/file.tsx(11,44): error TS1003: Identifier expected. -tests/cases/conformance/jsx/file.tsx(11,46): error TS1161: Unterminated regular expression literal. +tests/cases/conformance/jsx/file.tsx(11,30): error TS2695: Left side of comma operator is unused and has no side effects. +tests/cases/conformance/jsx/file.tsx(11,30): error TS18007: JSX expressions may not use the comma operator. Did you mean to write an array? -==== tests/cases/conformance/jsx/file.tsx (3 errors) ==== +==== tests/cases/conformance/jsx/file.tsx (2 errors) ==== declare module JSX { interface Element { } interface IntrinsicElements { @@ -15,10 +14,8 @@ tests/cases/conformance/jsx/file.tsx(11,46): error TS1161: Unterminated regular const class1 = "foo"; const class2 = "bar"; const elem =
; - ~ -!!! error TS1005: '}' expected. - ~ -!!! error TS1003: Identifier expected. - -!!! error TS1161: Unterminated regular expression literal. + ~~~~~~ +!!! error TS2695: Left side of comma operator is unused and has no side effects. + ~~~~~~~~~~~~~~ +!!! error TS18007: JSX expressions may not use the comma operator. Did you mean to write an array? \ No newline at end of file diff --git a/tests/baselines/reference/jsxParsingError1.js b/tests/baselines/reference/jsxParsingError1.js index bf2d48b049126..09bb9c113c162 100644 --- a/tests/baselines/reference/jsxParsingError1.js +++ b/tests/baselines/reference/jsxParsingError1.js @@ -16,5 +16,4 @@ const elem =
; // This should be a parse error var class1 = "foo"; var class2 = "bar"; -var elem =
; -/>;; +var elem =
; diff --git a/tests/baselines/reference/jsxParsingError1.symbols b/tests/baselines/reference/jsxParsingError1.symbols index 35d1ba6c29063..535f39f592701 100644 --- a/tests/baselines/reference/jsxParsingError1.symbols +++ b/tests/baselines/reference/jsxParsingError1.symbols @@ -25,5 +25,5 @@ const elem =
; >div : Symbol(JSX.IntrinsicElements, Decl(file.tsx, 1, 22)) >className : Symbol(className, Decl(file.tsx, 10, 17)) >class1 : Symbol(class1, Decl(file.tsx, 8, 5)) ->class2 : Symbol(class2, Decl(file.tsx, 10, 36)) +>class2 : Symbol(class2, Decl(file.tsx, 9, 5)) diff --git a/tests/baselines/reference/jsxParsingError1.types b/tests/baselines/reference/jsxParsingError1.types index 3278c28c724a5..2d4c65b91d020 100644 --- a/tests/baselines/reference/jsxParsingError1.types +++ b/tests/baselines/reference/jsxParsingError1.types @@ -18,10 +18,10 @@ const class2 = "bar"; const elem =
; >elem : JSX.Element ->
: JSX.Element >div : any >className : string +>class1, class2 : "bar" >class1 : "foo" ->class2 : true ->/>; : RegExp +>class2 : "bar" diff --git a/tests/baselines/reference/tsxErrorRecovery1.errors.txt b/tests/baselines/reference/tsxErrorRecovery1.errors.txt index d7e4f5c55dfe5..34c2a3fc87949 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.errors.txt +++ b/tests/baselines/reference/tsxErrorRecovery1.errors.txt @@ -1,26 +1,14 @@ -tests/cases/conformance/jsx/file.tsx(4,11): error TS17008: JSX element 'div' has no corresponding closing tag. tests/cases/conformance/jsx/file.tsx(4,19): error TS1109: Expression expected. -tests/cases/conformance/jsx/file.tsx(7,11): error TS2304: Cannot find name 'a'. -tests/cases/conformance/jsx/file.tsx(7,12): error TS1005: '}' expected. -tests/cases/conformance/jsx/file.tsx(8,1): error TS1005: ' {
- ~~~ -!!! error TS17008: JSX element 'div' has no corresponding closing tag. ~~ !!! error TS1109: Expression expected. } // Shouldn't see any errors down here var y = { a: 1 }; - ~ -!!! error TS2304: Cannot find name 'a'. - ~ -!!! error TS1005: '}' expected. - - -!!! error TS1005: ' {}div> -} -// Shouldn't see any errors down here -var y = {a} 1 }; - ; + var x =
{}
; } +// Shouldn't see any errors down here +var y = { a: 1 }; diff --git a/tests/baselines/reference/tsxErrorRecovery1.symbols b/tests/baselines/reference/tsxErrorRecovery1.symbols index de6bce7d278c2..2b39f9bbce759 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.symbols +++ b/tests/baselines/reference/tsxErrorRecovery1.symbols @@ -11,4 +11,6 @@ function foo() { } // Shouldn't see any errors down here var y = { a: 1 }; +>y : Symbol(y, Decl(file.tsx, 6, 3)) +>a : Symbol(a, Decl(file.tsx, 6, 9)) diff --git a/tests/baselines/reference/tsxErrorRecovery1.types b/tests/baselines/reference/tsxErrorRecovery1.types index d75b4ab89ae8e..010097d41669f 100644 --- a/tests/baselines/reference/tsxErrorRecovery1.types +++ b/tests/baselines/reference/tsxErrorRecovery1.types @@ -6,13 +6,15 @@ function foo() { var x =
{
>x : JSX.Element ->
{
}// Shouldn't see any errors down herevar y = { a: 1 }; : JSX.Element +>
{
: JSX.Element >div : any > : any +>div : any } // Shouldn't see any errors down here var y = { a: 1 }; ->a : any - -> : any +>y : { a: number; } +>{ a: 1 } : { a: number; } +>a : number +>1 : 1 diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 4252296d5da48..29bfe9acadedd 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -42,6 +42,8 @@ // // TODO: figure out a better solution to the API exposure problem. +/// + declare module ts { export type MapKey = string | number; export interface Map { @@ -70,6 +72,21 @@ declare module ts { text: string; } + enum DiagnosticCategory { + Warning, + Error, + Suggestion, + Message + } + + interface DiagnosticMessage { + key: string; + category: DiagnosticCategory; + code: number; + message: string; + reportsUnnecessary?: {}; + } + function flatMap(array: ReadonlyArray, mapfn: (x: T, i: number) => U | ReadonlyArray | undefined): U[]; } @@ -238,6 +255,7 @@ declare namespace FourSlashInterface { signatureHelp(...options: VerifySignatureHelpOptions[], ): void; // Checks that there are no compile errors. noErrors(): void; + errorExistsAtRange(range: Range, code: number, message?: string): void; numberOfErrorsInCurrentFile(expected: number): void; baselineCurrentFileBreakpointLocations(): void; baselineCurrentFileNameOrDottedNameSpans(): void; diff --git a/tests/cases/fourslash/jsxExpressionFollowedByIdentifier.ts b/tests/cases/fourslash/jsxExpressionFollowedByIdentifier.ts new file mode 100644 index 0000000000000..16dcb7f65602c --- /dev/null +++ b/tests/cases/fourslash/jsxExpressionFollowedByIdentifier.ts @@ -0,0 +1,12 @@ +/// + +//@Filename: jsxExpressionFollowedByIdentifier.tsx +////declare var React: any; +////const a =
{
[|x|]}
+////const b =
[|x|]} /> + +test.ranges().forEach(range => { + verify.errorExistsAtRange(range, ts.Diagnostics._0_expected.code, "'}' expected."); + // This is just to ensure getting quick info doesn’t crash + verify.not.quickInfoExists(); +}); diff --git a/tests/cases/fourslash/jsxExpressionWithCommaExpression.ts b/tests/cases/fourslash/jsxExpressionWithCommaExpression.ts new file mode 100644 index 0000000000000..829a21723641b --- /dev/null +++ b/tests/cases/fourslash/jsxExpressionWithCommaExpression.ts @@ -0,0 +1,11 @@ +/// + +//@Filename: jsxExpressionWithCommaExpression.tsx +//@jsx: react +////declare var React: any; +////declare var x: string; +////const a =
+////const b =
{[|x, x|]}
+ +verify.getSyntacticDiagnostics([]); +test.ranges().forEach(range => verify.errorExistsAtRange(range, 18006));