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

Add 'Remove unnecessary await' suggestion and fix #32363

Merged

Conversation

andrewbranch
Copy link
Member

Also related to #30646

I hesitated to use the word “unnecessary” since your program could be relying on bizarre side effects of the asynchrony introduced by awaiting something sync’ly available, and indeed the message I wrote for the checker suggestion speaks only of types:

'await' has no effect on the type of this expression. ts(80007)

But, it was just too awkward to try to write a code fix message that tries to dance around the issue—so it simply says

Remove unnecessary 'await'

Screen capture 🎥

remove-await

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.

  1. Need a test case with parenthesized numbers to make sure dots get treated correctly.
  2. Definitely say "Remove unnecessary await". It's punchier than a more accurate message.

return;
}

const parenthesizedExpression = awaitExpression && tryCast(awaitExpression.parent, isParenthesizedExpression);
Copy link
Member

Choose a reason for hiding this comment

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

what about (await 1).toFixed() ? Does this correctly produce 1..toFixed()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Gooood call. It does not 🙈

Copy link
Member

Choose a reason for hiding this comment

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

Is it too embarrassing to leave the parens in and just produce (1).toFixed() ? I mean, most people don't like parens, but they've always treated me well.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think that’s what I’ll do, when the inner expression is a DecimalIntegerLiteral.

@andrewbranch
Copy link
Member Author

Definitely say "Remove unnecessary await". It's punchier than a more accurate message.

Are you suggesting I change the checker suggestion message, or just affirming my choice for the refactor?

@sandersn
Copy link
Member

Affirming your choice for the refactor.

@andrewbranch
Copy link
Member Author

Other test failures show that await x being the same type as x when x: any shouldn’t trigger the suggestion 👌 Fixed now.

@fatcerberus
Copy link

fatcerberus commented Jul 12, 2019

'await' has no effect on the type of this expression

FWIW I personally find this a bit confusing without thinking about it; when dealing with async code I don’t intuitively think in terms of types - I just go “oh this runs in the background, I should await it”. So my initial reaction to the “no change of type” message is “what on earth are you talking about, of course await wouldn’t change the type” and only 10 seconds later would I go “...oh, Promise, right.”

Something to think about at least, especially since the async-await syntax is specifically designed to hide the existence of Promise objects.

@@ -1992,6 +1992,10 @@ namespace ts {
return typeIsAccessible ? res : undefined;
}

