2 * @fileoverview Rule to disallow use of unmodified expressions in loop conditions
3 * @author Toru Nagashima
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
12 const Traverser = require("../shared/traverser"),
13 astUtils = require("./utils/ast-utils");
15 //------------------------------------------------------------------------------
17 //------------------------------------------------------------------------------
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;
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
32 * @property {boolean} modified - The flag that the reference is modified in
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.
41 function isWriteReference(reference) {
43 const def = reference.resolved && reference.resolved.defs[0];
45 if (!def || def.type !== "Variable" || def.parent.kind !== "var") {
49 return reference.isWrite();
53 * Checks whether or not a given loop condition info does not have the modified
55 * @param {LoopConditionInfo} condition A loop condition info to check.
56 * @returns {boolean} `true` if the loop condition info is "unmodified".
58 function isUnmodified(condition) {
59 return !condition.modified;
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".
68 function isUnmodifiedAndNotBelongToGroup(condition) {
69 return !(condition.modified || condition.group);
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.
78 function isInRange(node, reference) {
79 const or = node.range;
80 const ir = reference.identifier.range;
82 return or[0] <= ir[0] && ir[1] <= or[1];
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
93 WhileStatement: isInRange,
94 DoWhileStatement: isInRange,
95 ForStatement(node, reference) {
97 isInRange(node, reference) &&
98 !(node.init && isInRange(node.init, reference))
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.
109 function getEncloseFunctionDeclaration(reference) {
110 let node = reference.identifier;
113 if (node.type === "FunctionDeclaration") {
114 return node.id ? node : null;
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.
129 function updateModifiedFlag(conditions, modifiers) {
131 for (let i = 0; i < conditions.length; ++i) {
132 const condition = conditions[i];
134 for (let j = 0; !condition.modified && j < modifiers.length; ++j) {
135 const modifier = modifiers[j];
136 let funcNode, funcVar;
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
142 * FIXME: This should probably be extracted to a function.
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)
150 condition.modified = inLoop;
155 //------------------------------------------------------------------------------
157 //------------------------------------------------------------------------------
164 description: "disallow unmodified loop conditions",
165 category: "Best Practices",
167 url: "https://eslint.org/docs/rules/no-unmodified-loop-condition"
173 loopConditionNotModified: "'{{name}}' is not modified in this loop."
178 const sourceCode = context.getSourceCode();
182 * Reports a given condition info.
183 * @param {LoopConditionInfo} condition A loop condition info to report.
186 function report(condition) {
187 const node = condition.reference.identifier;
191 messageId: "loopConditionNotModified",
197 * Registers given conditions to the group the condition belongs to.
198 * @param {LoopConditionInfo[]} conditions A loop condition info to
202 function registerConditionsToGroup(conditions) {
203 for (let i = 0; i < conditions.length; ++i) {
204 const condition = conditions[i];
206 if (condition.group) {
207 let group = groupMap.get(condition.group);
211 groupMap.set(condition.group, group);
213 group.push(condition);
219 * Reports references which are inside of unmodified groups.
220 * @param {LoopConditionInfo[]} conditions A loop condition info to report.
223 function checkConditionsInGroup(conditions) {
224 if (conditions.every(isUnmodified)) {
225 conditions.forEach(report);
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.
235 function hasDynamicExpressions(root) {
238 Traverser.traverse(root, {
239 visitorKeys: sourceCode.visitorKeys,
241 if (DYNAMIC_PATTERN.test(node.type)) {
244 } else if (SKIP_PATTERN.test(node.type)) {
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.
258 function toLoopCondition(reference) {
259 if (reference.init) {
264 let child = reference.identifier;
265 let node = child.parent;
268 if (SENTINEL_PATTERN.test(node.type)) {
269 if (LOOP_PATTERN.test(node.type) && node.test === child) {
271 // This reference is inside of a loop condition.
275 isInLoop: isInLoop[node.type].bind(null, node),
280 // This reference is outside of a loop condition.
285 * If it's inside of a group, OK if either operand is modified.
286 * So stores the group this reference belongs to.
288 if (GROUP_PATTERN.test(node.type)) {
290 // If this expression is dynamic, no need to check.
291 if (hasDynamicExpressions(node)) {
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.
311 function checkReferences(variable) {
313 // Gets references that exist in loop conditions.
314 const conditions = variable
316 .map(toLoopCondition)
319 if (conditions.length === 0) {
323 // Registers the conditions to belonging groups.
324 registerConditionsToGroup(conditions);
326 // Check the conditions are modified.
327 const modifiers = variable.references.filter(isWriteReference);
329 if (modifiers.length > 0) {
330 updateModifiedFlag(conditions, modifiers);
334 * Reports the conditions which are not belonging to groups.
335 * Others will be reported after all variables are done.
338 .filter(isUnmodifiedAndNotBelongToGroup)
344 const queue = [context.getScope()];
346 groupMap = new Map();
350 while ((scope = queue.pop())) {
351 queue.push(...scope.childScopes);
352 scope.variables.forEach(checkReferences);
355 groupMap.forEach(checkConditionsInGroup);