-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Improve parser recovery for unclosed/mismatched JSX elements #43780
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
Conversation
Everything works, the error messages for unmatched opening elements could still use improvement, plus there is tonnes of unused and ugly code. 1. Make sure the parser can recover from all kinds of unclosed tags. 2. Improve the parse tree for unmatched opening tags. 3. Better errors at some point.
| this.goToMarker(markerName); | ||
| const actual = this.languageService.getJsxClosingTagAtPosition(this.activeFile.fileName, this.currentCaretPosition); | ||
| assert.deepEqual(actual, map[markerName]); | ||
| assert.deepEqual(actual, map[markerName], markerName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol @ fourslash
| } | ||
| else { | ||
| parseExpected(SyntaxKind.SlashToken); | ||
| if (inExpressionContext) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix (1) follows
| function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean, topInvalidNodePosition?: number, openingTag?: JsxOpeningElement | JsxOpeningFragment): JsxElement | JsxSelfClosingElement | JsxFragment { | ||
| const pos = getNodePos(); | ||
| const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); | ||
| let result: JsxElement | JsxSelfClosingElement | JsxFragment; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the bulk of fix (2), with a couple of ifs below to correct parsing in the error case.
|
@weswigham I included you because you know a lot about JSX but if you don't feel like looking at a bunch of parsing, I think @elibarzilay and @andrewbranch have reviewed most of my recent parsing PRs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand fix (2) and it looks good. I don’t really understand fix (1) without debugging but it looks plausible and tested. How confident are you that this will not break valid syntax? Is our JSX test coverage pretty good, and do you feel good about merging this into 4.3 post-beta?
|
I share Andrew's concerns, conceptually. I wouldn't merge this too close to the RC but I think we have enough runway to go for it now. |
|
@andrewbranch I am very confident about fix (1); the manual advancement of the scanner before was just wrong, and in a confusing way. Some additional considerations:
I would personally rather hold this till 4.4, I just forgot to add that section in the PR discussion. I would be OK shipping fix (1) separately in 4.3, but there's really not much urgency here. |
<previously parsed [most of] the rest of the file as JsxText. That's because, nested inside another JSX tag, when attempting to parse a closing>, all the functions unconditionally advanced the token, whether or not the>was there. This parsed a following</(for example) as jsx text, and then the rest of parsing went wrong.parseExpectedhas a mode that doesn't advance the scanner, but it wasn't used consistently (or understandably) before.Previously this parsed as
<div>(<span></div>), which meant that the first<div>was unclosed, which again parsed the rest of the file as jsx text, or at least until another</div>was found.Now, in the code that finalises jsx elements, when there is a mismatched open/close (eg
<span></div>, and the mismatched close text (</div>) matches the parent's opening text (<div>), the parser creates a new jsx element with the mismatched open (<span></span>) and re-attaches the mismatched close to parent open (<div>...</div>).This may not be correct 100% of the time, but it's far better than the old nearest-attach behaviour.
Edit: I investigated those 6 tests and didn't learn much because the unclosed tags really were at the end of the file. I need to learn more about what can appear in jsx text to see if there are other boundaries that could indicate a runaway unclosed tag.
Fixes #43496