Skip to content
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

Specialized parsing error for apparent JSX tag in .ts file #56101

Conversation

JoshuaKGoldberg
Copy link
Contributor

Adds a specialized diagnostic for the case of a /> (slash and greater-than-sigh) after an identifier in a non-JSX-mode file.

JSX tags are not permitted in `.ts` files. Did you mean to change the file extension to `.tsx`?

I'm not at all confident in this PR's approach. It feels weird to work with a "previousNode" and then skip through tokens. Would love to know if there's a cleaner way of doing things, please!

This also doesn't capture every possible syntax variant that could appear to be from JSX syntax in a non-JSX file. I couldn't figure out any clean way to get more cases that came to mind.

Fixes #29375

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Oct 13, 2023
@RyanCavanaugh
Copy link
Member

A good fix might not exist. We really don't know something's syntactically invalid until way later; this code is legal (!):

declare const div: any, foo: any;
type div = {};
const a = <div>foo</div>/;

if (token() === SyntaxKind.SlashToken && scanner.scan() === SyntaxKind.GreaterThanToken) {
nextToken();
parseErrorAt(posLessThan - 1, getNodePos(), Diagnostics.JSX_tags_are_not_permitted_in_ts_files_Did_you_mean_to_change_the_file_extension_to_tsx, tokenToString(SyntaxKind.GreaterThanToken));
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd originally had it return here, so it doesn't try to parseSimpleUnaryExpression. But I couldn't figure out how to make an empty identifier that obeyed node ordering rules. See failures from up till fd5adf7.

Copy link
Member

Choose a reason for hiding this comment

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

createMissingNode would probably work.

Edit: also, the stuff after a tag is JSX content anyway, right? What's the likelihood that it'll be text that the TS parser actually expects?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hard to say. I would personally assume rather low given that most JSX element children are often one of:

  • More JSX
  • Plain text: will often be parsed mostly as identifiers and/or odd punctuation characters
  • {} blocks containing a function call or some other odd code - which is sometimes valid non-JSX

@JoshuaKGoldberg JoshuaKGoldberg marked this pull request as ready for review November 15, 2023 21:56
Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

There are parts I like and parts I dislike in this change. The change in parsePrimaryExpression is pretty invasive whereas the parseTypeAssertion is simple. How much of the benefit comes from the parseTypeAssertion change?

I also don't like the fact that old errors are still around, and that the new error spans the entire tag. I guess it doesn't really matter what the span is, since the important thing is for the "why not JSX?" message to be early in the list of error messages.

I almost wish we could add this message to an existing error that appears with JSX, only in cases where we can be sure that the cause really is a tag. Would that be a better approach? I'm not sure any of the existing errors have enough context to decide that they were really, truly caused by a JSX tag.

src/compiler/diagnosticMessages.json Outdated Show resolved Hide resolved
!!! error TS2304: Cannot find name 'yippee'.
</span>;
~~~~~~~
!!! error TS1465: JSX tags are not permitted in `.ts` files. Did you mean to change the file extension to `.tsx`?
Copy link
Member

Choose a reason for hiding this comment

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

is the span of this error the entire expression <span> ... </span> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's the entire expression, yes.

@@ -5698,7 +5699,7 @@ namespace Parser {
* 1) UnaryExpression[?Yield]
* 2) UpdateExpression[?Yield] ** ExponentiationExpression[?Yield]
*/
function parseUnaryExpressionOrHigher(): UnaryExpression | BinaryExpression {
function parseUnaryExpressionOrHigher(previousNode?: Node): UnaryExpression | BinaryExpression {
Copy link
Member

Choose a reason for hiding this comment

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

how much would you have to change to make this and the other new parameters non-optional? Even if it's not permanent, in can be a good exercise to see if other places need to change.

Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg Nov 20, 2023

Choose a reason for hiding this comment

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

It wouldn't be a 100% straightforward "replace the ? with | undefined then add /*previousNode*/ undefined to function calls" because of a nextTokenAnd(parseLeftHandSideExpressionOrHigher). But it's 13 one-line changes otherwise: JoshuaKGoldberg@2f6b35e

Copy link
Member

Choose a reason for hiding this comment

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

Thanks. The main thing this points out to me is that there is only one narrow code path that preserves the previous node, and a bunch of others that could, but don't. I guess nothing else needs this information, but it still seems wasteful to wire it up for one usage.

if (token() === SyntaxKind.SlashToken && scanner.scan() === SyntaxKind.GreaterThanToken) {
nextToken();
parseErrorAt(posLessThan - 1, getNodePos(), Diagnostics.JSX_tags_are_not_permitted_in_ts_files_Did_you_mean_to_change_the_file_extension_to_tsx, tokenToString(SyntaxKind.GreaterThanToken));
}
Copy link
Member

Choose a reason for hiding this comment

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

createMissingNode would probably work.

Edit: also, the stuff after a tag is JSX content anyway, right? What's the likelihood that it'll be text that the TS parser actually expects?

@sandersn sandersn self-assigned this Nov 17, 2023
@sandersn sandersn requested a review from jakebailey November 17, 2023 14:59
@jakebailey
Copy link
Member

I'm not totally sure how I feel about threading the previous node down into the parser like this, honestly. I'm not sure of a better way, though, besides passing down a boolean that's true if it's a type assertion or something.

@@ -5558,9 +5559,9 @@ namespace Parser {
);
}

function parseBinaryExpressionOrHigher(precedence: OperatorPrecedence): Expression {
function parseBinaryExpressionOrHigher(precedence: OperatorPrecedence, previousNode?: Node): Expression {
Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg Nov 20, 2023

Choose a reason for hiding this comment

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

From #56101 (comment):

I'm not totally sure how I feel about threading the previous node down into the parser like this, honestly. I'm not sure of a better way, though, besides passing down a boolean that's true if it's a type assertion or something.

I'll just go ahead and say this: I hate it. Adding in the first concept of a "previous" node to a parser for a specialized error message makes me unhappy.

An alternative could be to keep a variable alongside var currentToken: SyntaxKind & co. I dislike that ... more, honestly. But it's an option. 🤷

@@ -6601,6 +6611,25 @@ namespace Parser {
case SyntaxKind.NewKeyword:
return parseNewExpressionOrNewDotTarget();
case SyntaxKind.SlashToken:
// JSX inside a .ts (not .tsx) file gets a specialized error...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From #56101 (review):

The change in parsePrimaryExpression is pretty invasive whereas the parseTypeAssertion is simple. How much of the benefit comes from the parseTypeAssertion change?

Hard to say what balance comes from parsing self-closing tags (parseTypeAssertion) vs. tags with children (parsePrimaryExpression). My intuition is that tags with children (the one we find more invasive) is probably much more common - especially in complex pages where the error messages without this change are convoluted. 😕

@@ -6601,6 +6611,25 @@ namespace Parser {
case SyntaxKind.NewKeyword:
return parseNewExpressionOrNewDotTarget();
case SyntaxKind.SlashToken:
// JSX inside a .ts (not .tsx) file gets a specialized error...
Copy link
Contributor Author

Choose a reason for hiding this comment

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

From #56101 (review):

I almost wish we could add this message to an existing error that appears with JSX, only in cases where we can be sure that the cause really is a tag. Would that be a better approach? I'm not sure any of the existing errors have enough context to decide that they were really, truly caused by a JSX tag.

I looked into that at first but couldn't find any good path forward 😞. JSX syntax issues tend to cause multiple seemingly unrelated parsing errors. If you have a good path I would be very happy to go down it.

Copy link
Member

@sandersn sandersn Nov 28, 2023

Choose a reason for hiding this comment

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

There should be a TypeAssertion somewhere earlier in the parser tree -- but we don't know something has gone wrong until much later, when we see an unexpected / that doesn't end up parsing as a regex.

What if you are extra suspicious of TypeAssertionExpressions? Maybe after parsePrimaryExpression you can do a speculative check for illegal stuff. That should at least let you detect small things like <x>y</x>, and probably also illegal things like <x>y z</x>. In theory that would then let you do some error recovery to skip past the closing tag, but that sounds quite difficult.

Edit: performance of parseTypeAssertion should be getting less important since I hope people are using it less over time.

if (token() === SyntaxKind.SlashToken && scanner.scan() === SyntaxKind.GreaterThanToken) {
nextToken();
parseErrorAt(posLessThan - 1, getNodePos(), Diagnostics.JSX_tags_are_not_permitted_in_ts_files_Did_you_mean_for_the_file_extension_to_be_tsx, tokenToString(SyntaxKind.GreaterThanToken));
return finishNode(factory.createTypeAssertion(type, createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false)), pos);
Copy link
Contributor Author

@JoshuaKGoldberg JoshuaKGoldberg Nov 21, 2023

Choose a reason for hiding this comment

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

From #56101 (review):

I also don't like the fact that old errors are still around, and that the new error spans the entire tag. I guess it doesn't really matter what the span is, since the important thing is for the "why not JSX?" message to be early in the list of error messages.

Agreed! Even after adding createMissingNode (thanks for that!) there is still typically a Cannot find name '{0}'. reported on the type assertion. I tried replacing the factory.createTypeAssertion with a missingNode but that produced Error: Debug Failure. False expression: Invalid token in the tests.

If there's any way to tell TypeScript to not produce diagnostics for an entire area of code... that would be wonderful.

parseErrorAt(posLessThan - 1, getNodePos(), Diagnostics.JSX_tags_are_not_permitted_in_ts_files_Did_you_mean_for_the_file_extension_to_be_tsx, tokenToString(SyntaxKind.GreaterThanToken));
return finishNode(factory.createTypeAssertion(type, createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false)), pos);
}

parseExpected(SyntaxKind.GreaterThanToken);
const expression = parseSimpleUnaryExpression();
Copy link
Member

Choose a reason for hiding this comment

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

Right after this, you'd want to check if (token() === SyntaxKind.Identifier) and tryParse some function that combines parseJSXChildren and parseJSXClosingElement. If that succeeds, you'll need to rebuild the previously parsed nodes above into an opening element (somehow??), issue your error, and return a JSX element. In theory the function you tryParse could be extracted from the first if body of parseJsxElementOrSelfClosingElementOrFragment, although it may do too much error recovery to be usable in a possibly-erroneous position.

Aside: Edge's AI-suggestion features are getting more aggressive but boy howdy was it confused by that paragraph.

Anyway, if (token() === SyntaxKind.LessThanToken) here, you'd instead tryParse(parseJSXClosingElement), which covers the <x>y</x> case.

I'm not yet sure how to reparse <x>y as JSX opening/child token. Maybe there's somewhere else in the parser that does that, or maybe you can create some error fields for TypeAssertion and just cram the body/close token there. Like I said, TypeAssertions are rarely used at this point, so making its structure weird or inefficient is not a huge problem.

@JoshuaKGoldberg
Copy link
Contributor Author

JoshuaKGoldberg commented Mar 26, 2024

Sorry to not have a good update here - this is a tricky issue and I don't have the bandwidth to focus on it now. 😞

If someone else could push this forward, that'd be great.

Thanks for the review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggest renaming file from .ts to .tsx if needed?
5 participants