UNPKG

3.91 kBJavaScriptView Raw
1'use strict';
2const {findVariable} = require('eslint-utils');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4const getVariableIdentifiers = require('./utils/get-variable-identifiers');
5const methodSelector = require('./utils/method-selector');
6
7// `[]`
8const arrayExpressionSelector = [
9 '[init.type="ArrayExpression"]'
10].join('');
11
12// `Array()`
13const ArraySelector = [
14 '[init.type="CallExpression"]',
15 '[init.callee.type="Identifier"]',
16 '[init.callee.name="Array"]'
17].join('');
18
19// `new Array()`
20const newArraySelector = [
21 '[init.type="NewExpression"]',
22 '[init.callee.type="Identifier"]',
23 '[init.callee.name="Array"]'
24].join('');
25
26// `Array.from()`
27// `Array.of()`
28const arrayStaticMethodSelector = methodSelector({
29 object: 'Array',
30 names: ['from', 'of'],
31 property: 'init'
32});
33
34// `array.concat()`
35// `array.copyWithin()`
36// `array.fill()`
37// `array.filter()`
38// `array.flat()`
39// `array.flatMap()`
40// `array.map()`
41// `array.reverse()`
42// `array.slice()`
43// `array.sort()`
44// `array.splice()`
45const 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
62const selector = [
63 'VariableDeclaration',
64 // Exclude `export const foo = [];`
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
87const MESSAGE_ID = 'preferSetHas';
88
89const isIncludesCall = node => {
90 /* istanbul ignore next */
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
110const 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
123const isMultipleCall = (identifier, node) => {
124 const root = node.parent.parent.parent;
125 let {parent} = identifier.parent; // `.include()` callExpression
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
140const 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
179module.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};