Skip to content
Permalink
Browse files

feat(eslint-plugin): add return-await rule (#1050)

  • Loading branch information
drabinowitz authored and bradzacher committed Nov 25, 2019
1 parent efd4834 commit 0ff4620ca892816dfee9f77143ae9cdce34dbfab
@@ -204,6 +204,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: |
| [`@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: |
| [`@typescript-eslint/restrict-template-expressions`](./docs/rules/restrict-template-expressions.md) | Enforce template literal expressions to be of string type | | | :thought_balloon: |
| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Rules for awaiting returned promises | | | :thought_balloon: |
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | enforce consistent spacing before `function` definition opening parenthesis | | :wrench: | |
| [`@typescript-eslint/strict-boolean-expressions`](./docs/rules/strict-boolean-expressions.md) | Restricts the types allowed in boolean expressions | | | :thought_balloon: |
@@ -0,0 +1,123 @@
# Require/Disallow returning awaited values in specific contexts (@typescript-eslint/return-await)

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).

## Rule Details

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.

## Options

`in-try-catch` (default): `await`-ing a returned promise is required in `try-catch-finally` blocks and disallowed elsewhere.

`always`: `await`-ing a returned promise is required everywhere.

`never`: `await`-ing a returned promise is disallowed everywhere.

```typescript
// valid in-try-catch
async function validInTryCatch1() {
try {
return await Promise.resolve('try');
} catch (e) {}
}
async function validInTryCatch2() {
return Promise.resolve('try');
}
async function validInTryCatch3() {
return 'value';
}
// valid always
async function validAlways1() {
try {
return await Promise.resolve('try');
} catch (e) {}
}
async function validAlways2() {
return await Promise.resolve('try');
}
async function validAlways3() {
return 'value';
}
// valid never
async function validNever1() {
try {
return Promise.resolve('try');
} catch (e) {}
}
async function validNever2() {
return Promise.resolve('try');
}
async function validNever3() {
return 'value';
}
```

```typescript
// invalid in-try-catch
async function invalidInTryCatch1() {
try {
return Promise.resolve('try');
} catch (e) {}
}
async function invalidInTryCatch2() {
return await Promise.resolve('try');
}
async function invalidInTryCatch3() {
return await 'value';
}
// invalid always
async function invalidAlways1() {
try {
return Promise.resolve('try');
} catch (e) {}
}
async function invalidAlways2() {
return Promise.resolve('try');
}
async function invalidAlways3() {
return await 'value';
}
// invalid never
async function invalidNever1() {
try {
return await Promise.resolve('try');
} catch (e) {}
}
async function invalidNever2() {
return await Promise.resolve('try');
}
async function invalidNever3() {
return await 'value';
}
```

The rule also applies to `finally` blocks. So the following would be invalid with default options:

```typescript
async function invalid() {
try {
return await Promise.resolve('try');
} catch (e) {
return Promise.resolve('catch');
} finally {
// cleanup
}
}
```
@@ -79,6 +79,7 @@
"@typescript-eslint/require-await": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/restrict-template-expressions": "error",
"@typescript-eslint/return-await": "error",
"semi": "off",
"@typescript-eslint/semi": "error",
"space-before-function-paren": "off",
@@ -39,11 +39,12 @@ import noThisAlias from './no-this-alias';
import noTypeAlias from './no-type-alias';
import noUnnecessaryCondition from './no-unnecessary-condition';
import noUnnecessaryQualifier from './no-unnecessary-qualifier';
import useDefaultTypeParameter from './no-unnecessary-type-arguments';
import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion';
import noUnusedVars from './no-unused-vars';
import noUnusedVarsExperimental from './no-unused-vars-experimental';
import noUntypedPublicSignature from './no-untyped-public-signature';
import noUnusedExpressions from './no-unused-expressions';
import noUnusedVars from './no-unused-vars';
import noUnusedVarsExperimental from './no-unused-vars-experimental';
import noUseBeforeDefine from './no-use-before-define';
import noUselessConstructor from './no-useless-constructor';
import noVarRequires from './no-var-requires';
@@ -61,6 +62,7 @@ import requireArraySortCompare from './require-array-sort-compare';
import requireAwait from './require-await';
import restrictPlusOperands from './restrict-plus-operands';
import restrictTemplateExpressions from './restrict-template-expressions';
import returnAwait from './return-await';
import semi from './semi';
import spaceBeforeFunctionParen from './space-before-function-paren';
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -69,7 +71,6 @@ import typeAnnotationSpacing from './type-annotation-spacing';
import typedef from './typedef';
import unboundMethod from './unbound-method';
import unifiedSignatures from './unified-signatures';
import useDefaultTypeParameter from './no-unnecessary-type-arguments';

export default {
'adjacent-overload-signatures': adjacentOverloadSignatures,
@@ -136,6 +137,7 @@ export default {
'require-await': requireAwait,
'restrict-plus-operands': restrictPlusOperands,
'restrict-template-expressions': restrictTemplateExpressions,
'return-await': returnAwait,
semi: semi,
'space-before-function-paren': spaceBeforeFunctionParen,
'strict-boolean-expressions': strictBooleanExpressions,
@@ -0,0 +1,156 @@
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as tsutils from 'tsutils';
import ts, { SyntaxKind } from 'typescript';
import * as util from '../util';

export default util.createRule({
name: 'return-await',
meta: {
docs: {
description: 'Rules for awaiting returned promises',
category: 'Best Practices',
recommended: false,
requiresTypeChecking: true,
},
type: 'problem',
messages: {
nonPromiseAwait:
'returning an awaited value that is not a promise is not allowed',
disallowedPromiseAwait:
'returning an awaited promise is not allowed in this context',
requiredPromiseAwait:
'returning an awaited promise is required in this context',
},
schema: [
{
enum: ['in-try-catch', 'always', 'never'],
},
],
},
defaultOptions: ['in-try-catch'],

create(context, [option]) {
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

function inTryCatch(node: ts.Node): boolean {
let ancestor = node.parent;

while (ancestor && !ts.isFunctionLike(ancestor)) {
if (
tsutils.isTryStatement(ancestor) ||
tsutils.isCatchClause(ancestor)
) {
return true;
}

ancestor = ancestor.parent;
}

return false;
}

function test(
node: TSESTree.ReturnStatement | TSESTree.ArrowFunctionExpression,
expression: ts.Node,
): void {
let child: ts.Node;

const isAwait = expression.kind === SyntaxKind.AwaitExpression;

if (isAwait) {
child = expression.getChildAt(1);
} else {
child = expression;
}

const type = checker.getTypeAtLocation(child);

const isThenable =
tsutils.isTypeFlagSet(type, ts.TypeFlags.Any) ||
tsutils.isTypeFlagSet(type, ts.TypeFlags.Unknown) ||
tsutils.isThenableType(checker, expression, type);

if (!isAwait && !isThenable) {
return;
}

if (isAwait && !isThenable) {
context.report({
messageId: 'nonPromiseAwait',
node,
});
return;
}

if (option === 'always') {
if (!isAwait && isThenable) {
context.report({
messageId: 'requiredPromiseAwait',
node,
});
}

return;
}

if (option === 'never') {
if (isAwait) {
context.report({
messageId: 'disallowedPromiseAwait',
node,
});
}

return;
}

if (option === 'in-try-catch') {
const isInTryCatch = inTryCatch(expression);
if (isAwait && !isInTryCatch) {
context.report({
messageId: 'disallowedPromiseAwait',
node,
});
} else if (!isAwait && isInTryCatch) {
context.report({
messageId: 'requiredPromiseAwait',
node,
});
}

return;
}
}

return {
'ArrowFunctionExpression[async = true]:exit'(
node: TSESTree.ArrowFunctionExpression,
): void {
if (node.body.type !== AST_NODE_TYPES.BlockStatement) {
const expression = parserServices.esTreeNodeToTSNodeMap.get(
node.body,
);

test(node, expression);
}
},
ReturnStatement(node): void {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get<
ts.ReturnStatement
>(node);

const { expression } = originalNode;

if (!expression) {
return;
}

test(node, expression);
},
};
},
});

0 comments on commit 0ff4620

Please sign in to comment.
You can’t perform that action at this time.