UNPKG

eslint-plugin-unicorn

Version:
234 lines (201 loc) 5.48 kB
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;