Skip to content

feat(eslint-plugin): added new rule promise-function-async #194

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

1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements (`no-var-requires` from TSLint) | :heavy_check_mark: | |
| [`@typescript-eslint/prefer-interface`](./docs/rules/prefer-interface.md) | Prefer an interface declaration over a type literal (type T = { ... }) (`interface-over-type-literal` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules. (`no-internal-module` from TSLint) | :heavy_check_mark: | :wrench: |
| [`@typescript-eslint/promise-function-async`](./docs/rules/promise-function-async.md) | Requires any function or method that returns a Promise to be marked async. (`promise-function-async` from TSLint) | :heavy_check_mark: | |
| [`@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. (`restrict-plus-operands` from TSLint) | | |
| [`@typescript-eslint/type-annotation-spacing`](./docs/rules/type-annotation-spacing.md) | Require consistent spacing around type annotations (`typedef-whitespace` from TSLint) | :heavy_check_mark: | :wrench: |

Expand Down
2 changes: 1 addition & 1 deletion packages/eslint-plugin/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
| [`no-var-requires`] | ✅ | [`@typescript-eslint/no-var-requires`] |
| [`only-arrow-functions`] | 🔌 | [`prefer-arrow/prefer-arrow-functions`] |
| [`prefer-for-of`] | 🛑 | N/A |
| [`promise-function-async`] | 🛑 | N/A ([relevant plugin][plugin:promise]) |
| [`promise-function-async`] | | [`@typescript-eslint/promise-function-async`] |
| [`typedef`] | 🛑 | N/A |
| [`typedef-whitespace`] | ✅ | [`@typescript-eslint/type-annotation-spacing`] |
| [`unified-signatures`] | 🛑 | N/A |
Expand Down
65 changes: 65 additions & 0 deletions packages/eslint-plugin/docs/rules/promise-function-async.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Functions that return promises must be async (promise-function-async)

Requires any function or method that returns a Promise to be marked async.
Ensures that each function is only capable of:

- returning a rejected promise, or
- throwing an Error object.

In contrast, non-`async` `Promise`-returning functions are technically capable of either.
Code that handles the results of those functions will often need to handle both cases, which can get complex.
This rule's practice removes a requirement for creating code to handle both cases.

## Rule Details

Examples of **incorrect** code for this rule

```ts
const arrowFunctionReturnsPromise = () => Promise.resolve('value');

function functionDeturnsPromise() {
return Math.random() > 0.5 ? Promise.resolve('value') : false;
}
```

Examples of **correct** code for this rule

```ts
const arrowFunctionReturnsPromise = async () => 'value';

async function functionDeturnsPromise() {
return Math.random() > 0.5 ? 'value' : false;
}
```

## Options

Options may be provided as an object with:

- `allowedPromiseNames` to indicate any extra names of classes or interfaces to be considered Promises when returned.

In addition, each of the following properties may be provided, and default to `true`:

- `checkArrowFunctions`
- `checkFunctionDeclarations`
- `checkFunctionExpressions`
- `checkMethodDeclarations`

```json
{
"@typescript-eslint/promise-function-async": [
"error",
{
"allowedPromiseNames": ["Thenable"],
"checkArrowFunctions": true,
"checkFunctionDeclarations": true,
"checkFunctionExpressions": true,
"checkMethodDeclarations": true
}
]
}
```

## Related To

- TSLint: [promise-function-async](https://palantir.github.io/tslint/rules/promise-function-async)
125 changes: 125 additions & 0 deletions packages/eslint-plugin/lib/rules/promise-function-async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* @fileoverview Requires any function or method that returns a Promise to be marked async
* @author Josh Goldberg <https://github.com/joshuakgoldberg>
*/
'use strict';

const util = require('../util');
const types = require('../utils/types');

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

const defaultOptions = [
{
allowedPromiseNames: [],
checkArrowFunctions: true,
checkFunctionDeclarations: true,
checkFunctionExpressions: true,
checkMethodDeclarations: true
}
];

/**
* @type {import("eslint").Rule.RuleModule}
*/
module.exports = {
meta: {
type: 'suggestion',
docs: {
description:
'Requires any function or method that returns a Promise to be marked async.',
extraDescription: [util.tslintRule('promise-function-async')],
category: 'TypeScript',
url: util.metaDocsUrl('promise-function-async'),
recommended: 'error'
},
fixable: null,
messages: {
missingAsync: 'Functions that return promises must be async.'
},
schema: [
{
type: 'object',
properties: {
allowedPromiseNames: {
type: 'array',
items: {
type: 'string'
}
},
checkArrowFunctions: {
type: 'boolean'
},
checkFunctionDeclarations: {
type: 'boolean'
},
checkFunctionExpressions: {
type: 'boolean'
},
checkMethodDeclarations: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},

create(context) {
const {
allowedPromiseNames,
checkArrowFunctions,
checkFunctionDeclarations,
checkFunctionExpressions,
checkMethodDeclarations
} = util.applyDefault(defaultOptions, context.options)[0];

const allAllowedPromiseNames = new Set(['Promise', ...allowedPromiseNames]);
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

/**
* @param {import("estree").Function} node
*/
function validateNode(node) {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const [callSignature] = checker
.getTypeAtLocation(originalNode)
.getCallSignatures();
const returnType = checker.getReturnTypeOfSignature(callSignature);

if (!types.containsTypeByName(returnType, allAllowedPromiseNames)) {
return;
}

context.report({
messageId: 'missingAsync',
node
});
}

return {
ArrowFunctionExpression(node) {
if (checkArrowFunctions && !node.async) {
validateNode(node);
}
},
FunctionDeclaration(node) {
if (checkFunctionDeclarations && !node.async) {
validateNode(node);
}
},
FunctionExpression(node) {
if (!!node.parent && node.parent.kind === 'method') {
if (checkMethodDeclarations && !node.async) {
validateNode(node.parent);
}
} else if (checkFunctionExpressions && !node.async) {
validateNode(node);
}
}
};
}
};
38 changes: 38 additions & 0 deletions packages/eslint-plugin/lib/utils/types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
'use strict';

const tsutils = require('tsutils');
const ts = require('typescript');

/**
* @param {string} type Type being checked by name.
* @param {Set<string>} allowedNames Symbol names checking on the type.
* @returns {boolean} Whether the type is, extends, or contains any of the allowed names.
*/
function containsTypeByName(type, allowedNames) {
if (tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
return true;
}

if (tsutils.isTypeReference(type)) {
type = type.target;
}

if (
typeof type.symbol !== 'undefined' &&
allowedNames.has(type.symbol.name)
) {
return true;
}

if (tsutils.isUnionOrIntersectionType(type)) {
return type.types.some(t => containsTypeByName(t, allowedNames));
}

const bases = type.getBaseTypes();
return (
typeof bases !== 'undefined' &&
bases.some(t => containsTypeByName(t, allowedNames))
);
}

exports.containsTypeByName = containsTypeByName;
Loading