Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 1 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 2 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 65 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 125 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 38 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
Morty Proxy This is a proxified and sanitized view of the page, visit original site.