UNPKG

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