1/** 2 * @fileoverview Rule to disallow use of unmodified expressions in loop conditions 3 * @author Toru Nagashima 4 */ 5 6"use strict"; 7 8//------------------------------------------------------------------------------ 9// Requirements 10//------------------------------------------------------------------------------ 11 12const Traverser = require("../shared/traverser"), 13 astUtils = require("./utils/ast-utils"); 14 15//------------------------------------------------------------------------------ 16// Helpers 17//------------------------------------------------------------------------------ 18 19const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/u; 20const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/u; // for-in/of statements don't have `test` property. 21const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/u; 22const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/u; 23const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/u; 24 25/** 26 * @typedef {Object} LoopConditionInfo 27 * @property {eslint-scope.Reference} reference - The reference. 28 * @property {ASTNode} group - BinaryExpression or ConditionalExpression nodes 29 * that the reference is belonging to. 30 * @property {Function} isInLoop - The predicate which checks a given reference 31 * is in this loop. 32 * @property {boolean} modified - The flag that the reference is modified in 33 * this loop. 34 */ 35 36/** 37 * Checks whether or not a given reference is a write reference. 38 * @param {eslint-scope.Reference} reference A reference to check. 39 * @returns {boolean} `true` if the reference is a write reference. 40 */ 41function isWriteReference(reference) { 42 if (reference.init) { 43 const def = reference.resolved && reference.resolved.defs[0]; 44 45 if (!def || def.type !== "Variable" || def.parent.kind !== "var") { 46 return false; 47 } 48 } 49 return reference.isWrite(); 50} 51 52/** 53 * Checks whether or not a given loop condition info does not have the modified 54 * flag. 55 * @param {LoopConditionInfo} condition A loop condition info to check. 56 * @returns {boolean} `true` if the loop condition info is "unmodified". 57 */ 58function isUnmodified(condition) { 59 return !condition.modified; 60} 61 62/** 63 * Checks whether or not a given loop condition info does not have the modified 64 * flag and does not have the group this condition belongs to. 65 * @param {LoopConditionInfo} condition A loop condition info to check. 66 * @returns {boolean} `true` if the loop condition info is "unmodified". 67 */ 68function isUnmodifiedAndNotBelongToGroup(condition) { 69 return !(condition.modified || condition.group); 70} 71 72/** 73 * Checks whether or not a given reference is inside of a given node. 74 * @param {ASTNode} node A node to check. 75 * @param {eslint-scope.Reference} reference A reference to check. 76 * @returns {boolean} `true` if the reference is inside of the node. 77 */ 78function isInRange(node, reference) { 79 const or = node.range; 80 const ir = reference.identifier.range; 81 82 return or[0] <= ir[0] && ir[1] <= or[1]; 83} 84 85/** 86 * Checks whether or not a given reference is inside of a loop node's condition. 87 * @param {ASTNode} node A node to check. 88 * @param {eslint-scope.Reference} reference A reference to check. 89 * @returns {boolean} `true` if the reference is inside of the loop node's 90 * condition. 91 */ 92const isInLoop = { 93 WhileStatement: isInRange, 94 DoWhileStatement: isInRange, 95 ForStatement(node, reference) { 96 return ( 97 isInRange(node, reference) && 98 !(node.init && isInRange(node.init, reference)) 99 ); 100 } 101}; 102 103/** 104 * Gets the function which encloses a given reference. 105 * This supports only FunctionDeclaration. 106 * @param {eslint-scope.Reference} reference A reference to get. 107 * @returns {ASTNode|null} The function node or null. 108 */ 109function getEncloseFunctionDeclaration(reference) { 110 let node = reference.identifier; 111 112 while (node) { 113 if (node.type === "FunctionDeclaration") { 114 return node.id ? node : null; 115 } 116 117 node = node.parent; 118 } 119 120 return null; 121} 122 123/** 124 * Updates the "modified" flags of given loop conditions with given modifiers. 125 * @param {LoopConditionInfo[]} conditions The loop conditions to be updated. 126 * @param {eslint-scope.Reference[]} modifiers The references to update. 127 * @returns {void} 128 */ 129function updateModifiedFlag(conditions, modifiers) { 130 131 for (let i = 0; i < conditions.length; ++i) { 132 const condition = conditions[i]; 133 134 for (let j = 0; !condition.modified && j < modifiers.length; ++j) { 135 const modifier = modifiers[j]; 136 let funcNode, funcVar; 137 138 /* 139 * Besides checking for the condition being in the loop, we want to 140 * check the function that this modifier is belonging to is called 141 * in the loop. 142 * FIXME: This should probably be extracted to a function. 143 */ 144 const inLoop = condition.isInLoop(modifier) || Boolean( 145 (funcNode = getEncloseFunctionDeclaration(modifier)) && 146 (funcVar = astUtils.getVariableByName(modifier.from.upper, funcNode.id.name)) && 147 funcVar.references.some(condition.isInLoop) 148 ); 149 150 condition.modified = inLoop; 151 } 152 } 153} 154 155//------------------------------------------------------------------------------ 156// Rule Definition 157//------------------------------------------------------------------------------ 158 159module.exports = { 160 meta: { 161 type: "problem", 162 163 docs: { 164 description: "disallow unmodified loop conditions", 165 category: "Best Practices", 166 recommended: false, 167 url: "https://eslint.org/docs/rules/no-unmodified-loop-condition" 168 }, 169 170 schema: [], 171 172 messages: { 173 loopConditionNotModified: "'{{name}}' is not modified in this loop." 174 } 175 }, 176 177 create(context) { 178 const sourceCode = context.getSourceCode(); 179 let groupMap = null; 180 181 /** 182 * Reports a given condition info. 183 * @param {LoopConditionInfo} condition A loop condition info to report. 184 * @returns {void} 185 */ 186 function report(condition) { 187 const node = condition.reference.identifier; 188 189 context.report({ 190 node, 191 messageId: "loopConditionNotModified", 192 data: node 193 }); 194 } 195 196 /** 197 * Registers given conditions to the group the condition belongs to. 198 * @param {LoopConditionInfo[]} conditions A loop condition info to 199 * register. 200 * @returns {void} 201 */ 202 function registerConditionsToGroup(conditions) { 203 for (let i = 0; i < conditions.length; ++i) { 204 const condition = conditions[i]; 205 206 if (condition.group) { 207 let group = groupMap.get(condition.group); 208 209 if (!group) { 210 group = []; 211 groupMap.set(condition.group, group); 212 } 213 group.push(condition); 214 } 215 } 216 } 217 218 /** 219 * Reports references which are inside of unmodified groups. 220 * @param {LoopConditionInfo[]} conditions A loop condition info to report. 221 * @returns {void} 222 */ 223 function checkConditionsInGroup(conditions) { 224 if (conditions.every(isUnmodified)) { 225 conditions.forEach(report); 226 } 227 } 228 229 /** 230 * Checks whether or not a given group node has any dynamic elements. 231 * @param {ASTNode} root A node to check. 232 * This node is one of BinaryExpression or ConditionalExpression. 233 * @returns {boolean} `true` if the node is dynamic. 234 */ 235 function hasDynamicExpressions(root) { 236 let retv = false; 237 238 Traverser.traverse(root, { 239 visitorKeys: sourceCode.visitorKeys, 240 enter(node) { 241 if (DYNAMIC_PATTERN.test(node.type)) { 242 retv = true; 243 this.break(); 244 } else if (SKIP_PATTERN.test(node.type)) { 245 this.skip(); 246 } 247 } 248 }); 249 250 return retv; 251 } 252 253 /** 254 * Creates the loop condition information from a given reference. 255 * @param {eslint-scope.Reference} reference A reference to create. 256 * @returns {LoopConditionInfo|null} Created loop condition info, or null. 257 */ 258 function toLoopCondition(reference) { 259 if (reference.init) { 260 return null; 261 } 262 263 let group = null; 264 let child = reference.identifier; 265 let node = child.parent; 266 267 while (node) { 268 if (SENTINEL_PATTERN.test(node.type)) { 269 if (LOOP_PATTERN.test(node.type) && node.test === child) { 270 271 // This reference is inside of a loop condition. 272 return { 273 reference, 274 group, 275 isInLoop: isInLoop[node.type].bind(null, node), 276 modified: false 277 }; 278 } 279 280 // This reference is outside of a loop condition. 281 break; 282 } 283 284 /* 285 * If it's inside of a group, OK if either operand is modified. 286 * So stores the group this reference belongs to. 287 */ 288 if (GROUP_PATTERN.test(node.type)) { 289 290 // If this expression is dynamic, no need to check. 291 if (hasDynamicExpressions(node)) { 292 break; 293 } else { 294 group = node; 295 } 296 } 297 298 child = node; 299 node = node.parent; 300 } 301 302 return null; 303 } 304 305 /** 306 * Finds unmodified references which are inside of a loop condition. 307 * Then reports the references which are outside of groups. 308 * @param {eslint-scope.Variable} variable A variable to report. 309 * @returns {void} 310 */ 311 function checkReferences(variable) { 312 313 // Gets references that exist in loop conditions. 314 const conditions = variable 315 .references 316 .map(toLoopCondition) 317 .filter(Boolean); 318 319 if (conditions.length === 0) { 320 return; 321 } 322 323 // Registers the conditions to belonging groups. 324 registerConditionsToGroup(conditions); 325 326 // Check the conditions are modified. 327 const modifiers = variable.references.filter(isWriteReference); 328 329 if (modifiers.length > 0) { 330 updateModifiedFlag(conditions, modifiers); 331 } 332 333 /* 334 * Reports the conditions which are not belonging to groups. 335 * Others will be reported after all variables are done. 336 */ 337 conditions 338 .filter(isUnmodifiedAndNotBelongToGroup) 339 .forEach(report); 340 } 341 342 return { 343 "Program:exit"() { 344 const queue = [context.getScope()]; 345 346 groupMap = new Map(); 347 348 let scope; 349 350 while ((scope = queue.pop())) { 351 queue.push(...scope.childScopes); 352 scope.variables.forEach(checkReferences); 353 } 354 355 groupMap.forEach(checkConditionsInGroup); 356 groupMap = null; 357 } 358 }; 359 } 360}; 361