Skip to content

Commit edf4614

Browse files
committed
feat(eslint-plugin): add new rule require-await
1 parent af70a59 commit edf4614

File tree

6 files changed

+336
-0
lines changed

6 files changed

+336
-0
lines changed

Diff for: packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
175175
| [`@typescript-eslint/prefer-string-starts-ends-with`](./docs/rules/prefer-string-starts-ends-with.md) | Enforce the use of `String#startsWith` and `String#endsWith` instead of other equivalent methods of checking substrings | | :wrench: | :thought_balloon: |
176176
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async | | | :thought_balloon: |
177177
| [`@typescript-eslint/require-array-sort-compare`](./docs/rules/require-array-sort-compare.md) | Enforce giving `compare` argument to `Array#sort` | | | :thought_balloon: |
178+
| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | | | :thought_balloon: |
178179
| [`@typescript-eslint/restrict-plus-operands`](./docs/rules/restrict-plus-operands.md) | When adding two variables, operands must both be of type number or of type string | | | :thought_balloon: |
179180
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
180181
| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: |

Diff for: packages/eslint-plugin/docs/rules/require-await.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Disallow async functions which have no await expression (@typescript-eslint/require-await)
2+
3+
Asynchronous functions that don’t use `await` might not need to be asynchronous functions and could be the unintentional result of refactoring.
4+
5+
## Rule Details
6+
7+
The `@typescript-eslint/require-await` rule extends the `require-await` rule from ESLint core, and allows for cases where the additional typing information can prevent false positives that would otherwise trigger the rule.
8+
9+
One example is when a function marked as `async` returns a value that is:
10+
11+
1. already a promise; or
12+
2. the result of calling another `async` function
13+
14+
```typescript
15+
async function numberOne(): Promise<number> {
16+
return Promise.resolve(1);
17+
}
18+
19+
async function getDataFromApi(endpoint: string): Promise<Response> {
20+
return fetch(endpoint);
21+
}
22+
```
23+
24+
In the above examples, the core `require-await` triggers the following warnings:
25+
26+
```
27+
async function 'numberOne' has no 'await' expression
28+
async function 'getDataFromApi' has no 'await' expression
29+
```
30+
31+
One way to resolve these errors is to remove the `async` keyword. However doing so can cause a conflict with the [`@typescript-eslint/promise-function-async`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/promise-function-async.md) rule (if enabled), which requires any function returning a promise to be marked as `async`.
32+
33+
Another way to resolve these errors is to add an `await` keyword to the return statements. However doing so can cause a conflict with the [`no-return-await`](https://eslint.org/docs/rules/no-return-await) rule (if enabled), which warns against using `return await` since the return value of an `async` function is always wrapped in `Promise.resolve` anyway.
34+
35+
With the additional typing information available in Typescript code, this extension to the `require-await` rule is able to look at the _actual_ return types of an `async` function (before being implicitly wrapped in `Promise.resolve`), and avoid the need for an `await` expression when the return value is already a promise.
36+
37+
See the [ESLint documentation](https://eslint.org/docs/rules/require-await) for more details on the `require-await` rule.
38+
39+
## Rule Changes
40+
41+
```cjson
42+
{
43+
// note you must disable the base rule as it can report incorrect errors
44+
"require-await": "off",
45+
"@typescript-eslint/require-await": "error"
46+
}
47+
```
48+
49+
<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/require-await.md)</sup>

Diff for: packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import preferRegexpExec from './prefer-regexp-exec';
5151
import preferStringStartsEndsWith from './prefer-string-starts-ends-with';
5252
import promiseFunctionAsync from './promise-function-async';
5353
import requireArraySortCompare from './require-array-sort-compare';
54+
import requireAwait from './require-await';
5455
import restrictPlusOperands from './restrict-plus-operands';
5556
import semi from './semi';
5657
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -113,6 +114,7 @@ export default {
113114
'prefer-string-starts-ends-with': preferStringStartsEndsWith,
114115
'promise-function-async': promiseFunctionAsync,
115116
'require-array-sort-compare': requireArraySortCompare,
117+
'require-await': requireAwait,
116118
'restrict-plus-operands': restrictPlusOperands,
117119
semi: semi,
118120
'strict-boolean-expressions': strictBooleanExpressions,

Diff for: packages/eslint-plugin/src/rules/require-await.ts

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
TSESTree,
3+
TSESLint,
4+
AST_NODE_TYPES,
5+
} from '@typescript-eslint/experimental-utils';
6+
import baseRule from 'eslint/lib/rules/require-await';
7+
import * as tsutils from 'tsutils';
8+
import ts from 'typescript';
9+
import * as util from '../util';
10+
11+
type Options = util.InferOptionsTypeFromRule<typeof baseRule>;
12+
type MessageIds = util.InferMessageIdsTypeFromRule<typeof baseRule>;
13+
14+
interface ScopeInfo {
15+
upper: ScopeInfo | null;
16+
returnsPromise: boolean;
17+
}
18+
19+
export default util.createRule<Options, MessageIds>({
20+
name: 'require-await',
21+
meta: {
22+
type: 'suggestion',
23+
docs: {
24+
description: 'Disallow async functions which have no `await` expression',
25+
category: 'Best Practices',
26+
recommended: false,
27+
},
28+
schema: baseRule.meta.schema,
29+
messages: baseRule.meta.messages,
30+
},
31+
defaultOptions: [],
32+
create(context) {
33+
const rules = baseRule.create(context);
34+
const parserServices = util.getParserServices(context);
35+
const checker = parserServices.program.getTypeChecker();
36+
37+
let scopeInfo: ScopeInfo | null = null;
38+
39+
/**
40+
* Push the scope info object to the stack.
41+
*
42+
* @returns {void}
43+
*/
44+
function enterFunction(
45+
node:
46+
| TSESTree.FunctionDeclaration
47+
| TSESTree.FunctionExpression
48+
| TSESTree.ArrowFunctionExpression,
49+
) {
50+
scopeInfo = {
51+
upper: scopeInfo,
52+
returnsPromise: false,
53+
};
54+
55+
switch (node.type) {
56+
case AST_NODE_TYPES.FunctionDeclaration:
57+
rules.FunctionDeclaration(node);
58+
break;
59+
60+
case AST_NODE_TYPES.FunctionExpression:
61+
rules.FunctionExpression(node);
62+
break;
63+
64+
case AST_NODE_TYPES.ArrowFunctionExpression:
65+
rules.ArrowFunctionExpression(node);
66+
break;
67+
}
68+
}
69+
70+
/**
71+
* Pop the top scope info object from the stack.
72+
* Passes through to the base rule if the function doesn't return a promise
73+
*
74+
* @param {ASTNode} node - The node exiting
75+
* @returns {void}
76+
*/
77+
function exitFunction(
78+
node:
79+
| TSESTree.FunctionDeclaration
80+
| TSESTree.FunctionExpression
81+
| TSESTree.ArrowFunctionExpression,
82+
) {
83+
if (scopeInfo) {
84+
if (!scopeInfo.returnsPromise) {
85+
switch (node.type) {
86+
case AST_NODE_TYPES.FunctionDeclaration:
87+
rules['FunctionDeclaration:exit'](node);
88+
break;
89+
90+
case AST_NODE_TYPES.FunctionExpression:
91+
rules['FunctionExpression:exit'](node);
92+
break;
93+
94+
case AST_NODE_TYPES.ArrowFunctionExpression:
95+
rules['ArrowFunctionExpression:exit'](node);
96+
break;
97+
}
98+
}
99+
100+
scopeInfo = scopeInfo.upper;
101+
}
102+
}
103+
104+
return {
105+
'FunctionDeclaration[async = true]': enterFunction,
106+
'FunctionExpression[async = true]': enterFunction,
107+
'ArrowFunctionExpression[async = true]': enterFunction,
108+
'FunctionDeclaration[async = true]:exit': exitFunction,
109+
'FunctionExpression[async = true]:exit': exitFunction,
110+
'ArrowFunctionExpression[async = true]:exit': exitFunction,
111+
112+
ReturnStatement(node: TSESTree.ReturnStatement) {
113+
if (!scopeInfo) {
114+
return;
115+
}
116+
117+
const { expression } = parserServices.esTreeNodeToTSNodeMap.get(
118+
node,
119+
) as ts.ReturnStatement;
120+
if (!expression) {
121+
return;
122+
}
123+
124+
const type = checker.getTypeAtLocation(expression);
125+
if (tsutils.isThenableType(checker, expression, type)) {
126+
scopeInfo.returnsPromise = true;
127+
}
128+
},
129+
130+
AwaitExpression: rules.AwaitExpression as TSESLint.RuleFunction<
131+
TSESTree.Node
132+
>,
133+
ForOfStatement: rules.ForOfStatement as TSESLint.RuleFunction<
134+
TSESTree.Node
135+
>,
136+
};
137+
},
138+
});
+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import rule from '../../src/rules/require-await';
2+
import { RuleTester, getFixturesRootDir } from '../RuleTester';
3+
4+
const rootDir = getFixturesRootDir();
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 2018,
9+
tsconfigRootDir: rootDir,
10+
project: './tsconfig.json',
11+
},
12+
parser: '@typescript-eslint/parser',
13+
});
14+
15+
const noAwaitFunctionDeclaration: any = {
16+
message: "Async function 'numberOne' has no 'await' expression.",
17+
};
18+
19+
const noAwaitFunctionExpression: any = {
20+
message: "Async function has no 'await' expression.",
21+
};
22+
23+
const noAwaitAsyncFunctionExpression: any = {
24+
message: "Async arrow function has no 'await' expression.",
25+
};
26+
27+
ruleTester.run('require-await', rule, {
28+
valid: [
29+
{
30+
// Non-async function declaration
31+
code: `function numberOne(): number {
32+
return 1;
33+
}`,
34+
},
35+
{
36+
// Non-async function expression
37+
code: `const numberOne = function(): number {
38+
return 1;
39+
}`,
40+
},
41+
{
42+
// Non-async arrow function expression
43+
code: `const numberOne = (): number => 1;`,
44+
},
45+
{
46+
// Async function declaration with await
47+
code: `async function numberOne(): Promise<number> {
48+
return await 1;
49+
}`,
50+
},
51+
{
52+
// Async function expression with await
53+
code: `const numberOne = async function(): Promise<number> {
54+
return await 1;
55+
}`,
56+
},
57+
{
58+
// Async arrow function expression with await
59+
code: `const numberOne = async (): Promise<number> => await 1;`,
60+
},
61+
{
62+
// Async function declaration with promise return
63+
code: `async function numberOne(): Promise<number> {
64+
return Promise.resolve(1);
65+
}`,
66+
},
67+
{
68+
// Async function expression with promise return
69+
code: `const numberOne = async function(): Promise<number> {
70+
return Promise.resolve(1);
71+
}`,
72+
},
73+
/*{
74+
// Async arrow function expression with promise return
75+
code: `const numberOne = async (): Promise<number> => Promise.resolve(1);`,
76+
},*/
77+
{
78+
// Async function declaration with async function return
79+
code: `async function numberOne(): Promise<number> {
80+
return getAsyncNumber(1);
81+
}
82+
async function getAsyncNumber(x: number): Promise<number> {
83+
return Promise.resolve(x);
84+
}`,
85+
},
86+
{
87+
// Async function expression with async function return
88+
code: `const numberOne = async function(): Promise<number> {
89+
return getAsyncNumber(1);
90+
}
91+
const getAsyncNumber = async function(x: number): Promise<number> {
92+
return Promise.resolve(x);
93+
}`,
94+
},
95+
/*{
96+
// Async arrow function expression with async function return
97+
code: `const numberOne = async (): Promise<number> => getAsyncNumber(1);
98+
const getAsyncNumber = async (x: number): Promise<number> => Promise.resolve(x);`,
99+
},*/
100+
],
101+
102+
invalid: [
103+
{
104+
// Async function declaration with no await
105+
code: `async function numberOne(): Promise<number> {
106+
return 1;
107+
}`,
108+
errors: [noAwaitFunctionDeclaration],
109+
},
110+
{
111+
// Async function expression with no await
112+
code: `const numberOne = async function(): Promise<number> {
113+
return 1;
114+
}`,
115+
errors: [noAwaitFunctionExpression],
116+
},
117+
{
118+
// Async arrow function expression with no await
119+
code: `const numberOne = async (): Promise<number> => 1;`,
120+
errors: [noAwaitAsyncFunctionExpression],
121+
},
122+
],
123+
});

Diff for: packages/eslint-plugin/typings/eslint-rules.d.ts

+23
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,29 @@ declare module 'eslint/lib/rules/no-extra-parens' {
409409
export = rule;
410410
}
411411

412+
declare module 'eslint/lib/rules/require-await' {
413+
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
414+
415+
const rule: TSESLint.RuleModule<
416+
never,
417+
[],
418+
{
419+
FunctionDeclaration(node: TSESTree.FunctionDeclaration): void;
420+
FunctionExpression(node: TSESTree.FunctionExpression): void;
421+
ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void;
422+
'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration): void;
423+
'FunctionExpression:exit'(node: TSESTree.FunctionExpression): void;
424+
'ArrowFunctionExpression:exit'(
425+
node: TSESTree.ArrowFunctionExpression,
426+
): void;
427+
ReturnStatement(node: TSESTree.ReturnStatement): void;
428+
AwaitExpression(node: TSESTree.AwaitExpression): void;
429+
ForOfStatement(node: TSESTree.ForOfStatement): void;
430+
}
431+
>;
432+
export = rule;
433+
}
434+
412435
declare module 'eslint/lib/rules/semi' {
413436
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
414437

0 commit comments

Comments
 (0)