no-unreachable-loop.js 5.02 KB
Newer Older
Rosanny Sihombing's avatar
Rosanny Sihombing committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
/**
 * @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"];

/**
 * 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
//------------------------------------------------------------------------------

module.exports = {
    meta: {
        type: "problem",

        docs: {
            description: "disallow loops with a body that allows only one iteration",
            category: "Possible Errors",
            recommended: false,
            url: "https://eslint.org/docs/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();

        let currentCodePath = null;

        return {
            onCodePathStart(codePath) {
                currentCodePath = codePath;
            },

            onCodePathEnd() {
                currentCodePath = currentCodePath.upper;
            },

            [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 (currentCodePath.currentSegments.some(segment => segment.reachable)) {
                    loopsToReport.add(node);
                }
            },

            onCodePathSegmentStart(segment, node) {
                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);
                }
            },

            "Program:exit"() {
                loopsToReport.forEach(
                    node => context.report({ node, messageId: "invalid" })
                );
            }
        };
    }
};