285 lines
6.1 KiB
JavaScript
285 lines
6.1 KiB
JavaScript
import {isMethodCall} from './ast/index.js';
|
|
import {
|
|
isNodeMatches,
|
|
isNodeValueNotFunction,
|
|
isParenthesized,
|
|
getParenthesizedRange,
|
|
getParenthesizedText,
|
|
shouldAddParenthesesToCallExpressionCallee,
|
|
} from './utils/index.js';
|
|
|
|
const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
|
|
const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
|
|
const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
|
|
const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
|
|
const messages = {
|
|
[ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
|
|
[ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
|
|
[REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
|
|
[REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.',
|
|
};
|
|
|
|
const isAwaitExpressionArgument = node => node.parent.type === 'AwaitExpression' && node.parent.argument === node;
|
|
|
|
const iteratorMethods = new Map([
|
|
{
|
|
method: 'every',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'filter',
|
|
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'Vue'),
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'find',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'findLast',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'findIndex',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'findLastIndex',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
{
|
|
method: 'flatMap',
|
|
},
|
|
{
|
|
method: 'forEach',
|
|
returnsUndefined: true,
|
|
},
|
|
{
|
|
method: 'map',
|
|
shouldIgnoreCallExpression: node => (node.callee.object.type === 'Identifier' && node.callee.object.name === 'types'),
|
|
ignore: [
|
|
'String',
|
|
'Number',
|
|
'BigInt',
|
|
'Boolean',
|
|
'Symbol',
|
|
],
|
|
},
|
|
{
|
|
method: 'reduce',
|
|
parameters: [
|
|
'accumulator',
|
|
'element',
|
|
'index',
|
|
'array',
|
|
],
|
|
minParameters: 2,
|
|
},
|
|
{
|
|
method: 'reduceRight',
|
|
parameters: [
|
|
'accumulator',
|
|
'element',
|
|
'index',
|
|
'array',
|
|
],
|
|
minParameters: 2,
|
|
},
|
|
{
|
|
method: 'some',
|
|
ignore: [
|
|
'Boolean',
|
|
],
|
|
},
|
|
].map(({
|
|
method,
|
|
parameters = ['element', 'index', 'array'],
|
|
ignore = [],
|
|
minParameters = 1,
|
|
returnsUndefined = false,
|
|
shouldIgnoreCallExpression,
|
|
}) => [method, {
|
|
minParameters,
|
|
parameters,
|
|
returnsUndefined,
|
|
shouldIgnoreCallExpression(callExpression) {
|
|
if (
|
|
method !== 'reduce'
|
|
&& method !== 'reduceRight'
|
|
&& isAwaitExpressionArgument(callExpression)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (isNodeMatches(callExpression.callee.object, ignoredCallee)) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
callExpression.callee.object.type === 'CallExpression'
|
|
&& isNodeMatches(callExpression.callee.object.callee, ignoredCallee)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
return shouldIgnoreCallExpression?.(callExpression) ?? false;
|
|
},
|
|
shouldIgnoreCallback(callback) {
|
|
if (callback.type === 'Identifier' && ignore.includes(callback.name)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
},
|
|
}]));
|
|
|
|
const ignoredCallee = [
|
|
// http://bluebirdjs.com/docs/api/promise.map.html
|
|
'Promise',
|
|
'React.Children',
|
|
'Children',
|
|
'lodash',
|
|
'underscore',
|
|
'_',
|
|
'Async',
|
|
'async',
|
|
'this',
|
|
'$',
|
|
'jQuery',
|
|
];
|
|
|
|
function getProblem(context, node, method, options) {
|
|
const {type} = node;
|
|
|
|
const name = type === 'Identifier' ? node.name : '';
|
|
|
|
const problem = {
|
|
node,
|
|
messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
|
|
data: {
|
|
name,
|
|
method,
|
|
},
|
|
};
|
|
|
|
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(', ');
|
|
|
|
const suggest = {
|
|
messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
|
|
data: {
|
|
name,
|
|
parameters: suggestionParameters,
|
|
},
|
|
fix(fixer) {
|
|
let text = getParenthesizedText(node, context);
|
|
|
|
if (
|
|
!isParenthesized(node, context.sourceCode)
|
|
&& shouldAddParenthesesToCallExpressionCallee(node)
|
|
) {
|
|
text = `(${text})`;
|
|
}
|
|
|
|
return fixer.replaceTextRange(
|
|
getParenthesizedRange(node, context),
|
|
returnsUndefined
|
|
? `(${suggestionParameters}) => { ${text}(${suggestionParameters}); }`
|
|
: `(${suggestionParameters}) => ${text}(${suggestionParameters})`,
|
|
);
|
|
},
|
|
};
|
|
|
|
problem.suggest.push(suggest);
|
|
}
|
|
|
|
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(callExpression) {
|
|
if (
|
|
!isMethodCall(callExpression, {
|
|
minimumArguments: 1,
|
|
maximumArguments: 2,
|
|
optionalCall: false,
|
|
computed: false,
|
|
})
|
|
|| callExpression.callee.property.type !== 'Identifier'
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const methodNode = callExpression.callee.property;
|
|
const methodName = methodNode.name;
|
|
if (!iteratorMethods.has(methodName)) {
|
|
return;
|
|
}
|
|
|
|
const options = iteratorMethods.get(methodName);
|
|
if (options.shouldIgnoreCallExpression(callExpression)) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
yield getProblem(context, callback, methodName, options);
|
|
}
|
|
},
|
|
});
|
|
|
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
const config = {
|
|
create,
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Prevent passing a function reference directly to iterator methods.',
|
|
recommended: true,
|
|
},
|
|
hasSuggestions: true,
|
|
messages,
|
|
},
|
|
};
|
|
|
|
export default config;
|