UNPKG

3.17 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 create = context => {
111 return {
112 [selector]: node => {
113 const variable = findVariable(context.getScope(), node);
114 const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node);
115
116 if (
117 identifiers.length === 0 ||
118 identifiers.some(node => !isIncludesCall(node))
119 ) {
120 return;
121 }
122
123 context.report({
124 node,
125 messageId: MESSAGE_ID,
126 data: {
127 name: node.name
128 },
129 fix: fixer => [
130 fixer.insertTextBefore(node.parent.init, 'new Set('),
131 fixer.insertTextAfter(node.parent.init, ')'),
132 ...identifiers.map(identifier => fixer.replaceText(identifier.parent.property, 'has'))
133 ]
134 });
135 }
136 };
137};
138
139module.exports = {
140 create,
141 meta: {
142 type: 'suggestion',
143 docs: {
144 url: getDocumentationUrl(__filename)
145 },
146 fixable: 'code',
147 messages: {
148 [MESSAGE_ID]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.'
149 }
150 }
151};