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