186 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			186 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * @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" })
 | |
|                 );
 | |
|             }
 | |
|         };
 | |
|     }
 | |
| };
 |