Skip to content

Commit f6215f3

Browse files
Detect unnecessary Promise.resolve/reject in promise callback functions (#1666)
Co-authored-by: fisker Cheung <[email protected]>
1 parent 267115a commit f6215f3

File tree

4 files changed

+126
-7
lines changed

4 files changed

+126
-7
lines changed

docs/rules/no-useless-promise-resolve-reject.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Disallow returning/yielding `Promise.resolve/reject()` in async functions
1+
# Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks
22

33
*This rule is part of the [recommended](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config) config.*
44

55
🔧 *This rule is [auto-fixable](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems).*
66

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.
7+
Wrapping a return value in `Promise.resolve` in an async function or a `Promise#then`/`catch`/`finally` callback is unnecessary as all return values in async functions and promise callback functions 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.
88

99
## Fail
1010

@@ -21,6 +21,18 @@ async function * generator() {
2121
yield Promise.resolve(result);
2222
yield Promise.reject(error);
2323
}
24+
25+
promise
26+
.then(x => {
27+
if (x % 2 == 0) {
28+
return Promise.resolve(x / 2);
29+
}
30+
31+
return Promise.reject(new Error('odd number'));
32+
});
33+
.catch(error => Promise.reject(new FancyError(error)));
34+
35+
promise.finally(() => Promise.reject(new Error('oh no')));
2436
```
2537

2638
## Pass
@@ -38,4 +50,20 @@ async function * generator() {
3850
yield result;
3951
throw error;
4052
}
53+
54+
promise
55+
.then(x => {
56+
if (x % 2 == 0) {
57+
return x / 2;
58+
}
59+
60+
throw new Error('odd number');
61+
});
62+
.catch(error => {
63+
throw new FancyError(error);
64+
});
65+
66+
promise.finally(() => {
67+
throw new Error('oh no');
68+
});
4169
```

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Each rule has emojis denoting:
196196
| [no-unused-properties](docs/rules/no-unused-properties.md) | Disallow unused object properties. | | | |
197197
| [no-useless-fallback-in-spread](docs/rules/no-useless-fallback-in-spread.md) | Forbid useless fallback when spreading in object literals. || 🔧 | |
198198
| [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 || 🔧 | |
199+
| [no-useless-promise-resolve-reject](docs/rules/no-useless-promise-resolve-reject.md) | Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks || 🔧 | |
200200
| [no-useless-spread](docs/rules/no-useless-spread.md) | Disallow unnecessary spread. || 🔧 | |
201201
| [no-useless-undefined](docs/rules/no-useless-undefined.md) | Disallow useless `undefined`. || 🔧 | |
202202
| [no-zero-fractions](docs/rules/no-zero-fractions.md) | Disallow number literals with zero fractions or dangling dots. || 🔧 | |

rules/no-useless-promise-resolve-reject.js

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const selector = [
1515
methods: ['resolve', 'reject'],
1616
}),
1717
matches([
18-
'ArrowFunctionExpression[async=true] > .body',
18+
'ArrowFunctionExpression > .body',
1919
'ReturnStatement > .argument',
20-
'YieldExpression[delegate=false] > .argument',
20+
'YieldExpression[delegate!=true] > .argument',
2121
]),
2222
].join('');
2323

@@ -46,6 +46,42 @@ function getFunctionNode(node) {
4646
};
4747
}
4848

