UNPKG

9.69 kBJavaScriptView Raw
1'use strict';
2const getDocumentationUrl = require('./utils/get-documentation-url');
3const isLiteralValue = require('./utils/is-literal-value');
4
5const defaultElementName = 'element';
6const isLiteralZero = node => isLiteralValue(node, 0);
7const isLiteralOne = node => isLiteralValue(node, 1);
8
9const isIdentifierWithName = (node, name) => node && node.type === 'Identifier' && node.name === name;
10
11const getIndexIdentifierName = forStatement => {
12 const {init: variableDeclaration} = forStatement;
13
14 if (
15 !variableDeclaration ||
16 variableDeclaration.type !== 'VariableDeclaration'
17 ) {
18 return;
19 }
20
21 if (variableDeclaration.declarations.length !== 1) {
22 return;
23 }
24
25 const [variableDeclarator] = variableDeclaration.declarations;
26
27 if (!isLiteralZero(variableDeclarator.init)) {
28 return;
29 }
30
31 if (variableDeclarator.id.type !== 'Identifier') {
32 return;
33 }
34
35 return variableDeclarator.id.name;
36};
37
38const getStrictComparisonOperands = binaryExpression => {
39 if (binaryExpression.operator === '<') {
40 return {
41 lesser: binaryExpression.left,
42 greater: binaryExpression.right
43 };
44 }
45
46 if (binaryExpression.operator === '>') {
47 return {
48 lesser: binaryExpression.right,
49 greater: binaryExpression.left
50 };
51 }
52};
53
54const getArrayIdentifierNameFromBinaryExpression = (binaryExpression, indexIdentifierName) => {
55 const operands = getStrictComparisonOperands(binaryExpression);
56
57 if (!operands) {
58 return;
59 }
60
61 const {lesser, greater} = operands;
62
63 if (!isIdentifierWithName(lesser, indexIdentifierName)) {
64 return;
65 }
66
67 if (greater.type !== 'MemberExpression') {
68 return;
69 }
70
71 if (
72 greater.object.type !== 'Identifier' ||
73 greater.property.type !== 'Identifier'
74 ) {
75 return;
76 }
77
78 if (greater.property.name !== 'length') {
79 return;
80 }
81
82 return greater.object.name;
83};
84
85const getArrayIdentifierName = (forStatement, indexIdentifierName) => {
86 const {test} = forStatement;
87
88 if (!test || test.type !== 'BinaryExpression') {
89 return;
90 }
91
92 return getArrayIdentifierNameFromBinaryExpression(test, indexIdentifierName);
93};
94
95const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {
96 if (node && node.type === 'BinaryExpression' && node.operator === '+') {
97 return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right)) ||
98 (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left));
99 }
100
101 return false;
102};
103
104const checkUpdateExpression = (forStatement, indexIdentifierName) => {
105 const {update} = forStatement;
106
107 if (!update) {
108 return false;
109 }
110
111 if (update.type === 'UpdateExpression') {
112 return update.operator === '++' && isIdentifierWithName(update.argument, indexIdentifierName);
113 }
114
115 if (
116 update.type === 'AssignmentExpression' &&
117 isIdentifierWithName(update.left, indexIdentifierName)
118 ) {
119 if (update.operator === '+=') {
120 return isLiteralOne(update.right);
121 }
122
123 if (update.operator === '=') {
124 return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);
125 }
126 }
127
128 return false;
129};
130
131const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => {
132 return arrayReferences.every(reference => {
133 const node = reference.identifier.parent;
134
135 if (node.type !== 'MemberExpression') {
136 return false;
137 }
138
139 if (node.property.name !== indexIdentifierName) {
140 return false;
141 }
142
143 if (
144 node.parent.type === 'AssignmentExpression' &&
145 node.parent.left === node
146 ) {
147 return false;
148 }
149
150 return true;
151 });
152};
153
154const getRemovalRange = (node, sourceCode) => {
155 const declarationNode = node.parent;
156
157 if (declarationNode.declarations.length === 1) {
158 const {line} = sourceCode.getLocFromIndex(declarationNode.range[0]);
159 const lineText = sourceCode.lines[line - 1];
160
161 const isOnlyNodeOnLine = lineText.trim() === sourceCode.getText(declarationNode);
162
163 return isOnlyNodeOnLine ? [
164 sourceCode.getIndexFromLoc({line, column: 0}),
165 sourceCode.getIndexFromLoc({line: line + 1, column: 0})
166 ] : declarationNode.range;
167 }
168
169 const index = declarationNode.declarations.indexOf(node);
170
171 if (index === 0) {
172 return [
173 node.range[0],
174 declarationNode.declarations[1].range[0]
175 ];
176 }
177
178 return [
179 declarationNode.declarations[index - 1].range[1],
180 node.range[1]
181 ];
182};
183
184const resolveIdentifierName = (name, scope) => {
185 while (scope) {
186 const variable = scope.set.get(name);
187
188 if (variable) {
189 return variable;
190 }
191
192 scope = scope.upper;
193 }
194
195 return undefined;
196};
197
198const scopeContains = (ancestor, descendant) => {
199 while (descendant) {
200 if (descendant === ancestor) {
201 return true;
202 }
203
204 descendant = descendant.upper;
205 }
206
207 return false;
208};
209
210const nodeContains = (ancestor, descendant) => {
211 while (descendant) {
212 if (descendant === ancestor) {
213 return true;
214 }
215
216 descendant = descendant.parent;
217 }
218
219 return false;
220};
221
222const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arrayIdentifierName) => {
223 const inBodyReferences = indexVariable.references.filter(reference => scopeContains(bodyScope, reference.from));
224
225 const referencesOtherThanArrayAccess = inBodyReferences.filter(reference => {
226 const node = reference.identifier.parent;
227
228 if (node.type !== 'MemberExpression') {
229 return true;
230 }
231
232 if (node.object.name !== arrayIdentifierName) {
233 return true;
234 }
235
236 return false;
237 });
238
239 return referencesOtherThanArrayAccess.length > 0;
240};
241
242const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) => {
243 return indexVariable.references
244 .filter(reference => scopeContains(bodyScope, reference.from))
245 .some(inBodyReference => inBodyReference.isWrite());
246};
247
248const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) => {
249 return variables.some(variable => {
250 return !variable.references.every(reference => {
251 return scopeContains(forScope, reference.from) ||
252 nodeContains(forStatement, reference.identifier);
253 });
254 });
255};
256
257const getReferencesInChildScopes = (scope, name) => {
258 const references = scope.references.filter(reference => reference.identifier.name === name);
259 return [
260 ...references,
261 ...scope.childScopes
262 .map(s => getReferencesInChildScopes(s, name))
263 .reduce((accumulator, scopeReferences) => [...accumulator, ...scopeReferences], [])
264 ];
265};
266
267const create = context => {
268 const sourceCode = context.getSourceCode();
269 const {scopeManager} = sourceCode;
270
271 return {
272 ForStatement(node) {
273 const indexIdentifierName = getIndexIdentifierName(node);
274
275 if (!indexIdentifierName) {
276 return;
277 }
278
279 const arrayIdentifierName = getArrayIdentifierName(node, indexIdentifierName);
280
281 if (!arrayIdentifierName) {
282 return;
283 }
284
285 if (!checkUpdateExpression(node, indexIdentifierName)) {
286 return;
287 }
288
289 if (!node.body || node.body.type !== 'BlockStatement') {
290 return;
291 }
292
293 const forScope = scopeManager.acquire(node);
294 const bodyScope = scopeManager.acquire(node.body);
295
296 if (!bodyScope) {
297 return;
298 }
299
300 const indexVariable = resolveIdentifierName(indexIdentifierName, bodyScope);
301
302 if (isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {
303 return;
304 }
305
306 const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);
307
308 if (arrayReferences.length === 0) {
309 return;
310 }
311
312 if (!isOnlyArrayOfIndexVariableRead(arrayReferences, indexIdentifierName)) {
313 return;
314 }
315
316 const problem = {
317 node,
318 message: 'Use a `for-of` loop instead of this `for` loop.'
319 };
320
321 const elementReference = arrayReferences.find(reference => {
322 const node = reference.identifier.parent;
323
324 if (node.parent.type !== 'VariableDeclarator') {
325 return false;
326 }
327
328 return true;
329 });
330 const elementNode = elementReference && elementReference.identifier.parent.parent;
331 const elementIdentifierName = elementNode && elementNode.id.name;
332 const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);
333
334 const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope);
335
336 if (shouldFix) {
337 problem.fix = fixer => {
338 const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
339
340 const index = indexIdentifierName;
341 const element = elementIdentifierName || defaultElementName;
342 const array = arrayIdentifierName;
343
344 let declarationElement = element;
345 let declarationType = 'const';
346 let removeDeclaration = true;
347 if (
348 elementNode &&
349 (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern')
350 ) {
351 removeDeclaration = arrayReferences.length === 1;
352
353 if (removeDeclaration) {
354 declarationType = elementNode.parent.kind;
355 declarationElement = sourceCode.getText(elementNode.id);
356 }
357 }
358
359 const replacement = shouldGenerateIndex ?
360 `${declarationType} [${index}, ${declarationElement}] of ${array}.entries()` :
361 `${declarationType} ${declarationElement} of ${array}`;
362
363 return [
364 fixer.replaceTextRange([
365 node.init.range[0],
366 node.update.range[1]
367 ], replacement),
368 ...arrayReferences.map(reference => {
369 if (reference === elementReference) {
370 return undefined;
371 }
372
373 return fixer.replaceText(reference.identifier.parent, element);
374 }),
375 elementNode && (
376 removeDeclaration ?
377 fixer.removeRange(getRemovalRange(elementNode, sourceCode)) :
378 fixer.replaceText(elementNode.init, element)
379 )
380 ].filter(Boolean);
381 };
382 }
383
384 context.report(problem);
385 }
386 };
387};
388
389module.exports = {
390 create,
391 meta: {
392 type: 'suggestion',
393 docs: {
394 url: getDocumentationUrl(__filename)
395 },
396 fixable: 'code'
397 }
398};