• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * @fileoverview Rule to flag use constant conditions
3 * @author Christian Schulz <http://rndm.de>
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12const EQUALITY_OPERATORS = ["===", "!==", "==", "!="];
13const RELATIONAL_OPERATORS = [">", "<", ">=", "<=", "in", "instanceof"];
14
15//------------------------------------------------------------------------------
16// Rule Definition
17//------------------------------------------------------------------------------
18
19module.exports = {
20    meta: {
21        type: "problem",
22
23        docs: {
24            description: "disallow constant expressions in conditions",
25            category: "Possible Errors",
26            recommended: true,
27            url: "https://eslint.org/docs/rules/no-constant-condition"
28        },
29
30        schema: [
31            {
32                type: "object",
33                properties: {
34                    checkLoops: {
35                        type: "boolean",
36                        default: true
37                    }
38                },
39                additionalProperties: false
40            }
41        ],
42
43        messages: {
44            unexpected: "Unexpected constant condition."
45        }
46    },
47
48    create(context) {
49        const options = context.options[0] || {},
50            checkLoops = options.checkLoops !== false,
51            loopSetStack = [];
52
53        let loopsInCurrentScope = new Set();
54
55        //--------------------------------------------------------------------------
56        // Helpers
57        //--------------------------------------------------------------------------
58
59
60        /**
61         * Checks if a branch node of LogicalExpression short circuits the whole condition
62         * @param {ASTNode} node The branch of main condition which needs to be checked
63         * @param {string} operator The operator of the main LogicalExpression.
64         * @returns {boolean} true when condition short circuits whole condition
65         */
66        function isLogicalIdentity(node, operator) {
67            switch (node.type) {
68                case "Literal":
69                    return (operator === "||" && node.value === true) ||
70                           (operator === "&&" && node.value === false);
71
72                case "UnaryExpression":
73                    return (operator === "&&" && node.operator === "void");
74
75                case "LogicalExpression":
76                    return isLogicalIdentity(node.left, node.operator) ||
77                             isLogicalIdentity(node.right, node.operator);
78
79                // no default
80            }
81            return false;
82        }
83
84        /**
85         * Checks if a node has a constant truthiness value.
86         * @param {ASTNode} node The AST node to check.
87         * @param {boolean} inBooleanPosition `false` if checking branch of a condition.
88         *  `true` in all other cases
89         * @returns {Bool} true when node's truthiness is constant
90         * @private
91         */
92        function isConstant(node, inBooleanPosition) {
93
94            // node.elements can return null values in the case of sparse arrays ex. [,]
95            if (!node) {
96                return true;
97            }
98            switch (node.type) {
99                case "Literal":
100                case "ArrowFunctionExpression":
101                case "FunctionExpression":
102                case "ObjectExpression":
103                    return true;
104                case "TemplateLiteral":
105                    return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
106                        node.expressions.every(exp => isConstant(exp, inBooleanPosition));
107
108                case "ArrayExpression": {
109                    if (node.parent.type === "BinaryExpression" && node.parent.operator === "+") {
110                        return node.elements.every(element => isConstant(element, false));
111                    }
112                    return true;
113                }
114
115                case "UnaryExpression":
116                    if (node.operator === "void") {
117                        return true;
118                    }
119
120                    return (node.operator === "typeof" && inBooleanPosition) ||
121                        isConstant(node.argument, true);
122
123                case "BinaryExpression":
124                    return isConstant(node.left, false) &&
125                            isConstant(node.right, false) &&
126                            node.operator !== "in";
127
128                case "LogicalExpression": {
129                    const isLeftConstant = isConstant(node.left, inBooleanPosition);
130                    const isRightConstant = isConstant(node.right, inBooleanPosition);
131                    const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
132                    const isRightShortCircuit = (isRightConstant && isLogicalIdentity(node.right, node.operator));
133
134                    return (isLeftConstant && isRightConstant) ||
135                        (
136
137                            // in the case of an "OR", we need to know if the right constant value is truthy
138                            node.operator === "||" &&
139                            isRightConstant &&
140                            node.right.value &&
141                            (
142                                !node.parent ||
143                                node.parent.type !== "BinaryExpression" ||
144                                !(EQUALITY_OPERATORS.includes(node.parent.operator) || RELATIONAL_OPERATORS.includes(node.parent.operator))
145                            )
146                        ) ||
147                        isLeftShortCircuit ||
148                        isRightShortCircuit;
149                }
150
151                case "AssignmentExpression":
152                    return (node.operator === "=") && isConstant(node.right, inBooleanPosition);
153
154                case "SequenceExpression":
155                    return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
156
157                // no default
158            }
159            return false;
160        }
161
162        /**
163         * Tracks when the given node contains a constant condition.
164         * @param {ASTNode} node The AST node to check.
165         * @returns {void}
166         * @private
167         */
168        function trackConstantConditionLoop(node) {
169            if (node.test && isConstant(node.test, true)) {
170                loopsInCurrentScope.add(node);
171            }
172        }
173
174        /**
175         * Reports when the set contains the given constant condition node
176         * @param {ASTNode} node The AST node to check.
177         * @returns {void}
178         * @private
179         */
180        function checkConstantConditionLoopInSet(node) {
181            if (loopsInCurrentScope.has(node)) {
182                loopsInCurrentScope.delete(node);
183                context.report({ node: node.test, messageId: "unexpected" });
184            }
185        }
186
187        /**
188         * Reports when the given node contains a constant condition.
189         * @param {ASTNode} node The AST node to check.
190         * @returns {void}
191         * @private
192         */
193        function reportIfConstant(node) {
194            if (node.test && isConstant(node.test, true)) {
195                context.report({ node: node.test, messageId: "unexpected" });
196            }
197        }
198
199        /**
200         * Stores current set of constant loops in loopSetStack temporarily
201         * and uses a new set to track constant loops
202         * @returns {void}
203         * @private
204         */
205        function enterFunction() {
206            loopSetStack.push(loopsInCurrentScope);
207            loopsInCurrentScope = new Set();
208        }
209
210        /**
211         * Reports when the set still contains stored constant conditions
212         * @returns {void}
213         * @private
214         */
215        function exitFunction() {
216            loopsInCurrentScope = loopSetStack.pop();
217        }
218
219        /**
220         * Checks node when checkLoops option is enabled
221         * @param {ASTNode} node The AST node to check.
222         * @returns {void}
223         * @private
224         */
225        function checkLoop(node) {
226            if (checkLoops) {
227                trackConstantConditionLoop(node);
228            }
229        }
230
231        //--------------------------------------------------------------------------
232        // Public
233        //--------------------------------------------------------------------------
234
235        return {
236            ConditionalExpression: reportIfConstant,
237            IfStatement: reportIfConstant,
238            WhileStatement: checkLoop,
239            "WhileStatement:exit": checkConstantConditionLoopInSet,
240            DoWhileStatement: checkLoop,
241            "DoWhileStatement:exit": checkConstantConditionLoopInSet,
242            ForStatement: checkLoop,
243            "ForStatement > .test": node => checkLoop(node.parent),
244            "ForStatement:exit": checkConstantConditionLoopInSet,
245            FunctionDeclaration: enterFunction,
246            "FunctionDeclaration:exit": exitFunction,
247            FunctionExpression: enterFunction,
248            "FunctionExpression:exit": exitFunction,
249            YieldExpression: () => loopsInCurrentScope.clear()
250        };
251
252    }
253};
254