2 * @fileoverview Rule to disallow loops with a body that allows only one iteration
3 * @author Milos Djermanovic
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
15 * Determines whether the given node is the first node in the code path to which a loop statement
16 * 'loops' for the next iteration.
17 * @param {ASTNode} node The node to check.
18 * @returns {boolean} `true` if the node is a looping target.
20 function isLoopingTarget(node) {
21 const parent = node.parent;
24 switch (parent.type) {
25 case "WhileStatement":
26 return node === parent.test;
27 case "DoWhileStatement":
28 return node === parent.body;
30 return node === (parent.update || parent.test || parent.body);
31 case "ForInStatement":
32 case "ForOfStatement":
33 return node === parent.left;
43 * Creates an array with elements from the first given array that are not included in the second given array.
44 * @param {Array} arrA The array to compare from.
45 * @param {Array} arrB The array to compare against.
46 * @returns {Array} a new array that represents `arrA \ arrB`.
48 function getDifference(arrA, arrB) {
49 return arrA.filter(a => !arrB.includes(a));
52 //------------------------------------------------------------------------------
54 //------------------------------------------------------------------------------
61 description: "disallow loops with a body that allows only one iteration",
62 category: "Possible Errors",
64 url: "https://eslint.org/docs/rules/no-unreachable-loop"
78 additionalProperties: false
82 invalid: "Invalid loop. Its body allows only one iteration."
87 const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [],
88 loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes),
89 loopSelector = loopTypesToCheck.join(","),
90 loopsByTargetSegments = new Map(),
91 loopsToReport = new Set();
93 let currentCodePath = null;
96 onCodePathStart(codePath) {
97 currentCodePath = codePath;
101 currentCodePath = currentCodePath.upper;
104 [loopSelector](node) {
107 * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise.
108 * For unreachable segments, the code path analysis does not raise events required for this implementation.
110 if (currentCodePath.currentSegments.some(segment => segment.reachable)) {
111 loopsToReport.add(node);
115 onCodePathSegmentStart(segment, node) {
116 if (isLoopingTarget(node)) {
117 const loop = node.parent;
119 loopsByTargetSegments.set(segment, loop);
123 onCodePathSegmentLoop(_, toSegment, node) {
124 const loop = loopsByTargetSegments.get(toSegment);
127 * The second iteration is reachable, meaning that the loop is valid by the logic of this rule,
128 * only if there is at least one loop event with the appropriate target (which has been already
129 * determined in the `loopsByTargetSegments` map), raised from either:
131 * - the end of the loop's body (in which case `node === loop`)
132 * - a `continue` statement
134 * This condition skips loop events raised from `ForInStatement > .right` and `ForOfStatement > .right` nodes.
136 if (node === loop || node.type === "ContinueStatement") {
138 // Removes loop if it exists in the set. Otherwise, `Set#delete` has no effect and doesn't throw.
139 loopsToReport.delete(loop);
144 loopsToReport.forEach(
145 node => context.report({ node, messageId: "invalid" })