191 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			191 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
/**
 | 
						|
 * @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('../types').Rule.RuleModule} */
 | 
						|
module.exports = {
 | 
						|
	meta: {
 | 
						|
		type: "problem",
 | 
						|
 | 
						|
		defaultOptions: [{ ignore: [] }],
 | 
						|
 | 
						|
		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 [{ ignore: ignoredLoopTypes }] = context.options;
 | 
						|
		const 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" }),
 | 
						|
				);
 | 
						|
			},
 | 
						|
		};
 | 
						|
	},
 | 
						|
};
 |