UNPKG

9.2 kBJavaScriptView Raw
1'use strict';
2const {isParenthesized, findVariable} = require('eslint-utils');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4const methodSelector = require('./utils/method-selector');
5const getVariableIdentifiers = require('./utils/get-variable-identifiers');
6
7const ERROR_ZERO_INDEX = 'error-zero-index';
8const ERROR_SHIFT = 'error-shift';
9const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration';
10const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment';
11const ERROR_DECLARATION = 'error-variable';
12
13const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator';
14const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator';
15
16const filterMethodSelectorOptions = {
17 name: 'filter',
18 min: 1,
19 max: 2
20};
21
22const filterVariableSelector = [
23 'VariableDeclaration',
24 // Exclude `export const foo = [];`
25 `:not(${
26 [
27 'ExportNamedDeclaration',
28 '>',
29 'VariableDeclaration.declaration'
30 ].join('')
31 })`,
32 '>',
33 'VariableDeclarator.declarations',
34 '[id.type="Identifier"]',
35 methodSelector({
36 ...filterMethodSelectorOptions,
37 property: 'init'
38 })
39].join('');
40
41const zeroIndexSelector = [
42 'MemberExpression',
43 '[computed=true]',
44 '[property.type="Literal"]',
45 '[property.raw="0"]',
46 methodSelector({
47 ...filterMethodSelectorOptions,
48 property: 'object'
49 })
50].join('');
51
52const shiftSelector = [
53 methodSelector({
54 name: 'shift',
55 length: 0
56 }),
57 methodSelector({
58 ...filterMethodSelectorOptions,
59 property: 'callee.object'
60 })
61].join('');
62
63const destructuringDeclaratorSelector = [
64 'VariableDeclarator',
65 '[id.type="ArrayPattern"]',
66 '[id.elements.length=1]',
67 '[id.elements.0.type!="RestElement"]',
68 methodSelector({
69 ...filterMethodSelectorOptions,
70 property: 'init'
71 })
72].join('');
73
74const destructuringAssignmentSelector = [
75 'AssignmentExpression',
76 '[left.type="ArrayPattern"]',
77 '[left.elements.length=1]',
78 '[left.elements.0.type!="RestElement"]',
79 methodSelector({
80 ...filterMethodSelectorOptions,
81 property: 'right'
82 })
83].join('');
84
85// Need add `()` to the `AssignmentExpression`
86// - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)`
87// - `ObjectPattern`: `[{foo = baz}] = array.filter(bar)`
88const assignmentNeedParenthesize = (node, source) => {
89 const isAssign = node.type === 'AssignmentExpression';
90
91 if (!isAssign || isParenthesized(node, source)) {
92 return false;
93 }
94
95 const {left} = getDestructuringLeftAndRight(node);
96 const [element] = left.elements;
97 const {type} = element.type === 'AssignmentPattern' ? element.left : element;
98 return type === 'ObjectExpression' || type === 'ObjectPattern';
99};
100
101// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
102const hasLowerPrecedence = (node, operator) => (
103 (node.type === 'LogicalExpression' && (
104 node.operator === operator ||
105 // https://tc39.es/proposal-nullish-coalescing/ says
106 // `??` has lower precedence than `||`
107 // But MDN says
108 // `??` has higher precedence than `||`
109 (operator === '||' && node.operator === '??') ||
110 (operator === '??' && (node.operator === '||' || node.operator === '&&'))
111 )) ||
112 node.type === 'ConditionalExpression' ||
113 // Lower than `assignment`, should already parenthesized
114 /* istanbul ignore next */
115 node.type === 'AssignmentExpression' ||
116 node.type === 'YieldExpression' ||
117 node.type === 'SequenceExpression'
118);
119
120const getDestructuringLeftAndRight = node => {
121 /* istanbul ignore next */
122 if (!node) {
123 return {};
124 }
125
126 if (node.type === 'AssignmentExpression') {
127 return node;
128 }
129
130 if (node.type === 'VariableDeclarator') {
131 return {left: node.id, right: node.init};
132 }
133
134 return {};
135};
136
137function * fixDestructuring(node, source, fixer) {
138 const {left} = getDestructuringLeftAndRight(node);
139 const [element] = left.elements;
140
141 const leftText = source.getText(element.type === 'AssignmentPattern' ? element.left : element);
142 yield fixer.replaceText(left, leftText);
143
144 // `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI
145 if (assignmentNeedParenthesize(node, source)) {
146 yield fixer.insertTextBefore(node, '(');
147 yield fixer.insertTextAfter(node, ')');
148 }
149}
150
151const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern';
152
153const fixDestructuringDefaultValue = (node, source, fixer, operator) => {
154 const {left, right} = getDestructuringLeftAndRight(node);
155 const [element] = left.elements;
156 const defaultValue = element.right;
157 let defaultValueText = source.getText(defaultValue);
158
159 if (isParenthesized(defaultValue, source) || hasLowerPrecedence(defaultValue, operator)) {
160 defaultValueText = `(${defaultValueText})`;
161 }
162
163 return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`);
164};
165
166const fixDestructuringAndReplaceFilter = (source, node) => {
167 const {property} = getDestructuringLeftAndRight(node).right.callee;
168
169 let suggest;
170 let fix;
171
172 if (hasDefaultValue(node)) {
173 suggest = [
174 {operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR},
175 {operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR}
176 ].map(({messageId, operator}) => ({
177 messageId,
178 * fix(fixer) {
179 yield fixer.replaceText(property, 'find');
180 yield fixDestructuringDefaultValue(node, source, fixer, operator);
181 yield * fixDestructuring(node, source, fixer);
182 }
183 }));
184 } else {
185 fix = function * (fixer) {
186 yield fixer.replaceText(property, 'find');
187 yield * fixDestructuring(node, source, fixer);
188 };
189 }
190
191 return {fix, suggest};
192};
193
194const isAccessingZeroIndex = node =>
195 node.parent &&
196 node.parent.type === 'MemberExpression' &&
197 node.parent.computed === true &&
198 node.parent.object === node &&
199 node.parent.property &&
200 node.parent.property.type === 'Literal' &&
201 node.parent.property.raw === '0';
202
203const isDestructuringFirstElement = node => {
204 const {left, right} = getDestructuringLeftAndRight(node.parent);
205 return left &&
206 right &&
207 right === node &&
208 left.type === 'ArrayPattern' &&
209 left.elements &&
210 left.elements.length === 1 &&
211 left.elements[0].type !== 'RestElement';
212};
213
214const create = context => {
215 const source = context.getSourceCode();
216
217 return {
218 [zeroIndexSelector](node) {
219 context.report({
220 node: node.object.callee.property,
221 messageId: ERROR_ZERO_INDEX,
222 fix: fixer => [
223 fixer.replaceText(node.object.callee.property, 'find'),
224 fixer.removeRange([node.object.range[1], node.range[1]])
225 ]
226 });
227 },
228 [shiftSelector](node) {
229 context.report({
230 node: node.callee.object.callee.property,
231 messageId: ERROR_SHIFT,
232 fix: fixer => [
233 fixer.replaceText(node.callee.object.callee.property, 'find'),
234 fixer.removeRange([node.callee.object.range[1], node.range[1]])
235 ]
236 });
237 },
238 [destructuringDeclaratorSelector](node) {
239 context.report({
240 node: node.init.callee.property,
241 messageId: ERROR_DESTRUCTURING_DECLARATION,
242 ...fixDestructuringAndReplaceFilter(source, node)
243 });
244 },
245 [destructuringAssignmentSelector](node) {
246 context.report({
247 node: node.right.callee.property,
248 messageId: ERROR_DESTRUCTURING_ASSIGNMENT,
249 ...fixDestructuringAndReplaceFilter(source, node)
250 });
251 },
252 [filterVariableSelector](node) {
253 const variable = findVariable(context.getScope(), node.id);
254 const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);
255
256 if (identifiers.length === 0) {
257 return;
258 }
259
260 const zeroIndexNodes = [];
261 const destructuringNodes = [];
262 for (const identifier of identifiers) {
263 if (isAccessingZeroIndex(identifier)) {
264 zeroIndexNodes.push(identifier.parent);
265 } else if (isDestructuringFirstElement(identifier)) {
266 destructuringNodes.push(identifier.parent);
267 } else {
268 return;
269 }
270 }
271
272 const problem = {
273 node: node.init.callee.property,
274 messageId: ERROR_DECLARATION
275 };
276
277 // `const [foo = bar] = baz` is not fixable
278 if (!destructuringNodes.some(node => hasDefaultValue(node))) {
279 problem.fix = function * (fixer) {
280 yield fixer.replaceText(node.init.callee.property, 'find');
281
282 for (const node of zeroIndexNodes) {
283 yield fixer.removeRange([node.object.range[1], node.range[1]]);
284 }
285
286 for (const node of destructuringNodes) {
287 yield * fixDestructuring(node, source, fixer);
288 }
289 };
290 }
291
292 context.report(problem);
293 }
294 };
295};
296
297module.exports = {
298 create,
299 meta: {
300 type: 'suggestion',
301 docs: {
302 url: getDocumentationUrl(__filename)
303 },
304 fixable: 'code',
305 messages: {
306 [ERROR_DECLARATION]: 'Prefer `.find(…)` over `.filter(…)`.',
307 [ERROR_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.',
308 [ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.',
309 [ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
310 // Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case
311 [ERROR_DESTRUCTURING_ASSIGNMENT]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
312 [SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.',
313 [SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.'
314 }
315 }
316};