no-for-loop.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. 'use strict';
  2. const {isClosingParenToken, getStaticValue} = require('@eslint-community/eslint-utils');
  3. const avoidCapture = require('./utils/avoid-capture.js');
  4. const getScopes = require('./utils/get-scopes.js');
  5. const singular = require('./utils/singular.js');
  6. const toLocation = require('./utils/to-location.js');
  7. const getReferences = require('./utils/get-references.js');
  8. const {isLiteral} = require('./ast/index.js');
  9. const MESSAGE_ID = 'no-for-loop';
  10. const messages = {
  11. [MESSAGE_ID]: 'Use a `for-of` loop instead of this `for` loop.',
  12. };
  13. const defaultElementName = 'element';
  14. const isLiteralZero = node => isLiteral(node, 0);
  15. const isLiteralOne = node => isLiteral(node, 1);
  16. const isIdentifierWithName = (node, name) => node?.type === 'Identifier' && node.name === name;
  17. const getIndexIdentifierName = forStatement => {
  18. const {init: variableDeclaration} = forStatement;
  19. if (
  20. !variableDeclaration
  21. || variableDeclaration.type !== 'VariableDeclaration'
  22. ) {
  23. return;
  24. }
  25. if (variableDeclaration.declarations.length !== 1) {
  26. return;
  27. }
  28. const [variableDeclarator] = variableDeclaration.declarations;
  29. if (!isLiteralZero(variableDeclarator.init)) {
  30. return;
  31. }
  32. if (variableDeclarator.id.type !== 'Identifier') {
  33. return;
  34. }
  35. return variableDeclarator.id.name;
  36. };
  37. const getStrictComparisonOperands = binaryExpression => {
  38. if (binaryExpression.operator === '<') {
  39. return {
  40. lesser: binaryExpression.left,
  41. greater: binaryExpression.right,
  42. };
  43. }
  44. if (binaryExpression.operator === '>') {
  45. return {
  46. lesser: binaryExpression.right,
  47. greater: binaryExpression.left,
  48. };
  49. }
  50. };
  51. const getArrayIdentifierFromBinaryExpression = (binaryExpression, indexIdentifierName) => {
  52. const operands = getStrictComparisonOperands(binaryExpression);
  53. if (!operands) {
  54. return;
  55. }
  56. const {lesser, greater} = operands;
  57. if (!isIdentifierWithName(lesser, indexIdentifierName)) {
  58. return;
  59. }
  60. if (greater.type !== 'MemberExpression') {
  61. return;
  62. }
  63. if (
  64. greater.object.type !== 'Identifier'
  65. || greater.property.type !== 'Identifier'
  66. ) {
  67. return;
  68. }
  69. if (greater.property.name !== 'length') {
  70. return;
  71. }
  72. return greater.object;
  73. };
  74. const getArrayIdentifier = (forStatement, indexIdentifierName) => {
  75. const {test} = forStatement;
  76. if (!test || test.type !== 'BinaryExpression') {
  77. return;
  78. }
  79. return getArrayIdentifierFromBinaryExpression(test, indexIdentifierName);
  80. };
  81. const isLiteralOnePlusIdentifierWithName = (node, identifierName) => {
  82. if (node?.type === 'BinaryExpression' && node.operator === '+') {
  83. return (isIdentifierWithName(node.left, identifierName) && isLiteralOne(node.right))
  84. || (isIdentifierWithName(node.right, identifierName) && isLiteralOne(node.left));
  85. }
  86. return false;
  87. };
  88. const checkUpdateExpression = (forStatement, indexIdentifierName) => {
  89. const {update} = forStatement;
  90. if (!update) {
  91. return false;
  92. }
  93. if (update.type === 'UpdateExpression') {
  94. return update.operator === '++' && isIdentifierWithName(update.argument, indexIdentifierName);
  95. }
  96. if (
  97. update.type === 'AssignmentExpression'
  98. && isIdentifierWithName(update.left, indexIdentifierName)
  99. ) {
  100. if (update.operator === '+=') {
  101. return isLiteralOne(update.right);
  102. }
  103. if (update.operator === '=') {
  104. return isLiteralOnePlusIdentifierWithName(update.right, indexIdentifierName);
  105. }
  106. }
  107. return false;
  108. };
  109. const isOnlyArrayOfIndexVariableRead = (arrayReferences, indexIdentifierName) => arrayReferences.every(reference => {
  110. const node = reference.identifier.parent;
  111. if (node.type !== 'MemberExpression') {
  112. return false;
  113. }
  114. if (node.property.name !== indexIdentifierName) {
  115. return false;
  116. }
  117. if (
  118. node.parent.type === 'AssignmentExpression'
  119. && node.parent.left === node
  120. ) {
  121. return false;
  122. }
  123. return true;
  124. });
  125. const getRemovalRange = (node, sourceCode) => {
  126. const declarationNode = node.parent;
  127. if (declarationNode.declarations.length === 1) {
  128. const {line} = declarationNode.loc.start;
  129. const lineText = sourceCode.lines[line - 1];
  130. const isOnlyNodeOnLine = lineText.trim() === sourceCode.getText(declarationNode);
  131. return isOnlyNodeOnLine ? [
  132. sourceCode.getIndexFromLoc({line, column: 0}),
  133. sourceCode.getIndexFromLoc({line: line + 1, column: 0}),
  134. ] : declarationNode.range;
  135. }
  136. const index = declarationNode.declarations.indexOf(node);
  137. if (index === 0) {
  138. return [
  139. node.range[0],
  140. declarationNode.declarations[1].range[0],
  141. ];
  142. }
  143. return [
  144. declarationNode.declarations[index - 1].range[1],
  145. node.range[1],
  146. ];
  147. };
  148. const resolveIdentifierName = (name, scope) => {
  149. while (scope) {
  150. const variable = scope.set.get(name);
  151. if (variable) {
  152. return variable;
  153. }
  154. scope = scope.upper;
  155. }
  156. };
  157. const scopeContains = (ancestor, descendant) => {
  158. while (descendant) {
  159. if (descendant === ancestor) {
  160. return true;
  161. }
  162. descendant = descendant.upper;
  163. }
  164. return false;
  165. };
  166. const nodeContains = (ancestor, descendant) => {
  167. while (descendant) {
  168. if (descendant === ancestor) {
  169. return true;
  170. }
  171. descendant = descendant.parent;
  172. }
  173. return false;
  174. };
  175. const isIndexVariableUsedElsewhereInTheLoopBody = (indexVariable, bodyScope, arrayIdentifierName) => {
  176. const inBodyReferences = indexVariable.references.filter(reference => scopeContains(bodyScope, reference.from));
  177. const referencesOtherThanArrayAccess = inBodyReferences.filter(reference => {
  178. const node = reference.identifier.parent;
  179. if (node.type !== 'MemberExpression') {
  180. return true;
  181. }
  182. if (node.object.name !== arrayIdentifierName) {
  183. return true;
  184. }
  185. return false;
  186. });
  187. return referencesOtherThanArrayAccess.length > 0;
  188. };
  189. const isIndexVariableAssignedToInTheLoopBody = (indexVariable, bodyScope) =>
  190. indexVariable.references
  191. .filter(reference => scopeContains(bodyScope, reference.from))
  192. .some(inBodyReference => inBodyReference.isWrite());
  193. const someVariablesLeakOutOfTheLoop = (forStatement, variables, forScope) =>
  194. variables.some(
  195. variable => !variable.references.every(
  196. reference => scopeContains(forScope, reference.from) || nodeContains(forStatement, reference.identifier),
  197. ),
  198. );
  199. const getReferencesInChildScopes = (scope, name) =>
  200. getReferences(scope).filter(reference => reference.identifier.name === name);
  201. /** @param {import('eslint').Rule.RuleContext} context */
  202. const create = context => {
  203. const sourceCode = context.getSourceCode();
  204. const {scopeManager, text: sourceCodeText} = sourceCode;
  205. return {
  206. ForStatement(node) {
  207. const indexIdentifierName = getIndexIdentifierName(node);
  208. if (!indexIdentifierName) {
  209. return;
  210. }
  211. const arrayIdentifier = getArrayIdentifier(node, indexIdentifierName);
  212. if (!arrayIdentifier) {
  213. return;
  214. }
  215. const arrayIdentifierName = arrayIdentifier.name;
  216. const scope = context.getScope();
  217. const staticResult = getStaticValue(arrayIdentifier, scope);
  218. if (staticResult && !Array.isArray(staticResult.value)) {
  219. // Bail out if we can tell that the array variable has a non-array value (i.e. we're looping through the characters of a string constant).
  220. return;
  221. }
  222. if (!checkUpdateExpression(node, indexIdentifierName)) {
  223. return;
  224. }
  225. if (!node.body || node.body.type !== 'BlockStatement') {
  226. return;
  227. }
  228. const forScope = scopeManager.acquire(node);
  229. const bodyScope = scopeManager.acquire(node.body);
  230. if (!bodyScope) {
  231. return;
  232. }
  233. const indexVariable = resolveIdentifierName(indexIdentifierName, bodyScope);
  234. if (isIndexVariableAssignedToInTheLoopBody(indexVariable, bodyScope)) {
  235. return;
  236. }
  237. const arrayReferences = getReferencesInChildScopes(bodyScope, arrayIdentifierName);
  238. if (arrayReferences.length === 0) {
  239. return;
  240. }
  241. if (!isOnlyArrayOfIndexVariableRead(arrayReferences, indexIdentifierName)) {
  242. return;
  243. }
  244. const [start] = node.range;
  245. const [, end] = sourceCode.getTokenBefore(node.body, isClosingParenToken).range;
  246. const problem = {
  247. loc: toLocation([start, end], sourceCode),
  248. messageId: MESSAGE_ID,
  249. };
  250. const elementReference = arrayReferences.find(reference => {
  251. const node = reference.identifier.parent;
  252. if (node.parent.type !== 'VariableDeclarator') {
  253. return false;
  254. }
  255. return true;
  256. });
  257. const elementNode = elementReference?.identifier.parent.parent;
  258. const elementIdentifierName = elementNode?.id.name;
  259. const elementVariable = elementIdentifierName && resolveIdentifierName(elementIdentifierName, bodyScope);
  260. const shouldFix = !someVariablesLeakOutOfTheLoop(node, [indexVariable, elementVariable].filter(Boolean), forScope);
  261. if (shouldFix) {
  262. problem.fix = function * (fixer) {
  263. const shouldGenerateIndex = isIndexVariableUsedElsewhereInTheLoopBody(indexVariable, bodyScope, arrayIdentifierName);
  264. const index = indexIdentifierName;
  265. const element = elementIdentifierName
  266. || avoidCapture(singular(arrayIdentifierName) || defaultElementName, getScopes(bodyScope));
  267. const array = arrayIdentifierName;
  268. let declarationElement = element;
  269. let declarationType = 'const';
  270. let removeDeclaration = true;
  271. let typeAnnotation;
  272. if (elementNode) {
  273. if (elementNode.id.type === 'ObjectPattern' || elementNode.id.type === 'ArrayPattern') {
  274. removeDeclaration = arrayReferences.length === 1;
  275. }
  276. if (removeDeclaration) {
  277. declarationType = element.type === 'VariableDeclarator' ? elementNode.kind : elementNode.parent.kind;
  278. if (elementNode.id.typeAnnotation && shouldGenerateIndex) {
  279. declarationElement = sourceCodeText.slice(elementNode.id.range[0], elementNode.id.typeAnnotation.range[0]).trim();
  280. typeAnnotation = sourceCode.getText(
  281. elementNode.id.typeAnnotation,
  282. -1, // Skip leading `:`
  283. ).trim();
  284. } else {
  285. declarationElement = sourceCode.getText(elementNode.id);
  286. }
  287. }
  288. }
  289. const parts = [declarationType];
  290. if (shouldGenerateIndex) {
  291. parts.push(` [${index}, ${declarationElement}]`);
  292. if (typeAnnotation) {
  293. parts.push(`: [number, ${typeAnnotation}]`);
  294. }
  295. parts.push(` of ${array}.entries()`);
  296. } else {
  297. parts.push(` ${declarationElement} of ${array}`);
  298. }
  299. const replacement = parts.join('');
  300. yield fixer.replaceTextRange([
  301. node.init.range[0],
  302. node.update.range[1],
  303. ], replacement);
  304. for (const reference of arrayReferences) {
  305. if (reference !== elementReference) {
  306. yield fixer.replaceText(reference.identifier.parent, element);
  307. }
  308. }
  309. if (elementNode) {
  310. yield removeDeclaration
  311. ? fixer.removeRange(getRemovalRange(elementNode, sourceCode))
  312. : fixer.replaceText(elementNode.init, element);
  313. }
  314. };
  315. }
  316. return problem;
  317. },
  318. };
  319. };
  320. /** @type {import('eslint').Rule.RuleModule} */
  321. module.exports = {
  322. create,
  323. meta: {
  324. type: 'suggestion',
  325. docs: {
  326. description: 'Do not use a `for` loop that can be replaced with a `for-of` loop.',
  327. },
  328. fixable: 'code',
  329. messages,
  330. hasSuggestion: true,
  331. },
  332. };