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

fix: async arrow functions should not be allowed after binary operator. #11284

Merged
merged 4 commits into from Mar 21, 2020
Merged

fix: async arrow functions should not be allowed after binary operator. #11284

merged 4 commits into from Mar 21, 2020

Conversation

vedantroy
Copy link
Contributor

Q                       A
Fixed Issues? Fixes #11203
Patch: Bug Fix? Yes
Major: Breaking Change? No
Minor: New Feature? No
Tests Added + Pass? Yes
Documentation PR Link
Any Dependency Changes? No
License MIT

Previously, code such as 1 + async () => 2 was parsed succesfully.

The root cause of the issue is inside parseExprAtom, which is called by parseExprSubscripts. The switch statement matches to case tt.name: and returns an Identifier node with name "async" (skipping all the if stmts inside the case). Then, parseExprSubscripts will reach return this.parseSubscripts(expr, startPos, startLoc);, which parses the rest of the expression and returns an ArrowFunctionExpression.

The issue is fixed by adding an extra parameter to this.parseSubscripts that indicates an async arrow function declaration is forbidden at the location. This prevents Babel from parsing async() => 2 as an async function declaration. Instead, it will throw once it reaches the = sign, because it will expect a semicolon since it is parsing async() as a function call (instead of a async arrow function declaration).

This commit makes Babel throw an error when parsing
code like "3 + async() => 2".
): N.Expression {
const state = {
optionalChainMember: false,
maybeAsyncArrow: this.atPossibleAsync(base),
maybeAsyncArrow: this.atPossibleAsync(base) && !asyncArrowFuncForbidden,
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we simplify a bit to

Suggested change
maybeAsyncArrow: this.atPossibleAsync(base) && !asyncArrowFuncForbidden,
maybeAsyncArrow: this.atPossibleAsync(base) && startPos === this.state.potentialArrowAt,

Copy link
Member

Choose a reason for hiding this comment

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

I find your suggestion also easier to understand 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

I realize that atPossibleAsync is only used to detect whether there is an async arrow ahead. I suggest we rename atPossibleAsync to atPossibleAsyncArrow. And the condition base.start === this.state.potentialArrowAt can be added to current atPossibleAsync.

By doing so we can also fix another related case for free

4 + async<number>() => 2 
// This typescript snippet should throw but now it is parsed successfully

@existentialism existentialism added PR: Bug Fix 🐛 A type of pull request used for our changelog categories pkg: parser labels Mar 19, 2020
@vedantroy
Copy link
Contributor Author

vedantroy commented Mar 19, 2020

Fixed!

Also, out of curiosity/so I can better understand the parser, I have a few questions. Not really sure if this is the best place to ask them, but I'll give it a shot.

What does the noCalls parameter do in parseSubscripts?

Also, why is this.input.slice(base.start, base.end) === "async" needed if we know

base.type === "Identifier" &&
base.name === "async"

from earlier in atPossibleAsyncArrow

Lastly, what is a scenario where this.state.lastTokEnd would not equal base.end in atPossibleAsyncArrow?

@JLHwung
Copy link
Contributor

JLHwung commented Mar 20, 2020

Great questions!

What does the noCalls parameter do in parseSubscripts?

noCalls is used to indicate whether a left parenthesis after an identifier should be considered as a CallExpression. i.e. When parsing a NewExpression new C(), we set noCalls to be true so the ( after C is not a CallExpression.

node.callee = this.parseNoCallExpr();

Why is this.input.slice(base.start, base.end) === "async" needed if we know (the other two conditions)

This is a very good one! I think we can remove base.name === "async" check because this.input.slice(base.start, base.end) === "async" && base.type === "Identifier" implies base.name === "async". However the opposite direction can't follow because identifier may contain escaped sequence:

var \u{61}sync
// base.name === "async"

Oh I think base.name should be faster than this.input.slice. I think we can check base.end - base.start === 5 instead, see my comments at #11284 (comment).

What is a scenario where this.state.lastTokEnd would not equal base.end in

One of such scenarios is that the expression atom can be parenthesized

(async);
// base.end points to 'c'
// this.state.lastTokEnd points to ')'

@@ -748,7 +748,7 @@ export default class ExpressionParser extends LValParser {
return this.finishNode(node, "TaggedTemplateExpression");
}

atPossibleAsync(base: N.Expression): boolean {
atPossibleAsyncArrow(base: N.Expression): boolean {
return (
Copy link
Contributor

Choose a reason for hiding this comment

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

The conditions here corresponds to atPossibleAsync. I was meant to add base.start === this.state.potentialArrowAt here so it can be rebranded as atPossibleAsyncArrow.

atPossibleAsyncArrow(base: N.Expression): boolean {
  return (base.type === "Identifier" &&
  base.name === "async" &&
  this.state.lastTokEnd === base.end &&
  !this.canInsertSemicolon() &&
  base.end - base.start === 5 &&
  base.start === this.state.potentialArrowAt)
}

The first 5 conditions are about atPossibleAsync and the last one is hasPossibleArrow so it combines into atPossibleAsyncArrow.

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 think I understand. I'll add the extra condition to this atPossibleAsyncArrow method, and delete the atPossibleAsync method. This means I will also change

if (!noCalls && this.atPossibleAsync(base)) {

in the Typescript plugin to

if (!noCalls && this.atPossibleAsyncArrow(base)) {

Copy link
Member

Choose a reason for hiding this comment

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

If you are going with the base.end - base.start === 5 route, could you add a comment in the code that it's needed to check that there are no escape sequences?

Copy link
Contributor

Choose a reason for hiding this comment

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

Half joking: If this.state.lastTokEnd >= base.end && base.start >= this.state.potentialArrowAt is always true (I don't know). We can even further simplify it to

return (base.type === "Identifier" &&
  base.name === "async" &&
  !this.canInsertSemicolon() &&
  this.state.lastTokEnd - this.state.potentialArrowAt === 5)
)

Add an extra test to atPossibleAsync and refactor it to
atPossibleAsyncArrow. This also fixes a bug in the Typescript plugin,
so a new test has been added.
@vedantroy
Copy link
Contributor Author

vedantroy commented Mar 20, 2020

Updated. I also added a test for Typescript, since this PR will fix a bug there too.

Edit: I just realized this PR fixes async arrow functions after unary operators. So things like void async () => 1 will no longer be allowed.

Should I add a test for async arrow functions after unary operators?

@nicolo-ribaudo
Copy link
Member

Should I add a test for async arrow functions after unary operators?

If it was broken before, then yes please!

Copy link
Member

@nicolo-ribaudo nicolo-ribaudo left a comment

Choose a reason for hiding this comment

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

Thanks!

@nicolo-ribaudo nicolo-ribaudo merged commit 0e5c1da into babel:master Mar 21, 2020
@vedantroy vedantroy deleted the fix-11203 branch March 21, 2020 18:59
@github-actions github-actions bot added the outdated A closed issue/PR that is archived due to age. Recommended to make a new issue label Jun 21, 2020
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jun 21, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
outdated A closed issue/PR that is archived due to age. Recommended to make a new issue pkg: parser PR: Bug Fix 🐛 A type of pull request used for our changelog categories
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[parser] Async arrow cannot be rhs of binary operator
4 participants