Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

no-array-callback-reference: Check logical expressions, Check ternaries deeply #2289

Merged
merged 5 commits into from
Feb 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 71 additions & 42 deletions rules/no-array-callback-reference.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
'use strict';
const {isParenthesized} = require('@eslint-community/eslint-utils');
const {isMethodCall} = require('./ast/index.js');
const {isNodeMatches, isNodeValueNotFunction} = require('./utils/index.js');
const {
isNodeMatches,
isNodeValueNotFunction,
isParenthesized,
getParenthesizedRange,
getParenthesizedText,
shouldAddParenthesesToCallExpressionCallee,
} = require('./utils/index.js');

const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
Expand All @@ -25,7 +31,7 @@ const iteratorMethods = new Map([
},
{
method: 'filter',
test: node => !(node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
ignore: [
'Boolean',
],
Expand Down Expand Up @@ -63,7 +69,7 @@ const iteratorMethods = new Map([
},
{
method: 'map',
test: node => !(node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
ignore: [
'String',
'Number',
Expand Down Expand Up @@ -104,35 +110,39 @@ const iteratorMethods = new Map([
ignore = [],
minParameters = 1,
returnsUndefined = false,
test,
shouldIgnoreCallExpression,
}) => [method, {
minParameters,
parameters,
returnsUndefined,
test(node) {
shouldIgnoreCallExpression(callExpression) {
if (
method !== 'reduce'
&& method !== 'reduceRight'
&& isAwaitExpressionArgument(node)
&& isAwaitExpressionArgument(callExpression)
) {
return false;
return true;
}

if (isNodeMatches(node.callee.object, ignoredCallee)) {
return false;
if (isNodeMatches(callExpression.callee.object, ignoredCallee)) {
return true;
}

if (node.callee.object.type === 'CallExpression' && isNodeMatches(node.callee.object.callee, ignoredCallee)) {
return false;
if (
callExpression.callee.object.type === 'CallExpression'
&& isNodeMatches(callExpression.callee.object.callee, ignoredCallee)
) {
return true;
}

const [callback] = node.arguments;

return shouldIgnoreCallExpression?.(callExpression) ?? false;
},
shouldIgnoreCallback(callback) {
if (callback.type === 'Identifier' && ignore.includes(callback.name)) {
return false;
return true;
}

return !test || test(node);
return false;
},
}]));

Expand Down Expand Up @@ -163,9 +173,14 @@ function getProblem(context, node, method, options) {
name,
method,
},
suggest: [],
};

if (node.type === 'YieldExpression' || node.type === 'AwaitExpression') {
return problem;
}

problem.suggest = [];

const {parameters, minParameters, returnsUndefined} = options;
for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
Expand All @@ -178,16 +193,20 @@ function getProblem(context, node, method, options) {
},
fix(fixer) {
const {sourceCode} = context;
let nodeText = sourceCode.getText(node);
if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') {
nodeText = `(${nodeText})`;
let text = getParenthesizedText(node, sourceCode);

if (
!isParenthesized(node, sourceCode)
&& shouldAddParenthesesToCallExpressionCallee(node)
) {
text = `(${text})`;
}

return fixer.replaceText(
node,
return fixer.replaceTextRange(
getParenthesizedRange(node, sourceCode),
returnsUndefined
? `(${suggestionParameters}) => { ${nodeText}(${suggestionParameters}); }`
: `(${suggestionParameters}) => ${nodeText}(${suggestionParameters})`,
? `(${suggestionParameters}) => { ${text}(${suggestionParameters}); }`
: `(${suggestionParameters}) => ${text}(${suggestionParameters})`,
);
},
};
Expand All @@ -198,47 +217,57 @@ function getProblem(context, node, method, options) {
return problem;
}

function * getTernaryConsequentAndALternate(node) {
if (node.type === 'ConditionalExpression') {
yield * getTernaryConsequentAndALternate(node.consequent);
yield * getTernaryConsequentAndALternate(node.alternate);
return;
}

yield node;
}

/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
CallExpression(node) {
* CallExpression(callExpression) {
if (
!isMethodCall(node, {
!isMethodCall(callExpression, {
minimumArguments: 1,
maximumArguments: 2,
optionalCall: false,
optionalMember: false,
computed: false,
})
|| node.callee.property.type !== 'Identifier'
|| callExpression.callee.property.type !== 'Identifier'
) {
return;
}

const methodNode = node.callee.property;
const methodNode = callExpression.callee.property;
const methodName = methodNode.name;
if (!iteratorMethods.has(methodName)) {
return;
}

const [callback] = node.arguments;

if (
callback.type === 'FunctionExpression'
|| callback.type === 'ArrowFunctionExpression'
// Ignore all `CallExpression`s include `function.bind()`
|| callback.type === 'CallExpression'
|| isNodeValueNotFunction(callback)
) {
const options = iteratorMethods.get(methodName);
if (options.shouldIgnoreCallExpression(callExpression)) {
return;
}

const options = iteratorMethods.get(methodName);
for (const callback of getTernaryConsequentAndALternate(callExpression.arguments[0])) {
if (
callback.type === 'FunctionExpression'
|| callback.type === 'ArrowFunctionExpression'
// Ignore all `CallExpression`s include `function.bind()`
|| callback.type === 'CallExpression'
|| options.shouldIgnoreCallback(callback)
|| isNodeValueNotFunction(callback)
) {
continue;
}

if (!options.test(node)) {
return;
yield getProblem(context, callback, methodName, options);
}

return getProblem(context, callback, methodName, options);
},
});

Expand Down
1 change: 1 addition & 0 deletions rules/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ module.exports = {
isValueNotUsable: require('./is-value-not-usable.js'),
needsSemicolon: require('./needs-semicolon.js'),
shouldAddParenthesesToMemberExpressionObject: require('./should-add-parentheses-to-member-expression-object.js'),
shouldAddParenthesesToCallExpressionCallee: require('./should-add-parentheses-to-call-expression-callee.js'),
shouldAddParenthesesToAwaitExpressionArgument: require('./should-add-parentheses-to-await-expression-argument.js'),
singular: require('./singular.js'),
toLocation: require('./to-location.js'),
Expand Down
1 change: 0 additions & 1 deletion rules/utils/is-node-value-not-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const impossibleNodeTypes = new Set([
const mostLikelyNotNodeTypes = new Set([
'AssignmentExpression',
'AwaitExpression',
'LogicalExpression',
'NewExpression',
'TaggedTemplateExpression',
'ThisExpression',
Expand Down
22 changes: 22 additions & 0 deletions rules/utils/should-add-parentheses-to-call-expression-callee.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

/**
Check if parentheses should be added to a `node` when it's used as `callee` of `CallExpression`.

@param {Node} node - The AST node to check.
@returns {boolean}
*/
function shouldAddParenthesesToCallExpressionCallee(node) {
return node.type === 'SequenceExpression'
|| node.type === 'YieldExpression'
|| node.type === 'ArrowFunctionExpression'
|| node.type === 'ConditionalExpression'
|| node.type === 'AssignmentExpression'
|| node.type === 'LogicalExpression'
|| node.type === 'BinaryExpression'
|| node.type === 'UnaryExpression'
|| node.type === 'UpdateExpression'
|| node.type === 'NewExpression';
}

module.exports = shouldAddParenthesesToCallExpressionCallee;
63 changes: 49 additions & 14 deletions test/no-array-callback-reference.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -265,25 +265,16 @@ test({
),

// Need parenthesized

invalidTestCase({
code: 'foo.map(a ? b : c)',
code: 'foo.map(a || b)',
method: 'map',
suggestions: [
'foo.map((element) => (a ? b : c)(element))',
'foo.map((element, index) => (a ? b : c)(element, index))',
'foo.map((element, index, array) => (a ? b : c)(element, index, array))',
'foo.map((element) => (a || b)(element))',
'foo.map((element, index) => (a || b)(element, index))',
'foo.map((element, index, array) => (a || b)(element, index, array))',
],
}),
// Note: `await` is not handled, not sure if this is needed
// invalidTestCase({
// code: `foo.map(await foo())`,
// method: 'map',
// suggestions: [
// `foo.map(async (accumulator, element) => (await foo())(accumulator, element))`,
// `foo.map(async (accumulator, element, index) => (await foo())(accumulator, element, index))`,
// `foo.map(async (accumulator, element, index, array) => (await foo())(accumulator, element, index, array))`
// ]
// }),

// Actual messages
{
Expand Down Expand Up @@ -444,3 +435,47 @@ test({
}),
],
});

// Ternaries
test.snapshot({
valid: [
'foo.map(_ ? () => {} : _ ? () => {} : () => {})',
'foo.reduce(_ ? () => {} : _ ? () => {} : () => {})',
'foo.every(_ ? Boolean : _ ? Boolean : Boolean)',
'foo.map(_ ? String : _ ? Number : Boolean)',
],
invalid: [
outdent`
foo.map(
_
? String // This one should be ignored
: callback
);
`,
outdent`
foo.forEach(
_
? callbackA
: _
? callbackB
: callbackC
);
`,
// Needs parentheses
// Some of them was ignored since we know they are not callback function
outdent`
async function * foo () {
foo.map((0, bar));
foo.map(yield bar);
foo.map(yield* bar);
foo.map(() => bar);
foo.map(bar &&= baz);
foo.map(bar || baz);
foo.map(bar + bar);
foo.map(+ bar);
foo.map(++ bar);
foo.map(new Function(''));
}
`,
],
});
Loading
Loading