| /** | |
|  * @fileoverview Rule to disallow loops with a body that allows only one iteration | |
|  * @author Milos Djermanovic | |
|  */ | |
| 
 | |
| "use strict"; | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Helpers | |
| //------------------------------------------------------------------------------ | |
|  | |
| const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"]; | |
| 
 | |
| /** | |
|  * Checks all segments in a set and returns true if any are reachable. | |
|  * @param {Set<CodePathSegment>} segments The segments to check. | |
|  * @returns {boolean} True if any segment is reachable; false otherwise. | |
|  */ | |
| function isAnySegmentReachable(segments) { | |
| 
 | |
|     for (const segment of segments) { | |
|         if (segment.reachable) { | |
|             return true; | |
|         } | |
|     } | |
| 
 | |
|     return false; | |
| } | |
| 
 | |
| /** | |
|  * Determines whether the given node is the first node in the code path to which a loop statement | |
|  * 'loops' for the next iteration. | |
|  * @param {ASTNode} node The node to check. | |
|  * @returns {boolean} `true` if the node is a looping target. | |
|  */ | |
| function isLoopingTarget(node) { | |
|     const parent = node.parent; | |
| 
 | |
|     if (parent) { | |
|         switch (parent.type) { | |
|             case "WhileStatement": | |
|                 return node === parent.test; | |
|             case "DoWhileStatement": | |
|                 return node === parent.body; | |
|             case "ForStatement": | |
|                 return node === (parent.update || parent.test || parent.body); | |
|             case "ForInStatement": | |
|             case "ForOfStatement": | |
|                 return node === parent.left; | |
| 
 | |
|             // no default | |
|         } | |
|     } | |
| 
 | |
|     return false; | |
| } | |
| 
 | |
| /** | |
|  * Creates an array with elements from the first given array that are not included in the second given array. | |
|  * @param {Array} arrA The array to compare from. | |
|  * @param {Array} arrB The array to compare against. | |
|  * @returns {Array} a new array that represents `arrA \ arrB`. | |
|  */ | |
| function getDifference(arrA, arrB) { | |
|     return arrA.filter(a => !arrB.includes(a)); | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow loops with a body that allows only one iteration", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/no-unreachable-loop" | |
|         }, | |
| 
 | |
|         schema: [{ | |
|             type: "object", | |
|             properties: { | |
|                 ignore: { | |
|                     type: "array", | |
|                     items: { | |
|                         enum: allLoopTypes | |
|                     }, | |
|                     uniqueItems: true | |
|                 } | |
|             }, | |
|             additionalProperties: false | |
|         }], | |
| 
 | |
|         messages: { | |
|             invalid: "Invalid loop. Its body allows only one iteration." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const ignoredLoopTypes = context.options[0] && context.options[0].ignore || [], | |
|             loopTypesToCheck = getDifference(allLoopTypes, ignoredLoopTypes), | |
|             loopSelector = loopTypesToCheck.join(","), | |
|             loopsByTargetSegments = new Map(), | |
|             loopsToReport = new Set(); | |
| 
 | |
|         const codePathSegments = []; | |
|         let currentCodePathSegments = new Set(); | |
| 
 | |
|         return { | |
| 
 | |
|             onCodePathStart() { | |
|                 codePathSegments.push(currentCodePathSegments); | |
|                 currentCodePathSegments = new Set(); | |
|             }, | |
| 
 | |
|             onCodePathEnd() { | |
|                 currentCodePathSegments = codePathSegments.pop(); | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentStart(segment) { | |
|                 currentCodePathSegments.add(segment); | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentEnd(segment) { | |
|                 currentCodePathSegments.delete(segment); | |
|             }, | |
| 
 | |
|             onCodePathSegmentEnd(segment) { | |
|                 currentCodePathSegments.delete(segment); | |
|             }, | |
| 
 | |
|             onCodePathSegmentStart(segment, node) { | |
| 
 | |
|                 currentCodePathSegments.add(segment); | |
| 
 | |
|                 if (isLoopingTarget(node)) { | |
|                     const loop = node.parent; | |
| 
 | |
|                     loopsByTargetSegments.set(segment, loop); | |
|                 } | |
|             }, | |
| 
 | |
|             onCodePathSegmentLoop(_, toSegment, node) { | |
|                 const loop = loopsByTargetSegments.get(toSegment); | |
| 
 | |
|                 /** | |
|                  * The second iteration is reachable, meaning that the loop is valid by the logic of this rule, | |
|                  * only if there is at least one loop event with the appropriate target (which has been already | |
|                  * determined in the `loopsByTargetSegments` map), raised from either: | |
|                  * | |
|                  * - the end of the loop's body (in which case `node === loop`) | |
|                  * - a `continue` statement | |
|                  * | |
|                  * This condition skips loop events raised from `ForInStatement > .right` and `ForOfStatement > .right` nodes. | |
|                  */ | |
|                 if (node === loop || node.type === "ContinueStatement") { | |
| 
 | |
|                     // Removes loop if it exists in the set. Otherwise, `Set#delete` has no effect and doesn't throw. | |
|                     loopsToReport.delete(loop); | |
|                 } | |
|             }, | |
| 
 | |
|             [loopSelector](node) { | |
| 
 | |
|                 /** | |
|                  * Ignore unreachable loop statements to avoid unnecessary complexity in the implementation, or false positives otherwise. | |
|                  * For unreachable segments, the code path analysis does not raise events required for this implementation. | |
|                  */ | |
|                 if (isAnySegmentReachable(currentCodePathSegments)) { | |
|                     loopsToReport.add(node); | |
|                 } | |
|             }, | |
| 
 | |
| 
 | |
|             "Program:exit"() { | |
|                 loopsToReport.forEach( | |
|                     node => context.report({ node, messageId: "invalid" }) | |
|                 ); | |
|             } | |
|         }; | |
|     } | |
| };
 |