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

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Nov 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d489db2
feat(eslint-plugin): add return-await rule (#994)
drabinowitz Oct 8, 2019
9e2d657
Merge branch 'master' into master
drabinowitz Oct 8, 2019
5955244
Remove commented code
drabinowitz Oct 8, 2019
f5519d5
feat(eslint-plugin): fix return-await rule documentation (#994)
drabinowitz Oct 8, 2019
a9ce3ef
Merge branch 'master' of github.com:drabinowitz/typescript-eslint
drabinowitz Oct 8, 2019
1f46d45
feat(eslint-plugin): fix return-await rule config (#994)
drabinowitz Oct 8, 2019
459f668
feat(eslint-plugin): remove disallowed return-await (#994)
drabinowitz Oct 8, 2019
5cb5f4e
Merge branch 'master' into master
drabinowitz Oct 9, 2019
1801e05
Merge branch 'master' into master
drabinowitz Oct 10, 2019
61ba0d6
feat(eslint-plugin): add more tests for different codepaths
drabinowitz Oct 11, 2019
d80f954
Merge branch 'master' of github.com:drabinowitz/typescript-eslint
drabinowitz Oct 11, 2019
1fd694e
Merge branch 'master' into master
drabinowitz Oct 13, 2019
31ddcde
Merge branch 'master' into master
drabinowitz Oct 14, 2019
84fcd24
Merge branch 'master' into master
drabinowitz Oct 14, 2019
c5f25e2
Merge branch 'master' into master
drabinowitz Oct 16, 2019
a3e522a
Merge branch 'master' into master
drabinowitz Oct 17, 2019
8bf3144
Merge branch 'master' into master
drabinowitz Oct 21, 2019
8dc7f72
Merge branch 'master' into master
drabinowitz Oct 23, 2019
a3a5747
Merge branch 'master' into master
drabinowitz Nov 4, 2019
7dbf1da
Merge branch 'master' into master
drabinowitz Nov 18, 2019
73db70a
Merge branch 'master' of github.com:typescript-eslint/typescript-eslint
drabinowitz Nov 25, 2019
7fbe46b
docs(return-await): remove return-await from recommended rules
drabinowitz Nov 25, 2019
f687b93
Merge branch 'master' of github.com:drabinowitz/typescript-eslint
drabinowitz Nov 25, 2019
ea16344
Merge branch 'master' into master
drabinowitz Nov 25, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
123 changes: 123 additions & 0 deletions packages/eslint-plugin/docs/rules/return-await.md
Original file line number Diff line number Diff line change
@@ -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
}
}
```
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions packages/eslint-plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
156 changes: 156 additions & 0 deletions packages/eslint-plugin/src/rules/return-await.ts
Original file line number Diff line number Diff line change
@@ -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);
},
};
},
});
Loading