eslint-plugin-unicorn
Version:
More than 100 powerful ESLint rules
234 lines (201 loc) • 5.48 kB
JavaScript
import {findVariable} from '@eslint-community/eslint-utils';
import {getVariableIdentifiers} from './utils/index.js';
import {isCallOrNewExpression, isMethodCall} from './ast/index.js';
const MESSAGE_ID_ERROR = 'error';
const MESSAGE_ID_SUGGESTION = 'suggestion';
const messages = {
[MESSAGE_ID_ERROR]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.',
[MESSAGE_ID_SUGGESTION]: 'Switch `{{name}}` to `Set`.',
};
/*
Some of these methods can be `Iterator`.
Since `Iterator` don't have an `includes()` method, we are safe to assume they are array. Except `concat` and `slice` which can be a string: https://github.com/sindresorhus/eslint-plugin-unicorn/issues/2216
*/
const methodsReturnsArray = [
// `Array`
'copyWithin',
'fill',
'filter',
'flat',
'flatMap',
'map',
'reverse',
'sort',
'splice',
'toReversed',
'toSorted',
'toSpliced',
'with',
// `String`
'split',
// `Iterator`
'toArray',
];
const methodsReturnsArrayAndString = [
'slice',
'concat',
];
const isStringLiteral = node =>
(node.type === 'Literal' && typeof node.value === 'string')
|| (node.type === 'TemplateLiteral' && node.expressions.length === 0);
const isIdentifierInitializedWithArray = (node, scope, visitedVariables = new Set()) => {
if (node.type !== 'Identifier') {
return false;
}
const variable = findVariable(scope, node);
if (!variable || visitedVariables.has(variable) || variable.defs.length !== 1) {
return false;
}
visitedVariables.add(variable);
const [definition] = variable.defs;
return definition.type === 'Variable'
&& definition.kind === 'const'
&& Boolean(definition.node.init)
&& isArrayMethodCall(definition.node.init, scope, visitedVariables);
};
const isIncludesCall = node =>
isMethodCall(node.parent.parent, {
method: 'includes',
optionalCall: false,
optionalMember: false,
argumentsLength: 1,
})
&& node.parent.object === node;
const multipleCallNodeTypes = new Set([
'ForOfStatement',
'ForStatement',
'ForInStatement',
'WhileStatement',
'DoWhileStatement',
'FunctionDeclaration',
'FunctionExpression',
'ArrowFunctionExpression',
]);
const isMultipleCall = (identifier, node) => {
const root = node.parent.parent.parent;
let {parent} = identifier.parent; // `.include()` callExpression
while (
parent
&& parent !== root
) {
if (multipleCallNodeTypes.has(parent.type)) {
return true;
}
parent = parent.parent;
}
return false;
};
const isArrayMethodCall = (node, scope, visitedVariables = new Set()) =>
// `[]`
node.type === 'ArrayExpression'
// `Array()` and `new Array()`
|| isCallOrNewExpression(node, {
name: 'Array',
optional: false,
})
// `Array.from()` and `Array.of()`
|| isMethodCall(node, {
object: 'Array',
methods: ['from', 'of'],
optionalCall: false,
optionalMember: false,
})
// Methods that return an array
|| isMethodCall(node, {
methods: methodsReturnsArray,
optionalCall: false,
optionalMember: false,
})
|| (
isMethodCall(node, {
methods: methodsReturnsArrayAndString,
optionalCall: false,
optionalMember: false,
})
&& !isStringLiteral(node.callee.object)
&& (
node.callee.object.type !== 'Identifier'
|| isIdentifierInitializedWithArray(node.callee.object, scope, visitedVariables)
)
);
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => {
context.on('Identifier', node => {
const {parent} = node;
if (!(
parent.type === 'VariableDeclarator'
&& parent.id === node
&& Boolean(parent.init)
&& parent.parent.type === 'VariableDeclaration'
&& parent.parent.declarations.includes(parent)
// Exclude `export const foo = [];`
&& !(
parent.parent.parent.type === 'ExportNamedDeclaration'
&& parent.parent.parent.declaration === parent.parent
)
&& isArrayMethodCall(parent.init, context.sourceCode.getScope(parent.init))
)) {
return;
}
const variable = findVariable(context.sourceCode.getScope(node), node);
// This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342
// But can't reproduce, just ignore this case
/* c8 ignore next 3 */
if (!variable) {
return;
}
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node);
if (
identifiers.length === 0
|| identifiers.some(identifier => !isIncludesCall(identifier))
) {
return;
}
if (
identifiers.length === 1
&& identifiers.every(identifier => !isMultipleCall(identifier, node))
) {
return;
}
const problem = {
node,
messageId: MESSAGE_ID_ERROR,
data: {
name: node.name,
},
};
const fix = function * (fixer) {
yield fixer.insertTextBefore(node.parent.init, 'new Set(');
yield fixer.insertTextAfter(node.parent.init, ')');
for (const identifier of identifiers) {
yield fixer.replaceText(identifier.parent.property, 'has');
}
};
if (node.typeAnnotation) {
problem.suggest = [
{
messageId: MESSAGE_ID_SUGGESTION,
fix,
},
];
} else {
problem.fix = fix;
}
return problem;
});
};
/** @type {import('eslint').Rule.RuleModule} */
const config = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.',
recommended: 'unopinionated',
},
fixable: 'code',
hasSuggestions: true,
messages,
},
};
export default config;