Skip to content

Commit 054436e

Browse files
cherryblossom000sindresorhusfisker
authored
Add no-useless-promise-resolve-reject rule (#1623)
Co-authored-by: Sindre Sorhus <[email protected]> Co-authored-by: fisker Cheung <[email protected]>
1 parent c318644 commit 054436e

File tree

5 files changed

+688
-0
lines changed

5 files changed

+688
-0
lines changed

configs/recommended.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ module.exports = {
4747
'unicorn/no-unused-properties': 'off',
4848
'unicorn/no-useless-fallback-in-spread': 'error',
4949
'unicorn/no-useless-length-check': 'error',
50+
'unicorn/no-useless-promise-resolve-reject': 'error',
5051
'unicorn/no-useless-spread': 'error',
5152
'unicorn/no-useless-undefined': 'error',
5253
'unicorn/no-zero-fractions': 'error',
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Disallow returning/yielding `Promise.resolve/reject()` in async functions
2+
3+
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
4+
5+
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
6+
7+
Wrapping a return value in `Promise.resolve` in an async function is unnecessary as all return values of an async function are already wrapped in a `Promise`. Similarly, returning an error wrapped in `Promise.reject` is equivalent to simply `throw`ing the error. This is the same for `yield`ing in async generators as well.
8+
9+
## Fail
10+
11+
```js
12+
const main = async foo => {
13+
if (foo > 4) {
14+
return Promise.reject(new Error('🤪'));
15+
}
16+
17+
return Promise.resolve(result);
18+
};
19+
20+
async function * generator() {
21+
yield Promise.resolve(result);
22+
yield Promise.reject(error);
23+
}
24+
```
25+
26+
## Pass
27+
28+
```js
29+
const main = async foo => {
30+
if (foo > 4) {
31+
throw new Error('🤪');
32+
}
33+
34+
return result;
35+
};
36+
37+
async function * generator() {
38+
yield result;
39+
throw error;
40+
}
41+
```

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ Configure it in `package.json`.
8181
"unicorn/no-unused-properties": "off",
8282
"unicorn/no-useless-fallback-in-spread": "error",
8383
"unicorn/no-useless-length-check": "error",
84+
"unicorn/no-useless-promise-resolve-reject": "error",
8485
"unicorn/no-useless-spread": "error",
8586
"unicorn/no-useless-undefined": "error",
8687
"unicorn/no-zero-fractions": "error",
@@ -195,6 +196,7 @@ Each rule has emojis denoting:
195196
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |
196197
| [no-useless-fallback-in-spread](docs/rules/no-useless-fallback-in-spread.md) | Forbid useless fallback when spreading in object literals. || 🔧 | |
197198
| [no-useless-length-check](docs/rules/no-useless-length-check.md) | Disallow useless array length check. || 🔧 | |
199+
| [no-useless-promise-resolve-reject](docs/rules/no-useless-promise-resolve-reject.md) | Disallow returning/yielding `Promise.resolve/reject()` in async functions || 🔧 | |
198200
| [no-useless-spread](docs/rules/no-useless-spread.md) | Disallow unnecessary spread. || 🔧 | |
199201
| [no-useless-undefined](docs/rules/no-useless-undefined.md) | Disallow useless `undefined`. || 🔧 | |
200202
| [no-zero-fractions](docs/rules/no-zero-fractions.md) | Disallow number literals with zero fractions or dangling dots. || 🔧 | |
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
'use strict';
2+
const {matches, methodCallSelector} = require('./selectors/index.js');
3+
const {getParenthesizedRange} = require('./utils/parentheses.js');
4+
5+
const MESSAGE_ID_RESOLVE = 'resolve';
6+
const MESSAGE_ID_REJECT = 'reject';
7+
const messages = {
8+
[MESSAGE_ID_RESOLVE]: 'Prefer `{{type}} value` over `{{type}} Promise.resolve(error)`.',
9+
[MESSAGE_ID_REJECT]: 'Prefer `throw error` over `{{type}} Promise.reject(error)`.',
10+
};
11+
12+
const selector = [
13+
methodCallSelector({
14+
object: 'Promise',
15+
methods: ['resolve', 'reject'],
16+
}),
17+
matches([
18+
'ArrowFunctionExpression[async=true] > .body',
19+
'ReturnStatement > .argument',
20+
'YieldExpression[delegate=false] > .argument',
21+
]),
22+
].join('');
23+
24+
const functionTypes = new Set([
25+
'ArrowFunctionExpression',
26+
'FunctionDeclaration',
27+
'FunctionExpression',
28+
]);
29+
function getFunctionNode(node) {
30+
let isInTryStatement = false;
31+
let functionNode;
32+
for (; node; node = node.parent) {
33+
if (functionTypes.has(node.type)) {
34+
functionNode = node;
35+
break;
36+
}
37+
38+
if (node.type === 'TryStatement') {
39+
isInTryStatement = true;
40+
}
41+
}
42+
43+
return {
44+
functionNode,
45+
isInTryStatement,
46+
};
47+
}
48+
49+
function createProblem(callExpression, fix) {
50+
const {callee, parent} = callExpression;
51+
const method = callee.property.name;
52+
const type = parent.type === 'YieldExpression' ? 'yield' : 'return';
53+
54+
return {
55+
node: callee,
56+
messageId: method,
57+
data: {type},
58+
fix,
59+
};
60+
}
61+
62+
function fix(callExpression, isInTryStatement, sourceCode) {
63+
if (callExpression.arguments.length > 1) {
64+
return;
65+
}
66+
67+
const {callee, parent, arguments: [errorOrValue]} = callExpression;
68+
if (errorOrValue && errorOrValue.type === 'SpreadElement') {
69+
return;
70+
}
71+
72+
const isReject = callee.property.name === 'reject';
73+
const isYieldExpression = parent.type === 'YieldExpression';
74+
if (
75+
isReject
76+
&& (
77+
isInTryStatement
78+
|| (isYieldExpression && parent.parent.type !== 'ExpressionStatement')
79+
)
80+
) {
81+
return;
82+
}
83+
84+
return function (fixer) {
85+
const isArrowFunctionBody = parent.type === 'ArrowFunctionExpression';
86+
87+
let text = errorOrValue ? sourceCode.getText(errorOrValue) : '';
88+
89+
if (errorOrValue && errorOrValue.type === 'SequenceExpression') {
90+
text = `(${text})`;
91+
}
92+
93+
if (isReject) {
94+
// `return Promise.reject()` -> `throw undefined`
95+
text = text || 'undefined';
96+
text = `throw ${text}`;
97+
98+
if (isYieldExpression) {
99+
return fixer.replaceTextRange(
100+
getParenthesizedRange(parent, sourceCode),
101+
text,
102+
);
103+
}
104+
105+
text += ';';
106+
107+
// `=> Promise.reject(error)` -> `=> { throw error; }`
108+
if (isArrowFunctionBody) {
109+
text = `{ ${text} }`;
110+
return fixer.replaceTextRange(
111+
getParenthesizedRange(callExpression, sourceCode),
112+
text,
113+
);
114+
}
115+
} else {
116+
// eslint-disable-next-line no-lonely-if
117+
if (isYieldExpression) {
118+
text = `yield${text ? ' ' : ''}${text}`;
119+
} else if (parent.type === 'ReturnStatement') {
120+
text = `return${text ? ' ' : ''}${text};`;
121+
} else {
122+
if (errorOrValue && errorOrValue.type === 'ObjectExpression') {
123+
text = `(${text})`;
124+
}
125+
126+
// `=> Promise.resolve()` -> `=> {}`
127+
text = text || '{}';
128+
}
129+
}
130+
131+
return fixer.replaceText(
132+
isArrowFunctionBody ? callExpression : parent,
133+
text,
134+
);
135+
};
136+
}
137+
138+
/** @param {import('eslint').Rule.RuleContext} context */
139+
const create = context => {
140+
const sourceCode = context.getSourceCode();
141+
142+
return {
143+
[selector](callExpression) {
144+
const {functionNode, isInTryStatement} = getFunctionNode(callExpression);
145+
if (!functionNode || !functionNode.async) {
146+
return;
147+
}
148+
149+
return createProblem(
150+
callExpression,
151+
fix(callExpression, isInTryStatement, sourceCode),
152+
);
153+
},
154+
};
155+
};
156+
157+
const schema = [];
158+
159+
/** @type {import('eslint').Rule.RuleModule} */
160+
module.exports = {
161+
create,
162+
meta: {
163+
type: 'suggestion',
164+
docs: {
165+
description: 'Disallow returning/yielding `Promise.resolve/reject()` in async functions',
166+
},
167+
fixable: 'code',
168+
schema,
169+
messages,
170+
},
171+
};

0 commit comments

Comments
 (0)