Skip to content

Commit

Permalink
Add no-single-promise-in-promise-methods rule
Browse files Browse the repository at this point in the history
  • Loading branch information
Clement398 committed Jan 23, 2024
1 parent 702d51b commit 80e3343
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 0 deletions.
1 change: 1 addition & 0 deletions configs/recommended.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module.exports = {
'unicorn/no-null': 'error',
'unicorn/no-object-as-default-parameter': 'error',
'unicorn/no-process-exit': 'error',
'unicorn/no-single-promise-in-promise-methods': 'error',
'unicorn/no-static-only-class': 'error',
'unicorn/no-thenable': 'error',
'unicorn/no-this-assignment': 'error',
Expand Down
34 changes: 34 additions & 0 deletions docs/rules/no-single-promise-in-promise-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Disallow using `Promise` method with a single element array as parameter

💼 This rule is enabled in the ✅ `recommended` [config](https:/sindresorhus/eslint-plugin-unicorn#preset-configs).

🔧💡 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix) and manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).

<!-- end auto-generated rule header -->
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->

Single element array parameter in a Promise.all(), Promise.any() or Promise.race() method is probably a mistake.

## Fail

```js
Promise.all([promise])

Promise.any([promise])

Promise.race([promise])
```

## Pass

```js
Promise.all([promise, anotherPromise])
Promise.all(notArrayLiteral)
Promise.all([...promises])

Promise.any([promise, anotherPromise])

Promise.race([promise, anotherPromise])

Promise.allSettled([promise])
```
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ If you don't use the preset, ensure you use the same `env` and `parserOptions` c
| [no-null](docs/rules/no-null.md) | Disallow the use of the `null` literal. || 🔧 | 💡 |
| [no-object-as-default-parameter](docs/rules/no-object-as-default-parameter.md) | Disallow the use of objects as default parameters. || | |
| [no-process-exit](docs/rules/no-process-exit.md) | Disallow `process.exit()`. || | |
| [no-single-promise-in-promise-methods](docs/rules/no-single-promise-in-promise-methods.md) | Disallow using `Promise` method with a single element array as parameter. || 🔧 | 💡 |
| [no-static-only-class](docs/rules/no-static-only-class.md) | Disallow classes that only have static members. || 🔧 | |
| [no-thenable](docs/rules/no-thenable.md) | Disallow `then` property. || | |
| [no-this-assignment](docs/rules/no-this-assignment.md) | Disallow assigning `this` to a variable. || | |
Expand Down
95 changes: 95 additions & 0 deletions rules/no-single-promise-in-promise-methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';
const isPromiseMethodWithArray = require('./utils/is-promise-method-with-array.js');

const MESSAGE_ID_ERROR = 'no-single-promise-in-promise-methods/error';
const MESSAGE_ID_SUGGESTION_1 = 'no-single-promise-in-promise-methods/suggestion-1';
const MESSAGE_ID_SUGGESTION_2 = 'no-single-promise-in-promise-methods/suggestion-2';
const messages = {
[MESSAGE_ID_ERROR]: 'Parameter in `Promise.{{method}}` should not be a single element array.',
[MESSAGE_ID_SUGGESTION_1]: 'Use the value directly.',
[MESSAGE_ID_SUGGESTION_2]: 'Wrap the value in a `Promise.resolve`.',
};
const METHODS = ['all', 'any', 'race'];

const isPromiseMethodWithSinglePromise = (node, methods) => {
const types = new Set(['CallExpression', 'Identifier', 'MemberExpression']);

if (!isPromiseMethodWithArray(node, methods) || node.arguments[0].elements.length !== 1) {
return false;
}

const [element] = node.arguments[0].elements;

return types.has(element.type)
|| (element.type === 'AwaitExpression' && types.has(element.argument.type));
};

const getMethodName = node => node.callee.property.name;

const getAutoFixer = ({sourceCode}, node) => fixer => {
const [element] = node.arguments[0].elements;
const elementWithoutAwait = element.type === 'AwaitExpression' ? element.argument : element;

return fixer.replaceText(node, sourceCode.getText(elementWithoutAwait));
};

const getSuggestion1Fixer = ({sourceCode}, node) => fixer =>
fixer.replaceText(node, sourceCode.getText(node.arguments[0].elements[0]));

const getSuggestion2Fixer = ({sourceCode}, node) => fixer => {
const text = sourceCode.getText(node.arguments[0].elements[0]);

return fixer.replaceText(node, `Promise.resolve(${text})`);
};

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
CallExpression(node) {
if (!isPromiseMethodWithSinglePromise(node, METHODS)) {
return;
}

const descriptor = {
node,
messageId: MESSAGE_ID_ERROR,
data: {
method: getMethodName(node),
},
};

if (node.parent.type === 'AwaitExpression') {
context.report({
...descriptor,
fix: getAutoFixer(context, node),
});
} else {
context.report({
...descriptor,
suggest: [
{
messageId: MESSAGE_ID_SUGGESTION_1,
fix: getSuggestion1Fixer(context, node),
},
{
messageId: MESSAGE_ID_SUGGESTION_2,
fix: getSuggestion2Fixer(context, node),
},
],
});
}
},
});

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow using `Promise` method with a single element array as parameter.',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
13 changes: 13 additions & 0 deletions rules/utils/is-promise-method-with-array.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use strict';
const {isMethodCall} = require('../ast/index.js');

const isPromiseMethodWithArray = (node, methods) =>
node.callee.type === 'MemberExpression'
&& node.callee.object.type === 'Identifier'
&& node.callee.object.name === 'Promise'
&& isMethodCall(node, methods)
&& node.arguments.length === 1
&& node.arguments[0].type === 'ArrayExpression'
&& node.arguments[0].elements.some(element => element !== null);

module.exports = isPromiseMethodWithArray;
72 changes: 72 additions & 0 deletions test/no-single-promise-in-promise-methods.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {getTester} from './utils/test.mjs';

const {test} = getTester(import.meta);

const error = {
messageId: 'no-single-promise-in-promise-methods/error',
};

test({
valid: [
'Promise.all([promise, anotherPromise])',
'Promise.all(notArrayLiteral)',
'Promise.all([...promises])',
'Promise.all([await -1])',
'Promise.any([promise, anotherPromise])',
'Promise.race([promise, anotherPromise])',
'Promise.allSettled([promise])',
'Promise[all]([promise])',
'Promise.all([,])',
],

invalid: [
{
code: 'await Promise.all([promise])',
errors: [error],
output: 'await promise',
},
{
code: 'await Promise.all([func()])',
errors: [error],
output: 'await func()',
},
{
code: 'await Promise.all([promises[0]])',
errors: [error],
output: 'await promises[0]',
},
{
code: 'await Promise.all([await promise])',
errors: [error],
output: 'await promise',
},
{
code: 'await Promise.any([promise])',
errors: [error],
output: 'await promise',
},
{
code: 'await Promise.race([promise])',
errors: [error],
output: 'await promise',
},
{
code: 'Promise.all([somethingMaybeNotPromise])',
errors: [
{
...error,
suggestions: [
{
messageId: 'no-single-promise-in-promise-methods/suggestion-1',
output: 'somethingMaybeNotPromise',
},
{
messageId: 'no-single-promise-in-promise-methods/suggestion-2',
output: 'Promise.resolve(somethingMaybeNotPromise)',
},
],
},
],
},
],
});

0 comments on commit 80e3343

Please sign in to comment.