1 | 'use strict';
|
2 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
3 | const isLiteralValue = require('./utils/is-literal-value');
|
4 |
|
5 | const 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 |
|
26 |
|
27 |
|
28 | ])
|
29 | }
|
30 | ],
|
31 | [
|
32 | 'splice',
|
33 | {
|
34 | argumentsIndexes: [0],
|
35 | supportObjects: new Set([
|
36 | 'Array'
|
37 | ])
|
38 | }
|
39 | ]
|
40 | ]);
|
41 |
|
42 | const OPERATOR_MINUS = '-';
|
43 |
|
44 | const isPropertiesEqual = (node1, node2) => properties => {
|
45 | return properties.every(property => isEqual(node1[property], node2[property]));
|
46 | };
|
47 |
|
48 | const 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 |
|
57 | const 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 |
|
69 | const 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 |
|
100 | const isLengthMemberExpression = node => node &&
|
101 | node.type === 'MemberExpression' &&
|
102 | node.property &&
|
103 | node.property.type === 'Identifier' &&
|
104 | node.property.name === 'length' &&
|
105 | node.object;
|
106 |
|
107 | const isLiteralPositiveValue = node =>
|
108 | node &&
|
109 | node.type === 'Literal' &&
|
110 | typeof node.value === 'number' &&
|
111 | node.value > 0;
|
112 |
|
113 | const 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 |
|
134 | return getLengthMemberExpression(left);
|
135 | };
|
136 |
|
137 | const 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 |
|
148 | const 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 |
|
178 | const 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 |
|
190 | function 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 |
|
223 | (
|
224 | parentCallee.type === 'ArrayExpression' &&
|
225 | parentCallee.elements.length === 0
|
226 | ) ||
|
227 |
|
228 | (
|
229 | method === 'slice' &&
|
230 | isLiteralValue(parentCallee, '')
|
231 | ) ||
|
232 |
|
233 |
|
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 |
|
261 | const 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 |
|
303 | module.exports = {
|
304 | create,
|
305 | meta: {
|
306 | type: 'suggestion',
|
307 | docs: {
|
308 | url: getDocumentationUrl(__filename)
|
309 | },
|
310 | fixable: 'code'
|
311 | }
|
312 | };
|