1 | 'use strict';
|
2 | const {findVariable} = require('eslint-utils');
|
3 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
4 | const getVariableIdentifiers = require('./utils/get-variable-identifiers');
|
5 | const methodSelector = require('./utils/method-selector');
|
6 |
|
7 |
|
8 | const arrayExpressionSelector = [
|
9 | '[init.type="ArrayExpression"]'
|
10 | ].join('');
|
11 |
|
12 |
|
13 | const ArraySelector = [
|
14 | '[init.type="CallExpression"]',
|
15 | '[init.callee.type="Identifier"]',
|
16 | '[init.callee.name="Array"]'
|
17 | ].join('');
|
18 |
|
19 |
|
20 | const newArraySelector = [
|
21 | '[init.type="NewExpression"]',
|
22 | '[init.callee.type="Identifier"]',
|
23 | '[init.callee.name="Array"]'
|
24 | ].join('');
|
25 |
|
26 |
|
27 |
|
28 | const arrayStaticMethodSelector = methodSelector({
|
29 | object: 'Array',
|
30 | names: ['from', 'of'],
|
31 | property: 'init'
|
32 | });
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 | const arrayMethodSelector = methodSelector({
|
46 | names: [
|
47 | 'concat',
|
48 | 'copyWithin',
|
49 | 'fill',
|
50 | 'filter',
|
51 | 'flat',
|
52 | 'flatMap',
|
53 | 'map',
|
54 | 'reverse',
|
55 | 'slice',
|
56 | 'sort',
|
57 | 'splice'
|
58 | ],
|
59 | property: 'init'
|
60 | });
|
61 |
|
62 | const selector = [
|
63 | 'VariableDeclaration',
|
64 |
|
65 | `:not(${
|
66 | [
|
67 | 'ExportNamedDeclaration',
|
68 | '>',
|
69 | 'VariableDeclaration.declaration'
|
70 | ].join('')
|
71 | })`,
|
72 | '>',
|
73 | 'VariableDeclarator.declarations',
|
74 | `:matches(${
|
75 | [
|
76 | arrayExpressionSelector,
|
77 | ArraySelector,
|
78 | newArraySelector,
|
79 | arrayStaticMethodSelector,
|
80 | arrayMethodSelector
|
81 | ].join(',')
|
82 | })`,
|
83 | '>',
|
84 | 'Identifier.id'
|
85 | ].join('');
|
86 |
|
87 | const MESSAGE_ID = 'preferSetHas';
|
88 |
|
89 | const isIncludesCall = node => {
|
90 |
|
91 | if (!node.parent || !node.parent.parent) {
|
92 | return false;
|
93 | }
|
94 |
|
95 | const {type, optional, callee, arguments: parameters} = node.parent.parent;
|
96 | return (
|
97 | type === 'CallExpression' &&
|
98 | !optional,
|
99 | callee &&
|
100 | callee.type === 'MemberExpression' &&
|
101 | !callee.computed &&
|
102 | callee.object === node &&
|
103 | callee.property.type === 'Identifier' &&
|
104 | callee.property.name === 'includes' &&
|
105 | parameters.length === 1 &&
|
106 | parameters[0].type !== 'SpreadElement'
|
107 | );
|
108 | };
|
109 |
|
110 | const multipleCallNodeTypes = new Set([
|
111 | 'ForOfStatement',
|
112 | 'ForStatement',
|
113 | 'ForInStatement',
|
114 | 'WhileStatement',
|
115 | 'DoWhileStatement',
|
116 | 'FunctionDeclaration',
|
117 | 'FunctionExpression',
|
118 | 'ArrowFunctionExpression',
|
119 | 'ObjectMethod',
|
120 | 'ClassMethod'
|
121 | ]);
|
122 |
|
123 | const isMultipleCall = (identifier, node) => {
|
124 | const root = node.parent.parent.parent;
|
125 | let {parent} = identifier.parent;
|
126 | while (
|
127 | parent &&
|
128 | parent !== root
|
129 | ) {
|
130 | if (multipleCallNodeTypes.has(parent.type)) {
|
131 | return true;
|
132 | }
|
133 |
|
134 | parent = parent.parent;
|
135 | }
|
136 |
|
137 | return false;
|
138 | };
|
139 |
|
140 | const create = context => {
|
141 | return {
|
142 | [selector]: node => {
|
143 | const variable = findVariable(context.getScope(), node);
|
144 | const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node);
|
145 |
|
146 | if (
|
147 | identifiers.length === 0 ||
|
148 | identifiers.some(identifier => !isIncludesCall(identifier))
|
149 | ) {
|
150 | return;
|
151 | }
|
152 |
|
153 | if (
|
154 | identifiers.length === 1 &&
|
155 | identifiers.every(identifier => !isMultipleCall(identifier, node))
|
156 | ) {
|
157 | return;
|
158 | }
|
159 |
|
160 | context.report({
|
161 | node,
|
162 | messageId: MESSAGE_ID,
|
163 | data: {
|
164 | name: node.name
|
165 | },
|
166 | * fix(fixer) {
|
167 | yield fixer.insertTextBefore(node.parent.init, 'new Set(');
|
168 | yield fixer.insertTextAfter(node.parent.init, ')');
|
169 |
|
170 | for (const identifier of identifiers) {
|
171 | yield fixer.replaceText(identifier.parent.property, 'has');
|
172 | }
|
173 | }
|
174 | });
|
175 | }
|
176 | };
|
177 | };
|
178 |
|
179 | module.exports = {
|
180 | create,
|
181 | meta: {
|
182 | type: 'suggestion',
|
183 | docs: {
|
184 | url: getDocumentationUrl(__filename)
|
185 | },
|
186 | fixable: 'code',
|
187 | messages: {
|
188 | [MESSAGE_ID]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.'
|
189 | }
|
190 | }
|
191 | };
|