1 | /**
|
2 | * @fileoverview Rule to disallow loops with a body that allows only one iteration
|
3 | * @author Milos Djermanovic
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Helpers
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const 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 | */
|
20 | function 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 | */
|
48 | function getDifference(arrA, arrB) {
|
49 | return arrA.filter(a => !arrB.includes(a));
|
50 | }
|
51 |
|
52 | //------------------------------------------------------------------------------
|
53 | // Rule Definition
|
54 | //------------------------------------------------------------------------------
|
55 |
|
56 | module.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 | };
|