Skip to content

Commit

Permalink
feat: add suggestions to array-callback-return (#17590)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Tanujkanti4441 committed Sep 22, 2023
1 parent f9082ff commit 27d5a9e
Show file tree
Hide file tree
Showing 2 changed files with 320 additions and 37 deletions.
155 changes: 132 additions & 23 deletions lib/rules/array-callback-return.js
Expand Up @@ -136,6 +136,76 @@ function getArrayMethodName(node) {
return null;
}

/**
* Checks if the given node is a void expression.
* @param {ASTNode} node The node to check.
* @returns {boolean} - `true` if the node is a void expression
*/
function isExpressionVoid(node) {
return node.type === "UnaryExpression" && node.operator === "void";
}

/**
* Fixes the linting error by prepending "void " to the given node
* @param {Object} sourceCode context given by context.sourceCode
* @param {ASTNode} node The node to fix.
* @param {Object} fixer The fixer object provided by ESLint.
* @returns {Array<Object>} - An array of fix objects to apply to the node.
*/
function voidPrependFixer(sourceCode, node, fixer) {

const requiresParens =

// prepending `void ` will fail if the node has a lower precedence than void
astUtils.getPrecedence(node) < astUtils.getPrecedence({ type: "UnaryExpression", operator: "void" }) &&

// check if there are parentheses around the node to avoid redundant parentheses
!astUtils.isParenthesised(sourceCode, node);

// avoid parentheses issues
const returnOrArrowToken = sourceCode.getTokenBefore(
node,
node.parent.type === "ArrowFunctionExpression"
? astUtils.isArrowToken

// isReturnToken
: token => token.type === "Keyword" && token.value === "return"
);

const firstToken = sourceCode.getTokenAfter(returnOrArrowToken);

const prependSpace =

// is return token, as => allows void to be adjacent
returnOrArrowToken.value === "return" &&

// If two tokens (return and "(") are adjacent
returnOrArrowToken.range[1] === firstToken.range[0];

return [
fixer.insertTextBefore(firstToken, `${prependSpace ? " " : ""}void ${requiresParens ? "(" : ""}`),
fixer.insertTextAfter(node, requiresParens ? ")" : "")
];
}

/**
* Fixes the linting error by `wrapping {}` around the given node's body.
* @param {Object} sourceCode context given by context.sourceCode
* @param {ASTNode} node The node to fix.
* @param {Object} fixer The fixer object provided by ESLint.
* @returns {Array<Object>} - An array of fix objects to apply to the node.
*/
function curlyWrapFixer(sourceCode, node, fixer) {
const arrowToken = sourceCode.getTokenBefore(node.body, astUtils.isArrowToken);
const firstToken = sourceCode.getTokenAfter(arrowToken);
const lastToken = sourceCode.getLastToken(node);

return [
fixer.insertTextBefore(firstToken, "{"),
fixer.insertTextAfter(lastToken, "}")
];
}

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
Expand All @@ -151,6 +221,9 @@ module.exports = {
url: "https://eslint.org/docs/latest/rules/array-callback-return"
},

// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- false positive
hasSuggestions: true,

schema: [
{
type: "object",
Expand All @@ -176,7 +249,9 @@ module.exports = {
expectedAtEnd: "{{arrayMethodName}}() expects a value to be returned at the end of {{name}}.",
expectedInside: "{{arrayMethodName}}() expects a return value from {{name}}.",
expectedReturnValue: "{{arrayMethodName}}() expects a return value from {{name}}.",
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}."
expectedNoReturnValue: "{{arrayMethodName}}() expects no useless return value from {{name}}.",
wrapBraces: "Wrap the expression in `{}`.",
prependVoid: "Prepend `void` to the expression."
}
},

Expand Down Expand Up @@ -209,32 +284,56 @@ module.exports = {
return;
}

let messageId = null;
const messageAndSuggestions = { messageId: "", suggest: [] };

if (funcInfo.arrayMethodName === "forEach") {
if (options.checkForEach && node.type === "ArrowFunctionExpression" && node.expression) {
if (options.allowVoid &&
node.body.type === "UnaryExpression" &&
node.body.operator === "void") {
return;
}

messageId = "expectedNoReturnValue";
if (options.allowVoid) {
if (isExpressionVoid(node.body)) {
return;
}

messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [
{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(sourceCode, node, fixer);
}
},
{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(sourceCode, node.body, fixer);
}
}
];
} else {
messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [{
messageId: "wrapBraces",
fix(fixer) {
return curlyWrapFixer(sourceCode, node, fixer);
}
}];
}
}
} else {
if (node.body.type === "BlockStatement" && isAnySegmentReachable(funcInfo.currentSegments)) {
messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
messageAndSuggestions.messageId = funcInfo.hasReturn ? "expectedAtEnd" : "expectedInside";
}
}

if (messageId) {
if (messageAndSuggestions.messageId) {
const name = astUtils.getFunctionNameWithKind(node);

context.report({
node,
loc: astUtils.getFunctionHeadLoc(node, sourceCode),
messageId,
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) }
messageId: messageAndSuggestions.messageId,
data: { name, arrayMethodName: fullMethodName(funcInfo.arrayMethodName) },
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
});
}
}
Expand Down Expand Up @@ -295,36 +394,46 @@ module.exports = {

funcInfo.hasReturn = true;

let messageId = null;
const messageAndSuggestions = { messageId: "", suggest: [] };

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

// if checkForEach: true, returning a value at any path inside a forEach is not allowed
if (options.checkForEach && node.argument) {
if (options.allowVoid &&
node.argument.type === "UnaryExpression" &&
node.argument.operator === "void") {
return;
}

messageId = "expectedNoReturnValue";
if (options.allowVoid) {
if (isExpressionVoid(node.argument)) {
return;
}

messageAndSuggestions.messageId = "expectedNoReturnValue";
messageAndSuggestions.suggest = [{
messageId: "prependVoid",
fix(fixer) {
return voidPrependFixer(sourceCode, node.argument, fixer);
}
}];
} else {
messageAndSuggestions.messageId = "expectedNoReturnValue";
}
}
} else {

// if allowImplicit: false, should also check node.argument
if (!options.allowImplicit && !node.argument) {
messageId = "expectedReturnValue";
messageAndSuggestions.messageId = "expectedReturnValue";
}
}

if (messageId) {
if (messageAndSuggestions.messageId) {
context.report({
node,
messageId,
messageId: messageAndSuggestions.messageId,
data: {
name: astUtils.getFunctionNameWithKind(funcInfo.node),
arrayMethodName: fullMethodName(funcInfo.arrayMethodName)
}
},
suggest: messageAndSuggestions.suggest.length !== 0 ? messageAndSuggestions.suggest : null
});
}
},
Expand Down

0 comments on commit 27d5a9e

Please sign in to comment.