UNPKG

5.14 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to disallow loops with a body that allows only one iteration
3 * @author Milos Djermanovic
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12const allLoopTypes = ["WhileStatement", "DoWhileStatement", "ForStatement", "ForInStatement", "ForOfStatement"];
13
14/**
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.
19 */
20function isLoopingTarget(node) {
21 const parent = node.parent;
22
23 if (parent) {
24 switch (parent.type) {
25 case "WhileStatement":
26 return node === parent.test;
27 case "DoWhileStatement":
28 return node === parent.body;
29 case "ForStatement":
30 return node === (parent.update || parent.test || parent.body);
31 case "ForInStatement":
32 case "ForOfStatement":
33 return node === parent.left;
34
35 // no default
36 }
37 }
38
39 return false;
40}
41
42/**
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`.
47 */
48function getDifference(arrA, arrB) {
49 return arrA.filter(a => !arrB.includes(a));
50}
51
52//------------------------------------------------------------------------------
53// Rule Definition
54//------------------------------------------------------------------------------
55
56module.exports = {
57 meta: {
58 type: "problem",
59
60 docs: {
61 description: "disallow loops with a body that allows only one iteration",
62 category: "Possible Errors",
63 recommended: false,
64 url: "https://eslint.org/docs/rules/no-unreachable-loop"
65 },
66
67 schema: [{
68 type: "object",
69 properties: {
70 ignore: {
71 type: "array",
72 items: {
73 enum: allLoopTypes
74 },
75 uniqueItems: true
76 }
77 },
78 additionalProperties: false
79 }],
80
81 messages: {
82 invalid: "Invalid loop. Its body allows only one iteration."
83 }
84 },
85
86 create(context) {
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();
92
93 let currentCodePath = null;
94
95 return {
96 onCodePathStart(codePath) {
97 currentCodePath = codePath;
98 },
99
100 onCodePathEnd() {
101 currentCodePath = currentCodePath.upper;
102 },
103
104 [loopSelector](node) {
105
106 /**
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.
109 */
110 if (currentCodePath.currentSegments.some(segment => segment.reachable)) {
111 loopsToReport.add(node);
112 }
113 },
114
115 onCodePathSegmentStart(segment, node) {
116 if (isLoopingTarget(node)) {
117 const loop = node.parent;
118
119 loopsByTargetSegments.set(segment, loop);
120 }
121 },
122
123 onCodePathSegmentLoop(_, toSegment, node) {
124 const loop = loopsByTargetSegments.get(toSegment);
125
126 /**
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:
130 *
131 * - the end of the loop's body (in which case `node === loop`)
132 * - a `continue` statement
133 *
134 * This condition skips loop events raised from `ForInStatement > .right` and `ForOfStatement > .right` nodes.
135 */
136 if (node === loop || node.type === "ContinueStatement") {
137
138 // Removes loop if it exists in the set. Otherwise, `Set#delete` has no effect and doesn't throw.
139 loopsToReport.delete(loop);
140 }
141 },
142
143 "Program:exit"() {
144 loopsToReport.forEach(
145 node => context.report({ node, messageId: "invalid" })
146 );
147 }
148 };
149 }
150};