Skip to content

Commit ee2f718

Browse files
feat: Allow void in rule no-promise-executor-return (#17282)
* feat: Allow `void` in `no-promise-executor-return` (#17278) * feat: Autofix and suggestions for `no-promise-executor-return` * fix: update behavior for `no-promise-executor-return` * fix: update suggestion behavior * docs: update to match code * docs: misc fixes * fix: refactors * fix: parentheses issues * Update docs/src/rules/no-promise-executor-return.md Co-authored-by: Milos Djermanovic <[email protected]> * Update docs/src/rules/no-promise-executor-return.md Co-authored-by: Milos Djermanovic <[email protected]> * Update lib/rules/no-promise-executor-return.js Co-authored-by: Milos Djermanovic <[email protected]> * fix: adjacent token issue * fix comments --------- Co-authored-by: Milos Djermanovic <[email protected]>
1 parent 9e9edf9 commit ee2f718

File tree

3 files changed

+518
-50
lines changed

3 files changed

+518
-50
lines changed

docs/src/rules/no-promise-executor-return.md

+47
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ new Promise((resolve, reject) => getSomething((err, data) => {
6363
new Promise(() => {
6464
return 1;
6565
});
66+
67+
new Promise(r => r(1));
6668
```
6769

6870
:::
@@ -74,6 +76,7 @@ Examples of **correct** code for this rule:
7476
```js
7577
/*eslint no-promise-executor-return: "error"*/
7678

79+
// Turn return inline into two lines
7780
new Promise((resolve, reject) => {
7881
if (someCondition) {
7982
resolve(defaultResult);
@@ -88,6 +91,7 @@ new Promise((resolve, reject) => {
8891
});
8992
});
9093

94+
// Add curly braces
9195
new Promise((resolve, reject) => {
9296
getSomething((err, data) => {
9397
if (err) {
@@ -98,7 +102,50 @@ new Promise((resolve, reject) => {
98102
});
99103
});
100104

105+
new Promise(r => { r(1) });
106+
// or just use Promise.resolve
101107
Promise.resolve(1);
102108
```
103109

104110
:::
111+
112+
## Options
113+
114+
This rule takes one option, an object, with the following properties:
115+
116+
* `allowVoid`: If set to `true` (`false` by default), this rule will allow returning void values.
117+
118+
### allowVoid
119+
120+
Examples of **correct** code for this rule with the `{ "allowVoid": true }` option:
121+
122+
::: correct
123+
124+
```js
125+
/*eslint no-promise-executor-return: ["error", { allowVoid: true }]*/
126+
127+
new Promise((resolve, reject) => {
128+
if (someCondition) {
129+
return void resolve(defaultResult);
130+
}
131+
getSomething((err, result) => {
132+
if (err) {
133+
reject(err);
134+
} else {
135+
resolve(result);
136+
}
137+
});
138+
});
139+
140+
new Promise((resolve, reject) => void getSomething((err, data) => {
141+
if (err) {
142+
reject(err);
143+
} else {
144+
resolve(data);
145+
}
146+
}));
147+
148+
new Promise(r => void r(1));
149+
```
150+
151+
:::

lib/rules/no-promise-executor-return.js

+154-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//------------------------------------------------------------------------------
1111

1212
const { findVariable } = require("@eslint-community/eslint-utils");
13+
const astUtils = require("./utils/ast-utils");
1314

1415
//------------------------------------------------------------------------------
1516
// Helpers
@@ -59,6 +60,78 @@ function isPromiseExecutor(node, scope) {
5960
isGlobalReference(parent.callee, getOuterScope(scope));
6061
}
6162

63+
/**
64+
* Checks if the given node is a void expression.
65+
* @param {ASTNode} node The node to check.
66+
* @returns {boolean} - `true` if the node is a void expression
67+
*/
68+
function expressionIsVoid(node) {
69+
return node.type === "UnaryExpression" && node.operator === "void";
70+
}
71+
72+
/**
73+
* Fixes the linting error by prepending "void " to the given node
74+
* @param {Object} sourceCode context given by context.sourceCode
75+
* @param {ASTNode} node The node to fix.
76+
* @param {Object} fixer The fixer object provided by ESLint.
77+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
78+
*/
79+
function voidPrependFixer(sourceCode, node, fixer) {
80+
81+
const requiresParens =
82+
83+
// prepending `void ` will fail if the node has a lower precedence than void
84+
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
85+
86+
// check if there are parentheses around the node to avoid redundant parentheses
87+
!astUtils.isParenthesised(sourceCode, node);
88+
89+
// avoid parentheses issues
90+
const returnOrArrowToken = sourceCode.getTokenBefore(
91+
node,
92+
node.parent.type === "ArrowFunctionExpression"
93+
? astUtils.isArrowToken
94+
95+
// isReturnToken
96+
: token => token.type === "Keyword" && token.value === "return"
97+
);
98+
99+
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
100+
101+
const prependSpace =
102+
103+
// is return token, as => allows void to be adjacent
104+
returnOrArrowToken.value === "return" &&
105+
106+
// If two tokens (return and "(") are adjacent
107+
returnOrArrowToken.range[1] === firstToken.range[0];
108+
109+
return [
110+
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
111+
fixer.insertTextAfter(node, requiresParens ? ")" : "")
112+
];
113+
}
114+
115+
/**
116+
* Fixes the linting error by `wrapping {}` around the given node's body.
117+
* @param {Object} sourceCode context given by context.sourceCode
118+
* @param {ASTNode} node The node to fix.
119+
* @param {Object} fixer The fixer object provided by ESLint.
120+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
121+
*/
122+
function curlyWrapFixer(sourceCode, node, fixer) {
123+
124+
// https://github.com/eslint/eslint/pull/17282#issuecomment-1592795923
125+
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
126+
const firstToken = sourceCode.getTokenAfter(arrowToken);
127+
const lastToken = sourceCode.getLastToken(node);
128+
129+
return [
130+
fixer.insertTextBefore(firstToken, "{"),
131+
fixer.insertTextAfter(lastToken, "}")
132+
];
133+
}
134+
62135
//------------------------------------------------------------------------------
63136
// Rule Definition
64137
//------------------------------------------------------------------------------
@@ -74,37 +147,80 @@ module.exports = {
74147
url: "https://eslint.org/docs/latest/rules/no-promise-executor-return"
75148
},
76149

77-
schema: [],
150+
hasSuggestions: true,
151+
152+
schema: [{
153+
type: "object",
154+
properties: {
155+
allowVoid: {
156+
type: "boolean",
157+
default: false
158+
}
159+
},
160+
additionalProperties: false
161+
}],
78162

79163
messages: {
80-
returnsValue: "Return values from promise executor functions cannot be read."
164+
returnsValue: "Return values from promise executor functions cannot be read.",
165+
166+
// arrow and function suggestions
167+
prependVoid: "Prepend `void` to the expression.",
168+
169+
// only arrow suggestions
170+
wrapBraces: "Wrap the expression in `{}`."
81171
}
82172
},
83173

84174
create(context) {
85175

86176
let funcInfo = null;
87177
const sourceCode = context.sourceCode;
88-
89-
/**
90-
* Reports the given node.
91-
* @param {ASTNode} node Node to report.
92-
* @returns {void}
93-
*/
94-
function report(node) {
95-
context.report({ node, messageId: "returnsValue" });
96-
}
178+
const {
179+
allowVoid = false
180+
} = context.options[0] || {};
97181

98182
return {
99183

100184
onCodePathStart(_, node) {
101185
funcInfo = {
102186
upper: funcInfo,
103-
shouldCheck: functionTypesToCheck.has(node.type) && isPromiseExecutor(node, sourceCode.getScope(node))
187+
shouldCheck:
188+
functionTypesToCheck.has(node.type) &&
189+
isPromiseExecutor(node, sourceCode.getScope(node))
104190
};
105191

106-
if (funcInfo.shouldCheck && node.type === "ArrowFunctionExpression" && node.expression) {
107-
report(node.body);
192+
if (// Is a Promise executor
193+
funcInfo.shouldCheck &&
194+
node.type === "ArrowFunctionExpression" &&
195+
node.expression &&
196+
197+
// Except void
198+
!(allowVoid && expressionIsVoid(node.body))
199+
) {
200+
const suggest = [];
201+
202+
// prevent useless refactors
203+
if (allowVoid) {
204+
suggest.push({
205+
messageId: "prependVoid",
206+
fix(fixer) {
207+
return voidPrependFixer(sourceCode, node.body, fixer);
208+
}
209+
});
210+
}
211+
212+
suggest.push({
213+
messageId: "wrapBraces",
214+
fix(fixer) {
215+
return curlyWrapFixer(sourceCode, node, fixer);
216+
}
217+
});
218+
219+
context.report({
220+
node: node.body,
221+
messageId: "returnsValue",
222+
suggest
223+
});
108224
}
109225
},
110226

@@ -113,9 +229,31 @@ module.exports = {
113229
},
114230

115231
ReturnStatement(node) {
116-
if (funcInfo.shouldCheck && node.argument) {
117-
report(node);
232+
if (!(funcInfo.shouldCheck && node.argument)) {
233+
return;
118234
}
235+
236+
// node is `return <expression>`
237+
if (!allowVoid) {
238+
context.report({ node, messageId: "returnsValue" });
239+
return;
240+
}
241+
242+
if (expressionIsVoid(node.argument)) {
243+
return;
244+
}
245+
246+
// allowVoid && !expressionIsVoid
247+
context.report({
248+
node,
249+
messageId: "returnsValue",
250+
suggest: [{
251+
messageId: "prependVoid",
252+
fix(fixer) {
253+
return voidPrependFixer(sourceCode, node.argument, fixer);
254+
}
255+
}]
256+
});
119257
}
120258
};
121259
}

0 commit comments

Comments
 (0)