eslint-plugin-unicorn
Version:
More than 100 powerful ESLint rules
346 lines (305 loc) • 8.05 kB
JavaScript
import {findVariable} from '@eslint-community/eslint-utils';
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)
&& 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;
}
// These methods have dedicated type-predicate overloads in TypeScript's lib files.
// Wrapping a type guard can lose narrowing, so direct references should be allowed here.
const methodsWithTypePredicateOverloads = new Set([
'every',
'filter',
'find',
'findLast',
]);
function hasTypePredicateReturnType(node) {
return node.returnType?.typeAnnotation?.type === 'TSTypePredicate';
}
function hasTypePredicateFunctionType(node) {
return node.typeAnnotation?.typeAnnotation?.returnType?.typeAnnotation?.type === 'TSTypePredicate';
}
function isTypePredicateCallback(callback, context) {
if (callback.type !== 'Identifier') {
return false;
}
// Keep this local and syntax-based. Imported/member expressions need type-aware linting.
const variable = findVariable(context.sourceCode.getScope(callback), callback);
const definition = variable?.defs[0];
if (!definition) {
return false;
}
if (definition.type === 'FunctionName') {
return hasTypePredicateReturnType(definition.node);
}
// Imported callbacks may be type guards, but we can't inspect their predicate return
// type without type-aware linting. Be conservative on methods with predicate overloads.
if (definition.type === 'ImportBinding') {
return true;
}
if (definition.type === 'Parameter') {
return hasTypePredicateFunctionType(definition.name);
}
if (definition.type === 'Variable') {
if (hasTypePredicateFunctionType(definition.node.id)) {
return true;
}
const {init} = definition.node;
return init
&& (init.type === 'ArrowFunctionExpression' || init.type === 'FunctionExpression')
&& hasTypePredicateReturnType(init);
}
return false;
}
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('CallExpression', function * (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, including `function.bind()`
|| callback.type === 'CallExpression'
|| options.shouldIgnoreCallback(callback)
|| isNodeValueNotFunction(callback)
|| (methodsWithTypePredicateOverloads.has(methodName) && isTypePredicateCallback(callback, context))
) {
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;