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"
174 const sourceCode = context.getSourceCode();
178 * Reports a given condition info.
179 * @param {LoopConditionInfo} condition A loop condition info to report.
182 function report(condition) {
183 const node = condition.reference.identifier;
187 message: "'{{name}}' is not modified in this loop.",
193 * Registers given conditions to the group the condition belongs to.
194 * @param {LoopConditionInfo[]} conditions A loop condition info to
198 function registerConditionsToGroup(conditions) {
199 for (let i = 0; i < conditions.length; ++i) {
200 const condition = conditions[i];
202 if (condition.group) {
203 let group = groupMap.get(condition.group);
207 groupMap.set(condition.group, group);
209 group.push(condition);
215 * Reports references which are inside of unmodified groups.
216 * @param {LoopConditionInfo[]} conditions A loop condition info to report.
219 function checkConditionsInGroup(conditions) {
220 if (conditions.every(isUnmodified)) {
221 conditions.forEach(report);
226 * Checks whether or not a given group node has any dynamic elements.
227 * @param {ASTNode} root A node to check.
228 * This node is one of BinaryExpression or ConditionalExpression.
229 * @returns {boolean} `true` if the node is dynamic.
231 function hasDynamicExpressions(root) {
234 Traverser.traverse(root, {
235 visitorKeys: sourceCode.visitorKeys,
237 if (DYNAMIC_PATTERN.test(node.type)) {
240 } else if (SKIP_PATTERN.test(node.type)) {
250 * Creates the loop condition information from a given reference.
251 * @param {eslint-scope.Reference} reference A reference to create.
252 * @returns {LoopConditionInfo|null} Created loop condition info, or null.
254 function toLoopCondition(reference) {
255 if (reference.init) {
260 let child = reference.identifier;
261 let node = child.parent;
264 if (SENTINEL_PATTERN.test(node.type)) {
265 if (LOOP_PATTERN.test(node.type) && node.test === child) {
267 // This reference is inside of a loop condition.
271 isInLoop: isInLoop[node.type].bind(null, node),
276 // This reference is outside of a loop condition.
281 * If it's inside of a group, OK if either operand is modified.
282 * So stores the group this reference belongs to.
284 if (GROUP_PATTERN.test(node.type)) {
286 // If this expression is dynamic, no need to check.
287 if (hasDynamicExpressions(node)) {
302 * Finds unmodified references which are inside of a loop condition.
303 * Then reports the references which are outside of groups.
304 * @param {eslint-scope.Variable} variable A variable to report.
307 function checkReferences(variable) {
309 // Gets references that exist in loop conditions.
310 const conditions = variable
312 .map(toLoopCondition)
315 if (conditions.length === 0) {
319 // Registers the conditions to belonging groups.
320 registerConditionsToGroup(conditions);
322 // Check the conditions are modified.
323 const modifiers = variable.references.filter(isWriteReference);
325 if (modifiers.length > 0) {
326 updateModifiedFlag(conditions, modifiers);
330 * Reports the conditions which are not belonging to groups.
331 * Others will be reported after all variables are done.
334 .filter(isUnmodifiedAndNotBelongToGroup)
340 const queue = [context.getScope()];
342 groupMap = new Map();
346 while ((scope = queue.pop())) {
347 queue.push(...scope.childScopes);
348 scope.variables.forEach(checkReferences);
351 groupMap.forEach(checkConditionsInGroup);