Skip to content

Fix invalid JSXExpressions having identifier-ish things in their trivia, improve error messages for comma expressions in JSX #31480

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

Merged
merged 8 commits into from
Jun 26, 2019
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
7 changes: 7 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
10 changes: 7 additions & 3 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we successfully parse something we know is illegal, is that always a grammar error, or is there ever a reason why that should be a parse error? I know a parse error will prevent the node from being reused incrementally—in this case the tree should be well-formed, so I think reusing it isn’t a problem.

}
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);
Expand Down
19 changes: 19 additions & 0 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
11 changes: 7 additions & 4 deletions tests/baselines/reference/jsxAndTypeAssertion.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@ var foo = /** @class */ (function () {
return foo;
}());
var x;
x = <any> {test} <any></any> };
x = <any> {test}: <any></any> };
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TypeScript generating this was x = <any> { test: <any></any> };. Garbage in, garbage out?


x = <any><any></any>;

x = <foo>hello {<foo>} </foo>}
x = <foo>hello {<foo>} </foo>};

x = <foo test={<foo>}>hello</foo>}/>

x = <foo test={<foo>}>hello{<foo>}</foo>}
x = <foo test={<foo>}>hello{<foo>}</foo>};

x = <foo>x</foo>, x = <foo />;

<foo>{<foo><foo>{/foo/.test(x) ? <foo><foo></foo> : <foo><foo></foo>}</foo>}</foo>
:
}</></>}</></>}/></></></>;
}


</></>}</></>}/></></></>;
2 changes: 1 addition & 1 deletion tests/baselines/reference/jsxInvalidEsprimaTestSuite.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ var x = <div>one</div>, <div>two</div>;
var x = <div>one</div> /* intervening comment */, /* intervening comment */ <div>two</div>;
;
//// [20.jsx]
<a>{"str"}}</a>;
<a>{"str"};}</a>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird, but barely weirder than the previous

//// [21.jsx]
<span className="a" id="b"/>;
//// [22.jsx]
Expand Down
17 changes: 7 additions & 10 deletions tests/baselines/reference/jsxParsingError1.errors.txt
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,10 +14,8 @@ tests/cases/conformance/jsx/file.tsx(11,46): error TS1161: Unterminated regular
const class1 = "foo";
const class2 = "bar";
const elem = <div className={class1, class2}/>;
~
!!! 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?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two errors make a lot more sense to me than the previous three, although I could do without the first. I considered making the error range start on the comma operator instead of at the beginning of the expression. Thoughts?


3 changes: 1 addition & 2 deletions tests/baselines/reference/jsxParsingError1.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,4 @@ const elem = <div className={class1, class2}/>;
// This should be a parse error
var class1 = "foo";
var class2 = "bar";
var elem = <div className={class1} class2/>;
/>;;
var elem = <div className={class1, class2}/>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This emit is invalid although slightly less so than the previous? We could potentially put these inside a ParenthesizedExpression, but we're already emitting a grammar error, and it seems like there's no requirement for the emit to be totally valid if the input wasn't totally valid.

2 changes: 1 addition & 1 deletion tests/baselines/reference/jsxParsingError1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ const elem = <div className={class1, class2}/>;
>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))

6 changes: 3 additions & 3 deletions tests/baselines/reference/jsxParsingError1.types
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ const class2 = "bar";

const elem = <div className={class1, class2}/>;
>elem : JSX.Element
><div className={class1, class2 : JSX.Element
><div className={class1, class2}/> : JSX.Element
>div : any
>className : string
>class1, class2 : "bar"
>class1 : "foo"
>class2 : true
>/>; : RegExp
>class2 : "bar"

16 changes: 2 additions & 14 deletions tests/baselines/reference/tsxErrorRecovery1.errors.txt
Original file line number Diff line number Diff line change
@@ -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: '</' expected.


==== tests/cases/conformance/jsx/file.tsx (5 errors) ====
==== tests/cases/conformance/jsx/file.tsx (1 errors) ====
declare namespace JSX { interface Element { } }

function foo() {
var x = <div> { </div>
~~~
!!! error TS17008: JSX element 'div' has no corresponding closing tag.
~~
!!! error TS1109: Expression expected.
}
// Shouldn't see any errors down here
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now we don't. I think this gets significantly better.

var y = { a: 1 };
~
!!! error TS2304: Cannot find name 'a'.
~
!!! error TS1005: '}' expected.


!!! error TS1005: '</' expected.

8 changes: 3 additions & 5 deletions tests/baselines/reference/tsxErrorRecovery1.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ var y = { a: 1 };

//// [file.jsx]
function foo() {
var x = <div> {}div>
}
// Shouldn't see any errors down here
var y = {a} 1 };
</>;
var x = <div> {} </div>;
}
// Shouldn't see any errors down here
var y = { a: 1 };
2 changes: 2 additions & 0 deletions tests/baselines/reference/tsxErrorRecovery1.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -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))

10 changes: 6 additions & 4 deletions tests/baselines/reference/tsxErrorRecovery1.types
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ function foo() {

var x = <div> { </div>
>x : JSX.Element
><div> { </div>}// Shouldn't see any errors down herevar y = { a: 1 }; : JSX.Element
><div> { </div> : 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

18 changes: 18 additions & 0 deletions tests/cases/fourslash/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
//
// TODO: figure out a better solution to the API exposure problem.

/// <reference path="../../../src/compiler/diagnosticInformationMap.generated.ts" />

declare module ts {
export type MapKey = string | number;
export interface Map<T> {
Expand Down Expand Up @@ -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<T, U>(array: ReadonlyArray<T>, mapfn: (x: T, i: number) => U | ReadonlyArray<U> | undefined): U[];
}

Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions tests/cases/fourslash/jsxExpressionFollowedByIdentifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// <reference path="fourslash.ts" />

//@Filename: jsxExpressionFollowedByIdentifier.tsx
////declare var React: any;
////const a = <div>{<div />[|x|]}</div>
////const b = <div x={<div />[|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();
});
11 changes: 11 additions & 0 deletions tests/cases/fourslash/jsxExpressionWithCommaExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/// <reference path="fourslash.ts" />

//@Filename: jsxExpressionWithCommaExpression.tsx
//@jsx: react
////declare var React: any;
////declare var x: string;
////const a = <div x={[|x, x|]} />
////const b = <div>{[|x, x|]}</div>

verify.getSyntacticDiagnostics([]);
test.ranges().forEach(range => verify.errorExistsAtRange(range, 18006));