Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0ff4620

Browse files
drabinowitzbradzacher
authored andcommitted
feat(eslint-plugin): add return-await rule (typescript-eslint#1050)
1 parent efd4834 commit 0ff4620

File tree

7 files changed

+671
-5
lines changed

7 files changed

+671
-5
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
204204
| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: |
205205
| [`@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: |
206206
| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: |
207+
| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Rules for awaiting returned promises | | | :thought_balloon: |
207208
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
208209
| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :wrench: | |
209210
| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: |
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Require/Disallow returning awaited values in specific contexts (@typescript-eslint/return-await)
2+
3+
Returning an awaited promise can make sense for better stack trace information as well as for consistent error handling (returned promises will not be caught in an async function try/catch).
4+
5+
## Rule Details
6+
7+
The `@typescript-eslint/return-await` rule specifies that awaiting a returned non-promise is never allowed. By default, the rule requires awaiting a returned promise in a `try-catch-finally` block and disallows returning an awaited promise in any other context. Optionally, the rule can require awaiting returned promises in all contexts, or disallow them in all contexts.
8+
9+
## Options
10+
11+
`in-try-catch` (default): `await`-ing a returned promise is required in `try-catch-finally` blocks and disallowed elsewhere.
12+
13+
`always`: `await`-ing a returned promise is required everywhere.
14+
15+
`never`: `await`-ing a returned promise is disallowed everywhere.
16+
17+
```typescript
18+
// valid in-try-catch
19+
async function validInTryCatch1() {
20+
try {
21+
return await Promise.resolve('try');
22+
} catch (e) {}
23+
}
24+
25+
async function validInTryCatch2() {
26+
return Promise.resolve('try');
27+
}
28+
29+
async function validInTryCatch3() {
30+
return 'value';
31+
}
32+
33+
// valid always
34+
async function validAlways1() {
35+
try {
36+
return await Promise.resolve('try');
37+
} catch (e) {}
38+
}
39+
40+
async function validAlways2() {
41+
return await Promise.resolve('try');
42+
}
43+
44+
async function validAlways3() {
45+
return 'value';
46+
}
47+
48+
// valid never
49+
async function validNever1() {
50+
try {
51+
return Promise.resolve('try');
52+
} catch (e) {}
53+
}
54+
55+
async function validNever2() {
56+
return Promise.resolve('try');
57+
}
58+
59+
async function validNever3() {
60+
return 'value';
61+
}
62+
```
63+
64+
```typescript
65+
// invalid in-try-catch
66+
async function invalidInTryCatch1() {
67+
try {
68+
return Promise.resolve('try');
69+
} catch (e) {}
70+
}
71+
72+
async function invalidInTryCatch2() {
73+
return await Promise.resolve('try');
74+
}
75+
76+
async function invalidInTryCatch3() {
77+
return await 'value';
78+
}
79+
80+
// invalid always
81+
async function invalidAlways1() {
82+
try {
83+
return Promise.resolve('try');
84+
} catch (e) {}
85+
}
86+
87+
async function invalidAlways2() {
88+
return Promise.resolve('try');
89+
}
90+
91+
async function invalidAlways3() {
92+
return await 'value';
93+
}
94+
95+
// invalid never
96+
async function invalidNever1() {
97+
try {
98+
return await Promise.resolve('try');
99+
} catch (e) {}
100+
}
101+
102+
async function invalidNever2() {
103+
return await Promise.resolve('try');
104+
}
105+
106+
async function invalidNever3() {
107+
return await 'value';
108+
}
109+
```
110+
111+
The rule also applies to `finally` blocks. So the following would be invalid with default options:
112+
113+
```typescript
114+
async function invalid() {
115+
try {
116+
return await Promise.resolve('try');
117+
} catch (e) {
118+
return Promise.resolve('catch');
119+
} finally {
120+
// cleanup
121+
}
122+
}
123+
```

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"@typescript-eslint/require-await": "error",
8080
"@typescript-eslint/restrict-plus-operands": "error",
8181
"@typescript-eslint/restrict-template-expressions": "error",
82+
"@typescript-eslint/return-await": "error",
8283
"semi": "off",
8384
"@typescript-eslint/semi": "error",
8485
"space-before-function-paren": "off",

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ import noThisAlias from './no-this-alias';
3939
import noTypeAlias from './no-type-alias';
4040
import noUnnecessaryCondition from './no-unnecessary-condition';
4141
import noUnnecessaryQualifier from './no-unnecessary-qualifier';
42+
import useDefaultTypeParameter from './no-unnecessary-type-arguments';
4243
import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
43-
import noUnusedVars from './no-unused-vars';
44-
import noUnusedVarsExperimental from './no-unused-vars-experimental';
4544
import noUntypedPublicSignature from './no-untyped-public-signature';
4645
import noUnusedExpressions from './no-unused-expressions';
46+
import noUnusedVars from './no-unused-vars';
47+
import noUnusedVarsExperimental from './no-unused-vars-experimental';
4748
import noUseBeforeDefine from './no-use-before-define';
4849
import noUselessConstructor from './no-useless-constructor';
4950
import noVarRequires from './no-var-requires';
@@ -61,6 +62,7 @@ import requireArraySortCompare from './require-array-sort-compare';
6162
import requireAwait from './require-await';
6263
import restrictPlusOperands from './restrict-plus-operands';
6364
import restrictTemplateExpressions from './restrict-template-expressions';
65+
import returnAwait from './return-await';
6466
import semi from './semi';
6567
import spaceBeforeFunctionParen from './space-before-function-paren';
6668
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -69,7 +71,6 @@ import typeAnnotationSpacing from './type-annotation-spacing';
6971
import typedef from './typedef';
7072
import unboundMethod from './unbound-method';
7173
import unifiedSignatures from './unified-signatures';
72-
import useDefaultTypeParameter from './no-unnecessary-type-arguments';
7374

7475
export default {
7576
'adjacent-overload-signatures': adjacentOverloadSignatures,
@@ -136,6 +137,7 @@ export default {
136137
'require-await': requireAwait,
137138
'restrict-plus-operands': restrictPlusOperands,
138139
'restrict-template-expressions': restrictTemplateExpressions,
140+
'return-await': returnAwait,
139141
semi: semi,
140142
'space-before-function-paren': spaceBeforeFunctionParen,
141143
'strict-boolean-expressions': strictBooleanExpressions,
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
AST_NODE_TYPES,
3+
TSESTree,
4+
} from '@typescript-eslint/experimental-utils';
5+
import * as tsutils from 'tsutils';
6+
import ts, { SyntaxKind } from 'typescript';
7+
import * as util from '../util';
8+
9+
export default util.createRule({
10+
name: 'return-await',
11+
meta: {
12+
docs: {
13+
description: 'Rules for awaiting returned promises',
14+
category: 'Best Practices',
15+
recommended: false,
16+
requiresTypeChecking: true,
17+
},
18+
type: 'problem',
19+
messages: {
20+
nonPromiseAwait:
21+
'returning an awaited value that is not a promise is not allowed',
22+
disallowedPromiseAwait:
23+
'returning an awaited promise is not allowed in this context',
24+
requiredPromiseAwait:
25+
'returning an awaited promise is required in this context',
26+
},
27+
schema: [
28+
{
29+
enum: ['in-try-catch', 'always', 'never'],
30+
},
31+
],
32+
},
33+
defaultOptions: ['in-try-catch'],
34+
35+
create(context, [option]) {
36+
const parserServices = util.getParserServices(context);
37+
const checker = parserServices.program.getTypeChecker();
38+
39+
function inTryCatch(node: ts.Node): boolean {
40+
let ancestor = node.parent;
41+
42+
while (ancestor && !ts.isFunctionLike(ancestor)) {
43+
if (
44+
tsutils.isTryStatement(ancestor) ||
45+
tsutils.isCatchClause(ancestor)
46+
) {
47+
return true;
48+
}
49+
50+
ancestor = ancestor.parent;
51+
}
52+
53+
return false;
54+
}
55+
56+
function test(
57+
node: TSESTree.ReturnStatement | TSESTree.ArrowFunctionExpression,
58+
expression: ts.Node,
59+
): void {
60+
let child: ts.Node;
61+
62+
const isAwait = expression.kind === SyntaxKind.AwaitExpression;
63+
64+
if (isAwait) {
65+
child = expression.getChildAt(1);
66+
} else {
67+
child = expression;
68+
}
69+
70+
const type = checker.getTypeAtLocation(child);
71+
72+
const isThenable =
73+
tsutils.isTypeFlagSet(type, ts.TypeFlags.Any) ||
74+
tsutils.isTypeFlagSet(type, ts.TypeFlags.Unknown) ||
75+
tsutils.isThenableType(checker, expression, type);
76+
77+
if (!isAwait && !isThenable) {
78+
return;
79+
}
80+
81+
if (isAwait && !isThenable) {
82+
context.report({
83+
messageId: 'nonPromiseAwait',
84+
node,
85+
});
86+
return;
87+
}
88+
89+
if (option === 'always') {
90+
if (!isAwait && isThenable) {
91+
context.report({
92+
messageId: 'requiredPromiseAwait',
93+
node,
94+
});
95+
}
96+
97+
return;
98+
}
99+
100+
if (option === 'never') {
101+
if (isAwait) {
102+
context.report({
103+
messageId: 'disallowedPromiseAwait',
104+
node,
105+
});
106+
}
107+
108+
return;
109+
}
110+
111+
if (option === 'in-try-catch') {
112+
const isInTryCatch = inTryCatch(expression);
113+
if (isAwait && !isInTryCatch) {
114+
context.report({
115+
messageId: 'disallowedPromiseAwait',
116+
node,
117+
});
118+
} else if (!isAwait && isInTryCatch) {
119+
context.report({
120+
messageId: 'requiredPromiseAwait',
121+
node,
122+
});
123+
}
124+
125+
return;
126+
}
127+
}
128+
129+
return {
130+
'ArrowFunctionExpression[async = true]:exit'(
131+
node: TSESTree.ArrowFunctionExpression,
132+
): void {
133+
if (node.body.type !== AST_NODE_TYPES.BlockStatement) {
134+
const expression = parserServices.esTreeNodeToTSNodeMap.get(
135+
node.body,
136+
);
137+
138+
test(node, expression);
139+
}
140+
},
141+
ReturnStatement(node): void {
142+
const originalNode = parserServices.esTreeNodeToTSNodeMap.get<
143+
ts.ReturnStatement
144+
>(node);
145+
146+
const { expression } = originalNode;
147+
148+
if (!expression) {
149+
return;
150+
}
151+
152+
test(node, expression);
153+
},
154+
};
155+
},
156+
});

0 commit comments

Comments
 (0)