Skip to content

Commit 27d5a9e

Browse files
feat: add suggestions to array-callback-return (#17590)
* feat: add suggestions to array-callback-return * fix: spacing errors * fix: spacing errors * feat: change meta type to problem again * feat: disable eslint-plugin/require-meta-has-suggestions * feat: update tests
1 parent f9082ff commit 27d5a9e

2 files changed

Lines changed: 320 additions & 37 deletions

File tree

lib/rules/array-callback-return.js

Lines changed: 132 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,76 @@ function getArrayMethodName(node) {
136136
return null;
137137
}
138138

139+
/**
140+
* Checks if the given node is a void expression.
141+
* @param {ASTNode} node The node to check.
142+
* @returns {boolean} - `true` if the node is a void expression
143+
*/
144+
function isExpressionVoid(node) {
145+
return node.type === "UnaryExpression" && node.operator === "void";
146+
}
147+
148+
/**
149+
* Fixes the linting error by prepending "void " to the given node
150+
* @param {Object} sourceCode context given by context.sourceCode
151+
* @param {ASTNode} node The node to fix.
152+
* @param {Object} fixer The fixer object provided by ESLint.
153+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
154+
*/
155+
function voidPrependFixer(sourceCode, node, fixer) {
156+
157+
const requiresParens =
158+
159+
// prepending `void ` will fail if the node has a lower precedence than void
160+
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&
161+
162+
// check if there are parentheses around the node to avoid redundant parentheses
163+
!astUtils.isParenthesised(sourceCode, node);
164+
165+
// avoid parentheses issues
166+
const returnOrArrowToken = sourceCode.getTokenBefore(
167+
node,
168+
node.parent.type === "ArrowFunctionExpression"
169+
? astUtils.isArrowToken
170+
171+
// isReturnToken
172+
: token => token.type === "Keyword" && token.value === "return"
173+
);
174+
175+
const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);
176+
177+
const prependSpace =
178+
179+
// is return token, as => allows void to be adjacent
180+
returnOrArrowToken.value === "return" &&
181+
182+
// If two tokens (return and "(") are adjacent
183+
returnOrArrowToken.range[1] === firstToken.range[0];
184+
185+
return [
186+
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
187+
fixer.insertTextAfter(node, requiresParens ? ")" : "")
188+
];
189+
}
190+
191+
/**
192+
* Fixes the linting error by `wrapping {}` around the given node's body.
193+
* @param {Object} sourceCode context given by context.sourceCode
194+
* @param {ASTNode} node The node to fix.
195+
* @param {Object} fixer The fixer object provided by ESLint.
196+
* @returns {Array<Object>} - An array of fix objects to apply to the node.
197+
*/
198+
function curlyWrapFixer(sourceCode, node, fixer) {
199+
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
200+
const firstToken = sourceCode.getTokenAfter(arrowToken);
201+
const lastToken = sourceCode.getLastToken(node);
202+
203+
return [
204+
fixer.insertTextBefore(firstToken, "{"),
205+
fixer.insertTextAfter(lastToken, "}")
206+
];
207+
}
208+
139209
//------------------------------------------------------------------------------
140210
// Rule Definition
141211
//------------------------------------------------------------------------------
@@ -151,6 +221,9 @@ module.exports = {
151221
url: "https://eslint.org/docs/latest/rules/array-callback-return"
152222
},
153223

224+
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive
225+
hasSuggestions: true,
226+
154227
schema: [
155228
{
156229
type: "object",
@@ -176,7 +249,9 @@ module.exports = {
176249
expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
177250
expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
178251
expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
179-
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}."
252+
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.",
253+
wrapBraces: "Wrap the expression in `{}`.",
254+
prependVoid: "Prepend `void` to the expression."
180255
}
181256
},
182257

@@ -209,32 +284,56 @@ module.exports = {
209284
return;
210285
}
211286

212-
let messageId = null;
287+
const messageAndSuggestions = { messageId: "", suggest: [] };
213288

214289
if (funcInfo.arrayMethodName === "forEach") {
215290
if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
216-
if (options.allowVoid &&
217-
node.body.type === "UnaryExpression" &&
218-
node.body.operator === "void") {
219-
return;
220-
}
221291

222-
messageId = "expectedNoReturnValue";
292+
if (options.allowVoid) {
293+
if (isExpressionVoid(node.body)) {
294+
return;
295+
}
296+
297+
messageAndSuggestions.messageId = "expectedNoReturnValue";
298+
messageAndSuggestions.suggest = [
299+
{
300+
messageId: "wrapBraces",
301+
fix(fixer) {
302+
return curlyWrapFixer(sourceCode, node, fixer);
303+
}
304+
},
305+
{
306+
messageId: "prependVoid",
307+
fix(fixer) {
308+
return voidPrependFixer(sourceCode, node.body, fixer);
309+
}
310+
}
311+
];
312+
} else {
313+
messageAndSuggestions.messageId = "expectedNoReturnValue";
314+
messageAndSuggestions.suggest = [{
315+
messageId: "wrapBraces",
316+
fix(fixer) {
317+
return curlyWrapFixer(sourceCode, node, fixer);
318+
}
319+
}];
320+
}
223321
}
224322
} else {
225323
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
226-
messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
324+
messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
227325
}
228326
}
229327

230-
if (messageId) {
328+
if (messageAndSuggestions.messageId) {
231329
const name = astUtils.getFunctionNameWithKind(node);
232330

233331
context.report({
234332
node,
235333
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
236-
messageId,
237-
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }
334+
messageId: messageAndSuggestions.messageId,
335+
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) },
336+
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
238337
});
239338
}
240339
}
@@ -295,36 +394,46 @@ module.exports = {
295394

296395
funcInfo.hasReturn = true;
297396

298-
let messageId = null;
397+
const messageAndSuggestions = { messageId: "", suggest: [] };
299398

300399
if (funcInfo.arrayMethodName === "forEach") {
301400

302401
// if checkForEach: true, returning a value at any path inside a forEach is not allowed
303402
if (options.checkForEach && node.argument) {
304-
if (options.allowVoid &&
305-
node.argument.type === "UnaryExpression" &&
306-
node.argument.operator === "void") {
307-
return;
308-
}
309403

310-
messageId = "expectedNoReturnValue";
404+
if (options.allowVoid) {
405+
if (isExpressionVoid(node.argument)) {
406+
return;
407+
}
408+
409+
messageAndSuggestions.messageId = "expectedNoReturnValue";
410+
messageAndSuggestions.suggest = [{
411+
messageId: "prependVoid",
412+
fix(fixer) {
413+
return voidPrependFixer(sourceCode, node.argument, fixer);
414+
}
415+
}];
416+
} else {
417+
messageAndSuggestions.messageId = "expectedNoReturnValue";
418+
}
311419
}
312420
} else {
313421

314422
// if allowImplicit: false, should also check node.argument
315423
if (!options.allowImplicit && !node.argument) {
316-
messageId = "expectedReturnValue";
424+
messageAndSuggestions.messageId = "expectedReturnValue";
317425
}
318426
}
319427

320-
if (messageId) {
428+
if (messageAndSuggestions.messageId) {
321429
context.report({
322430
node,
323-
messageId,
431+
messageId: messageAndSuggestions.messageId,
324432
data: {
325433
name: astUtils.getFunctionNameWithKind(funcInfo.node),
326434
arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
327-
}
435+
},
436+
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
328437
});
329438
}
330439
},

0 commit comments

Comments
 (0)