49+
function isPromiseCallback(node) {
50+
if (
51+
node.parent.type === 'CallExpression'
52+
&& node.parent.callee.type === 'MemberExpression'
53+
&& !node.parent.callee.computed
54+
&& node.parent.callee.property.type === 'Identifier'
55+
) {
56+
const {callee: {property}, arguments: arguments_} = node.parent;
57+
58+
if (
59+
arguments_.length === 1
60+
&& (
61+
property.name === 'then'
62+
|| property.name === 'catch'
63+
|| property.name === 'finally'
64+
)
65+
&& arguments_[0] === node
66+
) {
67+
return true;
68+
}
69+
70+
if (
71+
arguments_.length === 2
72+
&& property.name === 'then'
73+
&& (
74+
arguments_[0] === node
75+
|| (arguments_[0].type !== 'SpreadElement' && arguments_[1] === node)
76+
)
77+
) {
78+
return true;
79+
}
80+
}
81+
82+
return false;
83+
}
84+
4985
function createProblem(callExpression, fix) {
5086
const {callee, parent} = callExpression;
5187
const method = callee.property.name;
@@ -142,7 +178,7 @@ const create = context => {
142178
return {
143179
[selector](callExpression) {
144180
const {functionNode, isInTryStatement} = getFunctionNode(callExpression);
145-
if (!functionNode || !functionNode.async) {
181+
if (!functionNode || !(functionNode.async || isPromiseCallback(functionNode))) {
146182
return;
147183
}
148184

@@ -162,7 +198,7 @@ module.exports = {
162198
meta: {
163199
type: 'suggestion',
164200
docs: {
165-
description: 'Disallow returning/yielding `Promise.resolve/reject()` in async functions',
201+
description: 'Disallow returning/yielding `Promise.resolve/reject()` in async functions or promise callbacks',
166202
},
167203
fixable: 'code',
168204
schema,

test/no-useless-promise-resolve-reject.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ test({
7777
yield* Promise.${fn}(bar);
7878
}
7979
`),
80+
// Promise#then/catch/finally
81+
'promise.then(() => foo).catch(() => bar).finally(() => baz)',
82+
'promise.then(() => foo, () => bar).finally(() => baz)',
83+
'promise.then(x, y, () => Promise.resolve(foo))',
84+
'promise.catch(x, () => Promise.resolve(foo))',
85+
'promise.finally(x, () => Promise.resolve(foo))',
86+
'promise[then](() => Promise.resolve(foo))',
8087
],
8188
invalid: [
8289
{
@@ -469,5 +476,53 @@ test({
469476
`,
470477
errors: [yieldRejectError],
471478
})),
479+
// Promise#then/catch/finally callbacks returning Promise.resolve/reject
480+
...['then', 'catch', 'finally'].flatMap(fn => [
481+
{
482+
code: `promise.${fn}(() => Promise.resolve(bar))`,
483+
errors: [returnResolveError],
484+
output: `promise.${fn}(() => bar)`,
485+
},
486+
{
487+
code: `promise.${fn}(() => { return Promise.resolve(bar); })`,
488+
errors: [returnResolveError],
489+
output: `promise.${fn}(() => { return bar; })`,
490+
},
491+
{
492+
code: `promise.${fn}(async () => Promise.reject(bar))`,
493+
errors: [returnRejectError],
494+
output: `promise.${fn}(async () => { throw bar; })`,
495+
},
496+
{
497+
code: `promise.${fn}(async () => { return Promise.reject(bar); })`,
498+
errors: [returnRejectError],
499+
output: `promise.${fn}(async () => { throw bar; })`,
500+
},
501+
]),
502+
{
503+
code: 'promise.then(() => {}, () => Promise.resolve(bar))',
504+
errors: [returnResolveError],
505+
output: 'promise.then(() => {}, () => bar)',
506+
},
507+
{
508+
code: 'promise.then(() => Promise.resolve(bar), () => Promise.resolve(baz))',
509+
errors: [returnResolveError, returnResolveError],
510+
output: 'promise.then(() => bar, () => baz)',
511+
},
512+
{
513+
code: 'promise.then(() => {}, () => { return Promise.resolve(bar); })',
514+
errors: [returnResolveError],
515+
output: 'promise.then(() => {}, () => { return bar; })',
516+
},
517+
{
518+
code: 'promise.then(() => {}, async () => Promise.reject(bar))',
519+
errors: [returnRejectError],
520+
output: 'promise.then(() => {}, async () => { throw bar; })',
521+
},
522+
{
523+
code: 'promise.then(() => {}, async () => { return Promise.reject(bar); })',
524+
errors: [returnRejectError],
525+
output: 'promise.then(() => {}, async () => { throw bar; })',
526+
},
472527
],
473528
});

0 commit comments

Comments
 (0)