export function isDecimalIntegerLiteral(node: Node): node is NumericLiteral {
return isNumericLiteral(node) && /^\d+$/.test(node.text);
Copy link
Contributor

Choose a reason for hiding this comment

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

this actually doesn't do what it's supposed to do. NumericLiteral#text contains the normalized value, e.g. 1.0 -> 1

you probably want to use .getText() here to get the raw source text of the numeric literal


const parenthesizedExpression = tryCast(awaitExpression.parent, isParenthesizedExpression);
// (await 0).toFixed() should keep its parens (or add an extra dot for 0..toFixed())
if (parenthesizedExpression && isPropertyAccessExpression(parenthesizedExpression.parent) && isDecimalIntegerLiteral(awaitExpression.expression)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this also needs to handle NewExpression without parens:

(await new C).foo();
// currently fixed to
new C.foo();
// which executes as
new (C.foo)();

// should instead be
(new C).foo()


const parenthesizedExpression = tryCast(awaitExpression.parent, isParenthesizedExpression);
// (await 0).toFixed() should keep its parens (or add an extra dot for 0..toFixed())
if (parenthesizedExpression && isPropertyAccessExpression(parenthesizedExpression.parent) && isDecimalIntegerLiteral(awaitExpression.expression)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

also needs to handle await inside NewExpression#expression:

new (await getC()).Class();
// currently fixed to
new getC().Class();
// which executes as
(new getC()).Class();

// should instead be
new (getC()).Class();

@andrewbranch
Copy link
Member Author

FWIW I personally find this a bit confusing without thinking about it; when dealing with async code I don’t intuitively think in terms of types...

@fatcerberus that’s definitely fair, but for a checker message I wanted to be fairly careful and accurate with the language we use. The only possible alternative I can think of is something like

Type '{0}' is not Promise-like and may not need to be awaited

or

Type '{0}' is not Promise-like. Did you mean to omit 'await'?

@sandersn @DanielRosenwasser thoughts?

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.

Two suggestions to consider for now or later.

src/services/codefixes/removeUnnecessaryAwait.ts Outdated Show resolved Hide resolved
@@ -27,12 +27,34 @@ namespace ts.codefix {
}

const parenthesizedExpression = tryCast(awaitExpression.parent, isParenthesizedExpression);
// (await 0).toFixed() should keep its parens (or add an extra dot for 0..toFixed())
if (parenthesizedExpression && isPropertyAccessExpression(parenthesizedExpression.parent) && isDecimalIntegerLiteral(awaitExpression.expression)) {
const preserveParens = parenthesizedExpression && (
Copy link
Member

Choose a reason for hiding this comment

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

I still think this is the right thing to do, but now that we have 3 exceptions, the queasy feeling in my stomach says that there more out there. I almost would like to always preserve parentheses, except that the result is so ugly in the common case.

What if we flipped it to only skip parens for identifiers and callexpressions?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I had the same feeling. I think this is probably a good plan.

@sandersn
Copy link
Member

@fatcerberus I disagree, I use types to guide my thinking whenever I write monadic code.

I'm not a normal programmer though. Specifically:

  1. I haven't written much async/await code, so I haven't gotten into the habit of thinking concretely about its behaviour.
  2. Most of the monadic code I've written is in Haskell, where all monads use the same syntax.
  3. And where there are tons of different monad utilities.

Still, I think a type checker reporting an error in terms of types is reasonable.

@fatcerberus
Copy link

@andrewbranch That's fair, but on the other side of that same coin, being too literal with calling out "type" can be confusing too when the existence of a type is an implementation detail, see e.g. discussion starting here w.r.t. excess object literal property errors (an endless source of confusion for newbies): #32158 (comment)

In that case the compiler says "type ... is not assignable to" which is literally true (it's a "fresh object literal" type), but ends up looking like a statement about structural typing in general, leading people to believe types are closed.

@fatcerberus
Copy link

@sandersn

I haven't written much async/await code, so I haven't gotten into the habit of thinking concretely about its behaviour.

Yeah, I like monads too but that's the thing, async/await is specifically designed to hide the existence of the promise monad and let you write basically the same code you would if the monad wasn't there. So re-surfacing the type discrepancy in that context reads as confusing to me.

When I decide whether to write an await the mindset is "okay I know this is a string, now do I have wait for it or not, is the function async?", as opposed to explicit monadic code where I know I'm concretely dealing with Promise abstractions. (concrete abstractions - there's an oxymoron for you!)

@fatcerberus
Copy link

I personally like this one:

Type '{0}' is not Promise-like and may not need to be awaited

It seems like a nice compromise: Still in terms of types, but much clearer about what went wrong, so I don't think my strings morphed into numbers or something 😄

@sandersn
Copy link
Member

:drakememe: (2620b0d, d0191da)

@andrewbranch andrewbranch force-pushed the enhancement/unnecessary-await branch from d0191da to f22dc9d Compare July 12, 2019 17:24
@andrewbranch
Copy link
Member Author

Type '{0}' is not Promise-like and may not need to be awaited

I don’t like this because

  • I don’t think printing the type is particularly necessary or useful
  • “and may not need” is kind of a mouthful and sounds like “anemone”
  • It sounds wishy-washy

'await' has no effect on the type of this expression

I like this because

  • It’s an unambiguous fact
  • The core of the concept is conveyed immediately—you could basically stop reading at “'await' has no effect” and think “Oh, I don’t need 'await'”
  • If you were unclear about what’s happening with types, you don’t really have to care once you see the codefix message “Remove unnecessary 'await'”

@fatcerberus I do very much appreciate the input—keep the suggestions coming in general, but I’m going to stick with the original this time. Thanks all!

(This makes me think of one of my favorite lines from an excellent film 😛)

tenor (1)

@andrewbranch andrewbranch merged commit 89badcc into microsoft:master Jul 12, 2019
@andrewbranch andrewbranch deleted the enhancement/unnecessary-await branch July 12, 2019 18:03
@fatcerberus
Copy link

fatcerberus commented Jul 12, 2019

It’s an unambiguous fact

Counterpoint: So is Type { ... } is not assignable to type { ... } (because it's an object literal and has extra properties), but that doesn't stop people from being misled into thinking the same error should apply to structural typing in general. There are lots of "bug" reports here that attest to that one. Sometimes being overly literal is bad too. Just ask anyone who ever got tricked by one of these: https://tvtropes.org/pmwiki/pmwiki.php/Main/LiteralGenie

😄

If you were unclear about what’s happening with types, you don’t really have to care once you see the codefix message “Remove unnecessary 'await'”

Point taken on this though, so I'll admit defeat. 👍

}

const parenthesizedExpression = tryCast(awaitExpression.parent, isParenthesizedExpression);
const removeParens = parenthesizedExpression && (isIdentifier(awaitExpression.expression) || isCallExpression(awaitExpression.expression));
Copy link
Contributor

Choose a reason for hiding this comment

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

@andrewbranch it seems you dropped the code necessary to avoid changing the parsing of NewExpression.

However, there's a whole category of exceptions that's not handled here: if the parenthesized AwaitExpression is at the start of the statement, the fixed code could parse as declaration instead of expression:

(await function(){}());
(await class {static fn(){}}.fn());
(await {fn() {}}.fn());

These cases are already handled in factory.ts, see the uses of getLeftmostExpression

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants