Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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 | |
| 12 | const Traverser = require("../shared/traverser"), |
| 13 | astUtils = require("./utils/ast-utils"); |
| 14 | |
| 15 | //------------------------------------------------------------------------------ |
| 16 | // Helpers |
| 17 | //------------------------------------------------------------------------------ |
| 18 | |
| 19 | const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/u; |
| 20 | const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/u; // for-in/of statements don't have `test` property. |
| 21 | const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/u; |
| 22 | const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/u; |
| 23 | const 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 38 | * @param {eslint-scope.Reference} reference A reference to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 39 | * @returns {boolean} `true` if the reference is a write reference. |
| 40 | */ |
| 41 | function 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 55 | * @param {LoopConditionInfo} condition A loop condition info to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 56 | * @returns {boolean} `true` if the loop condition info is "unmodified". |
| 57 | */ |
| 58 | function 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 65 | * @param {LoopConditionInfo} condition A loop condition info to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 66 | * @returns {boolean} `true` if the loop condition info is "unmodified". |
| 67 | */ |
| 68 | function 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 74 | * @param {ASTNode} node A node to check. |
| 75 | * @param {eslint-scope.Reference} reference A reference to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 76 | * @returns {boolean} `true` if the reference is inside of the node. |
| 77 | */ |
| 78 | function 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 87 | * @param {ASTNode} node A node to check. |
| 88 | * @param {eslint-scope.Reference} reference A reference to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 89 | * @returns {boolean} `true` if the reference is inside of the loop node's |
| 90 | * condition. |
| 91 | */ |
| 92 | const 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 106 | * @param {eslint-scope.Reference} reference A reference to get. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 107 | * @returns {ASTNode|null} The function node or null. |
| 108 | */ |
| 109 | function 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 125 | * @param {LoopConditionInfo[]} conditions The loop conditions to be updated. |
| 126 | * @param {eslint-scope.Reference[]} modifiers The references to update. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 127 | * @returns {void} |
| 128 | */ |
| 129 | function 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 | |
Tim van der Lippe | 0ceb465 | 2022-01-06 14:23:36 +0100 | [diff] [blame^] | 159 | /** @type {import('../shared/types').Rule} */ |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 160 | module.exports = { |
| 161 | meta: { |
| 162 | type: "problem", |
| 163 | |
| 164 | docs: { |
| 165 | description: "disallow unmodified loop conditions", |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 166 | recommended: false, |
| 167 | url: "https://eslint.org/docs/rules/no-unmodified-loop-condition" |
| 168 | }, |
| 169 | |
Tim van der Lippe | 16aca39 | 2020-11-13 11:37:13 +0000 | [diff] [blame] | 170 | schema: [], |
| 171 | |
| 172 | messages: { |
| 173 | loopConditionNotModified: "'{{name}}' is not modified in this loop." |
| 174 | } |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 175 | }, |
| 176 | |
| 177 | create(context) { |
| 178 | const sourceCode = context.getSourceCode(); |
| 179 | let groupMap = null; |
| 180 | |
| 181 | /** |
| 182 | * Reports a given condition info. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 183 | * @param {LoopConditionInfo} condition A loop condition info to report. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 184 | * @returns {void} |
| 185 | */ |
| 186 | function report(condition) { |
| 187 | const node = condition.reference.identifier; |
| 188 | |
| 189 | context.report({ |
| 190 | node, |
Tim van der Lippe | 16aca39 | 2020-11-13 11:37:13 +0000 | [diff] [blame] | 191 | messageId: "loopConditionNotModified", |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 192 | data: node |
| 193 | }); |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * Registers given conditions to the group the condition belongs to. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 198 | * @param {LoopConditionInfo[]} conditions A loop condition info to |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 220 | * @param {LoopConditionInfo[]} conditions A loop condition info to report. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 231 | * @param {ASTNode} root A node to check. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 255 | * @param {eslint-scope.Reference} reference A reference to create. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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. |
Tim van der Lippe | c8f6ffd | 2020-04-06 13:42:00 +0100 | [diff] [blame] | 308 | * @param {eslint-scope.Variable} variable A variable to report. |
Yang Guo | 4fd355c | 2019-09-19 10:59:03 +0200 | [diff] [blame] | 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 | }; |