| /** |
| * @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" }), |
| ); |
| }, |
| }; |
| }, |
| }; |