UNPKG

6.15 kBJavaScriptView Raw
1'use strict';
2const getDocumentationUrl = require('./utils/get-documentation-url');
3const isLiteralValue = require('./utils/is-literal-value');
4
5const methods = new Map([
6 [
7 'slice',
8 {
9 argumentsIndexes: [0, 1],
10 supportObjects: new Set([
11 'Array',
12 'String',
13 'ArrayBuffer',
14 'Int8Array',
15 'Uint8Array',
16 'Uint8ClampedArray',
17 'Int16Array',
18 'Uint16Array',
19 'Int32Array',
20 'Uint32Array',
21 'Float32Array',
22 'Float64Array',
23 'BigInt64Array',
24 'BigUint64Array'
25 // `{Blob,File}#slice()` are not generally used
26 // 'Blob'
27 // 'File'
28 ])
29 }
30 ],
31 [
32 'splice',
33 {
34 argumentsIndexes: [0],
35 supportObjects: new Set([
36 'Array'
37 ])
38 }
39 ]
40]);
41
42const OPERATOR_MINUS = '-';
43
44const isPropertiesEqual = (node1, node2) => properties => {
45 return properties.every(property => isEqual(node1[property], node2[property]));
46};
47
48const isTemplateElementEqual = (node1, node2) => {
49 return (
50 node1.value &&
51 node2.value &&
52 node1.tail === node2.tail &&
53 isPropertiesEqual(node1.value, node2.value)(['cooked', 'raw'])
54 );
55};
56
57const isTemplateLiteralEqual = (node1, node2) => {
58 const {quasis: quasis1} = node1;
59 const {quasis: quasis2} = node2;
60
61 return (
62 quasis1.length === quasis2.length &&
63 quasis1.every((templateElement, index) =>
64 isEqual(templateElement, quasis2[index])
65 )
66 );
67};
68
69const isEqual = (node1, node2) => {
70 if (node1 === node2) {
71 return true;
72 }
73
74 const compare = isPropertiesEqual(node1, node2);
75
76 if (!compare(['type'])) {
77 return false;
78 }
79
80 const {type} = node1;
81
82 switch (type) {
83 case 'Identifier':
84 return compare(['name', 'computed']);
85 case 'Literal':
86 return compare(['value', 'raw']);
87 case 'TemplateLiteral':
88 return isTemplateLiteralEqual(node1, node2);
89 case 'TemplateElement':
90 return isTemplateElementEqual(node1, node2);
91 case 'BinaryExpression':
92 return compare(['operator', 'left', 'right']);
93 case 'MemberExpression':
94 return compare(['object', 'property']);
95 default:
96 return false;
97 }
98};
99
100const isLengthMemberExpression = node => node &&
101 node.type === 'MemberExpression' &&
102 node.property &&
103 node.property.type === 'Identifier' &&
104 node.property.name === 'length' &&
105 node.object;
106
107const isLiteralPositiveValue = node =>
108 node &&
109 node.type === 'Literal' &&
110 typeof node.value === 'number' &&
111 node.value > 0;
112
113const getLengthMemberExpression = node => {
114 if (!node) {
115 return;
116 }
117
118 const {type, operator, left, right} = node;
119
120 if (
121 type !== 'BinaryExpression' ||
122 operator !== OPERATOR_MINUS ||
123 !left ||
124 !isLiteralPositiveValue(right)
125 ) {
126 return;
127 }
128
129 if (isLengthMemberExpression(left)) {
130 return left;
131 }
132
133 // Nested BinaryExpression
134 return getLengthMemberExpression(left);
135};
136
137const getRemoveAbleNode = (target, argument) => {
138 const lengthMemberExpression = getLengthMemberExpression(argument);
139
140 if (
141 lengthMemberExpression &&
142 isEqual(target, lengthMemberExpression.object)
143 ) {
144 return lengthMemberExpression;
145 }
146};
147
148const getRemovalRange = (node, sourceCode) => {
149 let before = sourceCode.getTokenBefore(node);
150 let after = sourceCode.getTokenAfter(node);
151
152 let [start, end] = node.range;
153
154 let hasParentheses = true;
155
156 while (hasParentheses) {
157 hasParentheses =
158 before.type === 'Punctuator' &&
159 before.value === '(' &&
160 after.type === 'Punctuator' &&
161 after.value === ')';
162 if (hasParentheses) {
163 before = sourceCode.getTokenBefore(before);
164 after = sourceCode.getTokenAfter(after);
165 start = before.range[1];
166 end = after.range[0];
167 }
168 }
169
170 const [nextStart] = after.range;
171 const textBetween = sourceCode.text.slice(end, nextStart);
172
173 end += textBetween.match(/\S|$/).index;
174
175 return [start, end];
176};
177
178const getMemberName = node => {
179 const {type, property} = node;
180
181 if (
182 type === 'MemberExpression' &&
183 property &&
184 property.type === 'Identifier'
185 ) {
186 return property.name;
187 }
188};
189
190function parse(node) {
191 const {callee, arguments: originalArguments} = node;
192
193 let method = callee.property.name;
194 let target = callee.object;
195 let argumentsNodes = originalArguments;
196
197 if (methods.has(method)) {
198 return {
199 method,
200 target,
201 argumentsNodes
202 };
203 }
204
205 if (method !== 'call' && method !== 'apply') {
206 return;
207 }
208
209 const isApply = method === 'apply';
210
211 method = getMemberName(callee.object);
212
213 if (!methods.has(method)) {
214 return;
215 }
216
217 const {supportObjects} = methods.get(method);
218
219 const parentCallee = callee.object.object;
220
221 if (
222 // [].{slice,splice}
223 (
224 parentCallee.type === 'ArrayExpression' &&
225 parentCallee.elements.length === 0
226 ) ||
227 // ''.slice
228 (
229 method === 'slice' &&
230 isLiteralValue(parentCallee, '')
231 ) ||
232 // {Array,String...}.prototype.slice
233 // Array.prototype.splice
234 (
235 getMemberName(parentCallee) === 'prototype' &&
236 parentCallee.object.type === 'Identifier' &&
237 supportObjects.has(parentCallee.object.name)
238 )
239 ) {
240 [target] = originalArguments;
241
242 if (isApply) {
243 const [, secondArgument] = originalArguments;
244 if (!secondArgument || secondArgument.type !== 'ArrayExpression') {
245 return;
246 }
247
248 argumentsNodes = secondArgument.elements;
249 } else {
250 argumentsNodes = originalArguments.slice(1);
251 }
252
253 return {
254 method,
255 target,
256 argumentsNodes
257 };
258 }
259}
260
261const create = context => ({
262 CallExpression: node => {
263 if (node.callee.type !== 'MemberExpression') {
264 return;
265 }
266
267 const parsed = parse(node);
268
269 if (!parsed) {
270 return;
271 }
272
273 const {
274 method,
275 target,
276 argumentsNodes
277 } = parsed;
278
279 const {argumentsIndexes} = methods.get(method);
280 const removableNodes = argumentsIndexes
281 .map(index => getRemoveAbleNode(target, argumentsNodes[index]))
282 .filter(Boolean);
283
284 if (removableNodes.length === 0) {
285 return;
286 }
287
288 context.report({
289 node,
290 message: `Prefer negative index over length minus index for \`${method}\`.`,
291 * fix(fixer) {
292 const sourceCode = context.getSourceCode();
293 for (const node of removableNodes) {
294 yield fixer.removeRange(
295 getRemovalRange(node, sourceCode)
296 );
297 }
298 }
299 });
300 }
301});
302
303module.exports = {
304 create,
305 meta: {
306 type: 'suggestion',
307 docs: {
308 url: getDocumentationUrl(__filename)
309 },
310 fixable: 'code'
311 }
312};