Skip to content

Commit 807bc2d

Browse files
scottoharabradzacher
authored andcommitted
feat(eslint-plugin): add new rule require-await (#674)
1 parent f949355 commit 807bc2d

File tree

8 files changed

+329
-0
lines changed

8 files changed

+329
-0
lines changed

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: |
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>

packages/eslint-plugin/src/configs/all.json

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@typescript-eslint/prefer-string-starts-ends-with": "error",
6363
"@typescript-eslint/promise-function-async": "error",
6464
"@typescript-eslint/require-array-sort-compare": "error",
65+
"@typescript-eslint/require-await": "error",
6566
"@typescript-eslint/restrict-plus-operands": "error",
6667
"semi": "off",
6768
"@typescript-eslint/semi": "error",

packages/eslint-plugin/src/configs/recommended.json

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"camelcase": "off",
88
"@typescript-eslint/camelcase": "error",
99
"@typescript-eslint/class-name-casing": "error",
10+
"@typescript-eslint/consistent-type-definitions": "error",
1011
"@typescript-eslint/explicit-function-return-type": "warn",
1112
"@typescript-eslint/explicit-member-accessibility": "error",
1213
"indent": "off",

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import preferRegexpExec from './prefer-regexp-exec';
5252
import preferStringStartsEndsWith from './prefer-string-starts-ends-with';
5353
import promiseFunctionAsync from './promise-function-async';
5454
import requireArraySortCompare from './require-array-sort-compare';
55+
import requireAwait from './require-await';
5556
import restrictPlusOperands from './restrict-plus-operands';
5657
import semi from './semi';
5758
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -115,6 +116,7 @@ export default {
115116
'prefer-string-starts-ends-with': preferStringStartsEndsWith,
116117
'promise-function-async': promiseFunctionAsync,
117118
'require-array-sort-compare': requireArraySortCompare,
119+
'require-await': requireAwait,
118120
'restrict-plus-operands': restrictPlusOperands,
119121
semi: semi,
120122
'strict-boolean-expressions': strictBooleanExpressions,
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+
ts.ReturnStatement
119+
>(node);
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+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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 function declaration with async function return
75+
code: `async function numberOne(): Promise<number> {
76+
return getAsyncNumber(1);
77+
}
78+
async function getAsyncNumber(x: number): Promise<number> {
79+
return Promise.resolve(x);
80+
}`,
81+
},
82+
{
83+
// Async function expression with async function return
84+
code: `const numberOne = async function(): Promise<number> {
85+
return getAsyncNumber(1);
86+
}
87+
const getAsyncNumber = async function(x: number): Promise<number> {
88+
return Promise.resolve(x);
89+
}`,
90+
},
91+
],
92+
93+
invalid: [
94+
{
95+
// Async function declaration with no await
96+
code: `async function numberOne(): Promise<number> {
97+
return 1;
98+
}`,
99+
errors: [noAwaitFunctionDeclaration],
100+
},
101+
{
102+
// Async function expression with no await
103+
code: `const numberOne = async function(): Promise<number> {
104+
return 1;
105+
}`,
106+
errors: [noAwaitFunctionExpression],
107+
},
108+
{
109+
// Async arrow function expression with no await
110+
code: `const numberOne = async (): Promise<number> => 1;`,
111+
errors: [noAwaitAsyncFunctionExpression],
112+
},
113+
],
114+
});

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)