no-unreachable-loop.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. /**
  2. * @fileoverview Rule to disallow loops with a body that allows only one iteration
  3. * @author Milos Djermanovic
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
  10. /**
  11. * Determines whether the given node is the first node in the code path to which a loop statement
  12. * 'loops' for the next iteration.
  13. * @param {ASTNode} node The node to check.
  14. * @returns {boolean} `true` if the node is a looping target.
  15. */
  16. function isLoopingTarget(node) {
  17. const parent = node.parent;
  18. if (parent) {
  19. switch (parent.type) {
  20. case "WhileStatement":
  21. return node === parent.test;
  22. case "DoWhileStatement":
  23. return node === parent.body;
  24. case "ForStatement":
  25. return node === (parent.update || parent.test || parent.body);
  26. case "ForInStatement":
  27. case "ForOfStatement":
  28. return node === parent.left;
  29. // no default
  30. }
  31. }
  32. return false;
  33. }
  34. /**
  35. * Creates an array with elements from the first given array that are not included in the second given array.
  36. * @param {Array} arrA The array to compare from.
  37. * @param {Array} arrB The array to compare against.
  38. * @returns {Array} a new array that represents `arrA \ arrB`.
  39. */
  40. function getDifference(arrA, arrB) {
  41. return arrA.filter(a => !arrB.includes(a));
  42. }
  43. //------------------------------------------------------------------------------
  44. // Rule Definition
  45. //------------------------------------------------------------------------------
  46. /** @type {import('../shared/types').Rule} */
  47. module.exports = {
  48. meta: {
  49. type: "problem",
  50. docs: {
  51. description: "Disallow loops with a body that allows only one iteration",
  52. recommended: false,
  53. url: "https://eslint.org/docs/rules/no-unreachable-loop"
  54. },
  55. schema: [{
  56. type: "object",
  57. properties: {
  58. ignore: {
  59. type: "array",
  60. items: {
  61. enum: allLoopTypes
  62. },
  63. uniqueItems: true
  64. }
  65. },
  66. additionalProperties: false
  67. }],
  68. messages: {
  69. invalid: "Invalid loop. Its body allows only one iteration."
  70. }
  71. },
  72. create(context) {
  73. const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [],
  74. loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes),
  75. loopSelector = loopTypesToCheck.join(","),
  76. loopsByTargetSegments = new Map(),
  77. loopsToReport = new Set();
  78. let currentCodePath = null;
  79. return {
  80. onCodePathStart(codePath) {
  81. currentCodePath = codePath;
  82. },
  83. onCodePathEnd() {
  84. currentCodePath = currentCodePath.upper;
  85. },
  86. [loopSelector](node) {
  87. /**
  88. * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise.
  89. * For unreachable segments, the code path analysis does not raise events required for this implementation.
  90. */
  91. if (currentCodePath.currentSegments.some(segment => segment.reachable)) {
  92. loopsToReport.add(node);
  93. }
  94. },
  95. onCodePathSegmentStart(segment, node) {
  96. if (isLoopingTarget(node)) {
  97. const loop = node.parent;
  98. loopsByTargetSegments.set(segment, loop);
  99. }
  100. },
  101. onCodePathSegmentLoop(_, toSegment, node) {
  102. const loop = loopsByTargetSegments.get(toSegment);
  103. /**
  104. * The second iteration is reachable, meaning that the loop is valid by the logic of this rule,
  105. * only if there is at least one loop event with the appropriate target (which has been already
  106. * determined in the `loopsByTargetSegments` map), raised from either:
  107. *
  108. * - the end of the loop's body (in which case `node === loop`)
  109. * - a `continue` statement
  110. *
  111. * This condition skips loop events raised from `ForInStatement > .right` and `ForOfStatement > .right` nodes.
  112. */
  113. if (node === loop || node.type === "ContinueStatement") {
  114. // Removes loop if it exists in the set. Otherwise, `Set#delete` has no effect and doesn't throw.
  115. loopsToReport.delete(loop);
  116. }
  117. },
  118. "Program:exit"() {
  119. loopsToReport.forEach(
  120. node => context.report({ node, messageId: "invalid" })
  121. );
  122. }
  123. };
  124. }
  125. };