UNPKG

10.7 kBJavaScriptView Raw
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
12//------------------------------------------------------------------------------
13// Rule Definition
14//------------------------------------------------------------------------------
15
16module.exports = {
17 meta: {
18 type: "problem",
19
20 docs: {
21 description: "disallow constant expressions in conditions",
22 category: "Possible Errors",
23 recommended: true,
24 url: "https://eslint.org/docs/rules/no-constant-condition"
25 },
26
27 schema: [
28 {
29 type: "object",
30 properties: {
31 checkLoops: {
32 type: "boolean",
33 default: true
34 }
35 },
36 additionalProperties: false
37 }
38 ],
39
40 messages: {
41 unexpected: "Unexpected constant condition."
42 }
43 },
44
45 create(context) {
46 const options = context.options[0] || {},
47 checkLoops = options.checkLoops !== false,
48 loopSetStack = [];
49
50 let loopsInCurrentScope = new Set();
51
52 //--------------------------------------------------------------------------
53 // Helpers
54 //--------------------------------------------------------------------------
55
56 /**
57 * Returns literal's value converted to the Boolean type
58 * @param {ASTNode} node any `Literal` node
59 * @returns {boolean | null} `true` when node is truthy, `false` when node is falsy,
60 * `null` when it cannot be determined.
61 */
62 function getBooleanValue(node) {
63 if (node.value === null) {
64
65 /*
66 * it might be a null literal or bigint/regex literal in unsupported environments .
67 * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es5.md#regexpliteral
68 * https://github.com/estree/estree/blob/14df8a024956ea289bd55b9c2226a1d5b8a473ee/es2020.md#bigintliteral
69 */
70
71 if (node.raw === "null") {
72 return false;
73 }
74
75 // regex is always truthy
76 if (typeof node.regex === "object") {
77 return true;
78 }
79
80 return null;
81 }
82
83 return !!node.value;
84 }
85
86 /**
87 * Checks if a branch node of LogicalExpression short circuits the whole condition
88 * @param {ASTNode} node The branch of main condition which needs to be checked
89 * @param {string} operator The operator of the main LogicalExpression.
90 * @returns {boolean} true when condition short circuits whole condition
91 */
92 function isLogicalIdentity(node, operator) {
93 switch (node.type) {
94 case "Literal":
95 return (operator === "||" && getBooleanValue(node) === true) ||
96 (operator === "&&" && getBooleanValue(node) === false);
97
98 case "UnaryExpression":
99 return (operator === "&&" && node.operator === "void");
100
101 case "LogicalExpression":
102
103 /*
104 * handles `a && false || b`
105 * `false` is an identity element of `&&` but not `||`
106 */
107 return operator === node.operator &&
108 (
109 isLogicalIdentity(node.left, operator) ||
110 isLogicalIdentity(node.right, operator)
111 );
112
113 case "AssignmentExpression":
114 return ["||=", "&&="].includes(node.operator) &&
115 operator === node.operator.slice(0, -1) &&
116 isLogicalIdentity(node.right, operator);
117
118 // no default
119 }
120 return false;
121 }
122
123 /**
124 * Checks if a node has a constant truthiness value.
125 * @param {ASTNode} node The AST node to check.
126 * @param {boolean} inBooleanPosition `false` if checking branch of a condition.
127 * `true` in all other cases
128 * @returns {Bool} true when node's truthiness is constant
129 * @private
130 */
131 function isConstant(node, inBooleanPosition) {
132
133 // node.elements can return null values in the case of sparse arrays ex. [,]
134 if (!node) {
135 return true;
136 }
137 switch (node.type) {
138 case "Literal":
139 case "ArrowFunctionExpression":
140 case "FunctionExpression":
141 case "ObjectExpression":
142 return true;
143 case "TemplateLiteral":
144 return (inBooleanPosition && node.quasis.some(quasi => quasi.value.cooked.length)) ||
145 node.expressions.every(exp => isConstant(exp, inBooleanPosition));
146
147 case "ArrayExpression": {
148 if (node.parent.type === "BinaryExpression" && node.parent.operator === "+") {
149 return node.elements.every(element => isConstant(element, false));
150 }
151 return true;
152 }
153
154 case "UnaryExpression":
155 if (
156 node.operator === "void" ||
157 node.operator === "typeof" && inBooleanPosition
158 ) {
159 return true;
160 }
161
162 if (node.operator === "!") {
163 return isConstant(node.argument, true);
164 }
165
166 return isConstant(node.argument, false);
167
168 case "BinaryExpression":
169 return isConstant(node.left, false) &&
170 isConstant(node.right, false) &&
171 node.operator !== "in";
172
173 case "LogicalExpression": {
174 const isLeftConstant = isConstant(node.left, inBooleanPosition);
175 const isRightConstant = isConstant(node.right, inBooleanPosition);
176 const isLeftShortCircuit = (isLeftConstant && isLogicalIdentity(node.left, node.operator));
177 const isRightShortCircuit = (inBooleanPosition && isRightConstant && isLogicalIdentity(node.right, node.operator));
178
179 return (isLeftConstant && isRightConstant) ||
180 isLeftShortCircuit ||
181 isRightShortCircuit;
182 }
183
184 case "AssignmentExpression":
185 if (node.operator === "=") {
186 return isConstant(node.right, inBooleanPosition);
187 }
188
189 if (["||=", "&&="].includes(node.operator) && inBooleanPosition) {
190 return isLogicalIdentity(node.right, node.operator.slice(0, -1));
191 }
192
193 return false;
194
195 case "SequenceExpression":
196 return isConstant(node.expressions[node.expressions.length - 1], inBooleanPosition);
197
198 // no default
199 }
200 return false;
201 }
202
203 /**
204 * Tracks when the given node contains a constant condition.
205 * @param {ASTNode} node The AST node to check.
206 * @returns {void}
207 * @private
208 */
209 function trackConstantConditionLoop(node) {
210 if (node.test && isConstant(node.test, true)) {
211 loopsInCurrentScope.add(node);
212 }
213 }
214
215 /**
216 * Reports when the set contains the given constant condition node
217 * @param {ASTNode} node The AST node to check.
218 * @returns {void}
219 * @private
220 */
221 function checkConstantConditionLoopInSet(node) {
222 if (loopsInCurrentScope.has(node)) {
223 loopsInCurrentScope.delete(node);
224 context.report({ node: node.test, messageId: "unexpected" });
225 }
226 }
227
228 /**
229 * Reports when the given node contains a constant condition.
230 * @param {ASTNode} node The AST node to check.
231 * @returns {void}
232 * @private
233 */
234 function reportIfConstant(node) {
235 if (node.test && isConstant(node.test, true)) {
236 context.report({ node: node.test, messageId: "unexpected" });
237 }
238 }
239
240 /**
241 * Stores current set of constant loops in loopSetStack temporarily
242 * and uses a new set to track constant loops
243 * @returns {void}
244 * @private
245 */
246 function enterFunction() {
247 loopSetStack.push(loopsInCurrentScope);
248 loopsInCurrentScope = new Set();
249 }
250
251 /**
252 * Reports when the set still contains stored constant conditions
253 * @returns {void}
254 * @private
255 */
256 function exitFunction() {
257 loopsInCurrentScope = loopSetStack.pop();
258 }
259
260 /**
261 * Checks node when checkLoops option is enabled
262 * @param {ASTNode} node The AST node to check.
263 * @returns {void}
264 * @private
265 */
266 function checkLoop(node) {
267 if (checkLoops) {
268 trackConstantConditionLoop(node);
269 }
270 }
271
272 //--------------------------------------------------------------------------
273 // Public
274 //--------------------------------------------------------------------------
275
276 return {
277 ConditionalExpression: reportIfConstant,
278 IfStatement: reportIfConstant,
279 WhileStatement: checkLoop,
280 "WhileStatement:exit": checkConstantConditionLoopInSet,
281 DoWhileStatement: checkLoop,
282 "DoWhileStatement:exit": checkConstantConditionLoopInSet,
283 ForStatement: checkLoop,
284 "ForStatement > .test": node => checkLoop(node.parent),
285 "ForStatement:exit": checkConstantConditionLoopInSet,
286 FunctionDeclaration: enterFunction,
287 "FunctionDeclaration:exit": exitFunction,
288 FunctionExpression: enterFunction,
289 "FunctionExpression:exit": exitFunction,
290 YieldExpression: () => loopsInCurrentScope.clear()
291 };
292
293 }
294};