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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy