@@ -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