123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- 'use strict';
- const {findVariable} = require('eslint-utils');
- const getVariableIdentifiers = require('./utils/get-variable-identifiers.js');
- const {
- matches,
- not,
- methodCallSelector,
- callOrNewExpressionSelector,
- } = require('./selectors/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`.',
- };
- // `[]`
- const arrayExpressionSelector = [
- '[init.type="ArrayExpression"]',
- ].join('');
- // `Array()` and `new Array()`
- const newArraySelector = callOrNewExpressionSelector({name: 'Array', path: 'init'});
- // `Array.from()` and `Array.of()`
- const arrayStaticMethodSelector = methodCallSelector({
- object: 'Array',
- methods: ['from', 'of'],
- path: 'init',
- });
- // `array.concat()`
- // `array.copyWithin()`
- // `array.fill()`
- // `array.filter()`
- // `array.flat()`
- // `array.flatMap()`
- // `array.map()`
- // `array.reverse()`
- // `array.slice()`
- // `array.sort()`
- // `array.splice()`
- const arrayMethodSelector = methodCallSelector({
- methods: [
- 'concat',
- 'copyWithin',
- 'fill',
- 'filter',
- 'flat',
- 'flatMap',
- 'map',
- 'reverse',
- 'slice',
- 'sort',
- 'splice',
- ],
- path: 'init',
- });
- const selector = [
- 'VariableDeclaration',
- // Exclude `export const foo = [];`
- not('ExportNamedDeclaration > .declaration'),
- ' > ',
- 'VariableDeclarator.declarations',
- matches([
- arrayExpressionSelector,
- newArraySelector,
- arrayStaticMethodSelector,
- arrayMethodSelector,
- ]),
- ' > ',
- 'Identifier.id',
- ].join('');
- const isIncludesCall = node => {
- /* istanbul ignore next */
- if (!node.parent || !node.parent.parent) {
- return false;
- }
- const {type, optional, callee, arguments: includesArguments} = node.parent.parent;
- return (
- type === 'CallExpression'
- && !optional
- && callee
- && callee.type === 'MemberExpression'
- && !callee.computed
- && !callee.optional
- && callee.object === node
- && callee.property.type === 'Identifier'
- && callee.property.name === 'includes'
- && includesArguments.length === 1
- && includesArguments[0].type !== 'SpreadElement'
- );
- };
- 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;
- };
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => ({
- [selector]: node => {
- const variable = findVariable(context.getScope(), node);
- // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342
- // But can't reproduce, just ignore this case
- /* istanbul ignore next */
- 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,
- data: {
- name: node.name,
- },
- fix,
- },
- ];
- } else {
- problem.fix = fix;
- }
- return problem;
- },
- });
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'suggestion',
- docs: {
- description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.',
- },
- fixable: 'code',
- hasSuggestions: true,
- messages,
- },
- };
|