Skip to content

Commit 412fc6f

Browse files
authored
Add no-unnecessary-await rule (#1904)
1 parent 0886544 commit 412fc6f

12 files changed

+1002
-38
lines changed

configs/recommended.js

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ module.exports = {
5050
'unicorn/no-static-only-class': 'error',
5151
'unicorn/no-thenable': 'error',
5252
'unicorn/no-this-assignment': 'error',
53+
'unicorn/no-unnecessary-await': 'error',
5354
'unicorn/no-unreadable-array-destructuring': 'error',
5455
'unicorn/no-unreadable-iife': 'error',
5556
'unicorn/no-unsafe-regex': 'off',

docs/rules/no-unnecessary-await.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Disallow awaiting non-promise values
2+
3+
<!-- Do not manually modify RULE_NOTICE part. Run: `npm run generate-rule-notices` -->
4+
<!-- RULE_NOTICE -->
5+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
6+
7+
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
8+
<!-- /RULE_NOTICE -->
9+
10+
The [`await` operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await) should only be used on [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) values.
11+
12+
## Fail
13+
14+
```js
15+
await await promise;
16+
```
17+
18+
```js
19+
await [promise1, promise2];
20+
```
21+
22+
## Pass
23+
24+
```js
25+
await promise;
26+
```
27+
28+
```js
29+
await Promise.allSettled([promise1, promise2]);
30+
```

readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ Each rule has emojis denoting:
9191
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. || 🔧 | |
9292
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. || | |
9393
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
94+
| [no-unnecessary-await](docs/rules/no-unnecessary-await.md) | Disallow awaiting non-promise values. || 🔧 | |
9495
| [no-unreadable-array-destructuring](docs/rules/no-unreadable-array-destructuring.md) | Disallow unreadable array destructuring. || 🔧 | |
9596
| [no-unreadable-iife](docs/rules/no-unreadable-iife.md) | Disallow unreadable IIFEs. || | |
9697
| [no-unsafe-regex](docs/rules/no-unsafe-regex.md) | Disallow unsafe regular expressions. | | | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
const {isSemicolonToken} = require('eslint-utils');
3+
4+
function * addParenthesizesToReturnOrThrowExpression(fixer, node, sourceCode) {
5+
if (node.type !== 'ReturnStatement' && node.type !== 'ThrowStatement') {
6+
return;
7+
}
8+
9+
const returnOrThrowToken = sourceCode.getFirstToken(node);
10+
yield fixer.insertTextAfter(returnOrThrowToken, ' (');
11+
12+
const lastToken = sourceCode.getLastToken(node);
13+
if (!isSemicolonToken(lastToken)) {
14+
yield fixer.insertTextAfter(node, ')');
15+
return;
16+
}
17+
18+
yield fixer.insertTextBefore(lastToken, ')');
19+
}
20+
21+
module.exports = addParenthesizesToReturnOrThrowExpression;

rules/fix/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ module.exports = {
1919
removeSpacesAfter: require('./remove-spaces-after.js'),
2020
fixSpaceAroundKeyword: require('./fix-space-around-keywords.js'),
2121
replaceStringLiteral: require('./replace-string-literal.js'),
22+
addParenthesizesToReturnOrThrowExpression: require('./add-parenthesizes-to-return-or-throw-expression.js'),
2223
};

rules/fix/remove-spaces-after.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
'use strict';
22

3-
function removeSpacesAfter(indexOrNode, sourceCode, fixer) {
4-
let index = indexOrNode;
5-
if (typeof indexOrNode === 'object' && Array.isArray(indexOrNode.range)) {
6-
index = indexOrNode.range[1];
3+
function removeSpacesAfter(indexOrNodeOrToken, sourceCode, fixer) {
4+
let index = indexOrNodeOrToken;
5+
if (typeof indexOrNodeOrToken === 'object' && Array.isArray(indexOrNodeOrToken.range)) {
6+
index = indexOrNodeOrToken.range[1];
77
}
88

99
const textAfter = sourceCode.text.slice(index);
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,17 @@
11
'use strict';
22
const isNewExpressionWithParentheses = require('../utils/is-new-expression-with-parentheses.js');
33
const {isParenthesized} = require('../utils/parentheses.js');
4+
const isOnSameLine = require('../utils/is-on-same-line.js');
5+
const addParenthesizesToReturnOrThrowExpression = require('./add-parenthesizes-to-return-or-throw-expression.js');
6+
const removeSpaceAfter = require('./remove-spaces-after.js');
47

5-
function * fixReturnOrThrowStatementArgument(newExpression, sourceCode, fixer) {
6-
const {parent} = newExpression;
7-
if (
8-
(parent.type !== 'ReturnStatement' && parent.type !== 'ThrowStatement')
9-
|| parent.argument !== newExpression
10-
|| isParenthesized(newExpression, sourceCode)
11-
) {
12-
return;
13-
}
14-
15-
const returnStatement = parent;
16-
const returnToken = sourceCode.getFirstToken(returnStatement);
17-
const classNode = newExpression.callee;
18-
19-
// Ideally, we should use first parenthesis of the `callee`, and should check spaces after the `new` token
20-
// But adding extra parentheses is harmless, no need to be too complicated
21-
if (returnToken.loc.start.line === classNode.loc.start.line) {
22-
return;
23-
}
8+
function * switchNewExpressionToCallExpression(newExpression, sourceCode, fixer) {
9+
const newToken = sourceCode.getFirstToken(newExpression);
10+
yield fixer.remove(newToken);
11+
yield removeSpaceAfter(newToken, sourceCode, fixer);
2412

25-
yield fixer.insertTextAfter(returnToken, ' (');
26-
yield fixer.insertTextAfter(newExpression, ')');
27-
}
28-
29-
function * switchNewExpressionToCallExpression(node, sourceCode, fixer) {
30-
const [start] = node.range;
31-
let end = start + 3; // `3` = length of `new`
32-
const textAfter = sourceCode.text.slice(end);
33-
const [leadingSpaces] = textAfter.match(/^\s*/);
34-
end += leadingSpaces.length;
35-
yield fixer.removeRange([start, end]);
36-
37-
if (!isNewExpressionWithParentheses(node, sourceCode)) {
38-
yield fixer.insertTextAfter(node, '()');
13+
if (!isNewExpressionWithParentheses(newExpression, sourceCode)) {
14+
yield fixer.insertTextAfter(newExpression, '()');
3915
}
4016

4117
/*
@@ -48,7 +24,11 @@ function * switchNewExpressionToCallExpression(node, sourceCode, fixer) {
4824
}
4925
```
5026
*/
51-
yield * fixReturnOrThrowStatementArgument(node, sourceCode, fixer);
27+
if (!isOnSameLine(newToken, newExpression.callee) && !isParenthesized(newExpression, sourceCode)) {
28+
// Ideally, we should use first parenthesis of the `callee`, and should check spaces after the `new` token
29+
// But adding extra parentheses is harmless, no need to be too complicated
30+
yield * addParenthesizesToReturnOrThrowExpression(fixer, newExpression.parent, sourceCode);
31+
}
5232
}
5333

5434
module.exports = switchNewExpressionToCallExpression;

rules/no-unnecessary-await.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict';
2+
const {
3+
addParenthesizesToReturnOrThrowExpression,
4+
removeSpacesAfter,
5+
} = require('./fix/index.js');
6+
const {isParenthesized} = require('./utils/parentheses.js');
7+
const needsSemicolon = require('./utils/needs-semicolon.js');
8+
const isOnSameLine = require('./utils/is-on-same-line.js');
9+
10+
const MESSAGE_ID = 'no-unnecessary-await';
11+
const messages = {
12+
[MESSAGE_ID]: 'Do not `await` non-promise value.',
13+
};
14+
15+
function notPromise(node) {
16+
switch (node.type) {
17+
case 'ArrayExpression':
18+
case 'ArrowFunctionExpression':
19+
case 'AwaitExpression':
20+
case 'BinaryExpression':
21+
case 'ClassExpression':
22+
case 'FunctionExpression':
23+
case 'JSXElement':
24+
case 'JSXFragment':
25+
case 'Literal':
26+
case 'TemplateLiteral':
27+
case 'UnaryExpression':
28+
case 'UpdateExpression':
29+
return true;
30+
case 'SequenceExpression':
31+
return notPromise(node.expressions[node.expressions.length - 1]);
32+
// No default
33+
}
34+
35+
return false;
36+
}
37+
38+
/** @param {import('eslint').Rule.RuleContext} context */
39+
const create = context => ({
40+
AwaitExpression(node) {
41+
if (!notPromise(node.argument)) {
42+
return;
43+
}
44+
45+
const sourceCode = context.getSourceCode();
46+
const awaitToken = sourceCode.getFirstToken(node);
47+
const problem = {
48+
node,
49+
loc: awaitToken.loc,
50+
messageId: MESSAGE_ID,
51+
};
52+
53+
const valueNode = node.argument;
54+
if (
55+
// Removing `await` may change them to a declaration, if there is no `id` will cause SyntaxError
56+
valueNode.type === 'FunctionExpression'
57+
|| valueNode.type === 'ClassExpression'
58+
// `+await +1` -> `++1`
59+
|| (
60+
node.parent.type === 'UnaryExpression'
61+
&& valueNode.type === 'UnaryExpression'
62+
&& node.parent.operator === valueNode.operator
63+
)
64+
) {
65+
return problem;
66+
}
67+
68+
return Object.assign(problem, {
69+
/** @param {import('eslint').Rule.RuleFixer} fixer */
70+
* fix(fixer) {
71+
if (
72+
!isOnSameLine(awaitToken, valueNode)
73+
&& !isParenthesized(node, sourceCode)
74+
) {
75+
yield * addParenthesizesToReturnOrThrowExpression(fixer, node.parent, sourceCode);
76+
}
77+
78+
yield fixer.remove(awaitToken);
79+
yield removeSpacesAfter(awaitToken, sourceCode, fixer);
80+
81+
const nextToken = sourceCode.getTokenAfter(awaitToken);
82+
const tokenBefore = sourceCode.getTokenBefore(awaitToken);
83+
if (needsSemicolon(tokenBefore, sourceCode, nextToken.value)) {
84+
yield fixer.insertTextBefore(nextToken, ';');
85+
}
86+
},
87+
});
88+
},
89+
});
90+
91+
/** @type {import('eslint').Rule.RuleModule} */
92+
module.exports = {
93+
create,
94+
meta: {
95+
type: 'suggestion',
96+
docs: {
97+
description: 'Disallow awaiting non-promise values.',
98+
},
99+
fixable: 'code',
100+
101+
messages,
102+
},
103+
};

rules/utils/is-on-same-line.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict';
2+
3+
function isOnSameLine(nodeOrTokenA, nodeOrTokenB) {
4+
return nodeOrTokenA.loc.start.line === nodeOrTokenB.loc.start.line;
5+
}
6+
7+
module.exports = isOnSameLine;

test/no-unnecessary-await.mjs

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import outdent from 'outdent';
2+
import {getTester} from './utils/test.mjs';
3+
4+
const {test} = getTester(import.meta);
5+
6+
test.snapshot({
7+
testerOptions: {
8+
parserOptions: {
9+
ecmaFeatures: {
10+
jsx: true,
11+
},
12+
},
13+
},
14+
valid: [
15+
'await {then}',
16+
'await a ? b : c',
17+
'await a || b',
18+
'await a && b',
19+
'await a ?? b',
20+
'await new Foo()',
21+
'await tagged``',
22+
'class A { async foo() { await this }}',
23+
'async function * foo() {await (yield bar);}',
24+
'await (1, Promise.resolve())',
25+
],
26+
invalid: [
27+
'await []',
28+
'await [Promise.resolve()]',
29+
'await (() => {})',
30+
'await (() => Promise.resolve())',
31+
'await (a === b)',
32+
'await (a instanceof Promise)',
33+
'await (a > b)',
34+
'await class {}',
35+
'await class extends Promise {}',
36+
'await function() {}',
37+
'await function name() {}',
38+
'await function() { return Promise.resolve() }',
39+
'await (<></>)',
40+
'await (<a></a>)',
41+
'await 0',
42+
'await 1',
43+
'await ""',
44+
'await "string"',
45+
'await true',
46+
'await false',
47+
'await null',
48+
'await 0n',
49+
'await 1n',
50+
// eslint-disable-next-line no-template-curly-in-string
51+
'await `${Promise.resolve()}`',
52+
'await !Promise.resolve()',
53+
'await void Promise.resolve()',
54+
'await +Promise.resolve()',
55+
'await ~1',
56+
'await ++foo',
57+
'await foo--',
58+
'await (Promise.resolve(), 1)',
59+
outdent`
60+
async function foo() {
61+
return await
62+
// comment
63+
1;
64+
}
65+
`,
66+
outdent`
67+
async function foo() {
68+
return await
69+
// comment
70+
1
71+
}
72+
`,
73+
outdent`
74+
async function foo() {
75+
return( await
76+
// comment
77+
1);
78+
}
79+
`,
80+
outdent`
81+
foo()
82+
await []
83+
`,
84+
outdent`
85+
foo()
86+
await +1
87+
`,
88+
outdent`
89+
async function foo() {
90+
return await
91+
// comment
92+
[];
93+
}
94+
`,
95+
outdent`
96+
async function foo() {
97+
throw await
98+
// comment
99+
1;
100+
}
101+
`,
102+
outdent`
103+
console.log(
104+
await
105+
// comment
106+
[]
107+
);
108+
`,
109+
'async function foo() {+await +1}',
110+
'async function foo() {-await-1}',
111+
'async function foo() {+await -1}',
112+
],
113+
});

0 commit comments

Comments
 (0)