blob: 3b8e7417d5b58632a63c47faf7cb7caacf2ada8d [file] [log] [blame]
Yang Guo4fd355c2019-09-19 10:59:03 +02001/**
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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010038 * @param {eslint-scope.Reference} reference A reference to check.
Yang Guo4fd355c2019-09-19 10:59:03 +020039 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010055 * @param {LoopConditionInfo} condition A loop condition info to check.
Yang Guo4fd355c2019-09-19 10:59:03 +020056 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010065 * @param {LoopConditionInfo} condition A loop condition info to check.
Yang Guo4fd355c2019-09-19 10:59:03 +020066 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010074 * @param {ASTNode} node A node to check.
75 * @param {eslint-scope.Reference} reference A reference to check.
Yang Guo4fd355c2019-09-19 10:59:03 +020076 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +010087 * @param {ASTNode} node A node to check.
88 * @param {eslint-scope.Reference} reference A reference to check.
Yang Guo4fd355c2019-09-19 10:59:03 +020089 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100106 * @param {eslint-scope.Reference} reference A reference to get.
Yang Guo4fd355c2019-09-19 10:59:03 +0200107 * @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.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100125 * @param {LoopConditionInfo[]} conditions The loop conditions to be updated.
126 * @param {eslint-scope.Reference[]} modifiers The references to update.
Yang Guo4fd355c2019-09-19 10:59:03 +0200127 * @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
173 create(context) {
174 const sourceCode = context.getSourceCode();
175 let groupMap = null;
176
177 /**
178 * Reports a given condition info.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100179 * @param {LoopConditionInfo} condition A loop condition info to report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200180 * @returns {void}
181 */
182 function report(condition) {
183 const node = condition.reference.identifier;
184
185 context.report({
186 node,
187 message: "'{{name}}' is not modified in this loop.",
188 data: node
189 });
190 }
191
192 /**
193 * Registers given conditions to the group the condition belongs to.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100194 * @param {LoopConditionInfo[]} conditions A loop condition info to
Yang Guo4fd355c2019-09-19 10:59:03 +0200195 * register.
196 * @returns {void}
197 */
198 function registerConditionsToGroup(conditions) {
199 for (let i = 0; i < conditions.length; ++i) {
200 const condition = conditions[i];
201
202 if (condition.group) {
203 let group = groupMap.get(condition.group);
204
205 if (!group) {
206 group = [];
207 groupMap.set(condition.group, group);
208 }
209 group.push(condition);
210 }
211 }
212 }
213
214 /**
215 * Reports references which are inside of unmodified groups.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100216 * @param {LoopConditionInfo[]} conditions A loop condition info to report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200217 * @returns {void}
218 */
219 function checkConditionsInGroup(conditions) {
220 if (conditions.every(isUnmodified)) {
221 conditions.forEach(report);
222 }
223 }
224
225 /**
226 * Checks whether or not a given group node has any dynamic elements.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100227 * @param {ASTNode} root A node to check.
Yang Guo4fd355c2019-09-19 10:59:03 +0200228 * This node is one of BinaryExpression or ConditionalExpression.
229 * @returns {boolean} `true` if the node is dynamic.
230 */
231 function hasDynamicExpressions(root) {
232 let retv = false;
233
234 Traverser.traverse(root, {
235 visitorKeys: sourceCode.visitorKeys,
236 enter(node) {
237 if (DYNAMIC_PATTERN.test(node.type)) {
238 retv = true;
239 this.break();
240 } else if (SKIP_PATTERN.test(node.type)) {
241 this.skip();
242 }
243 }
244 });
245
246 return retv;
247 }
248
249 /**
250 * Creates the loop condition information from a given reference.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100251 * @param {eslint-scope.Reference} reference A reference to create.
Yang Guo4fd355c2019-09-19 10:59:03 +0200252 * @returns {LoopConditionInfo|null} Created loop condition info, or null.
253 */
254 function toLoopCondition(reference) {
255 if (reference.init) {
256 return null;
257 }
258
259 let group = null;
260 let child = reference.identifier;
261 let node = child.parent;
262
263 while (node) {
264 if (SENTINEL_PATTERN.test(node.type)) {
265 if (LOOP_PATTERN.test(node.type) && node.test === child) {
266
267 // This reference is inside of a loop condition.
268 return {
269 reference,
270 group,
271 isInLoop: isInLoop[node.type].bind(null, node),
272 modified: false
273 };
274 }
275
276 // This reference is outside of a loop condition.
277 break;
278 }
279
280 /*
281 * If it's inside of a group, OK if either operand is modified.
282 * So stores the group this reference belongs to.
283 */
284 if (GROUP_PATTERN.test(node.type)) {
285
286 // If this expression is dynamic, no need to check.
287 if (hasDynamicExpressions(node)) {
288 break;
289 } else {
290 group = node;
291 }
292 }
293
294 child = node;
295 node = node.parent;
296 }
297
298 return null;
299 }
300
301 /**
302 * Finds unmodified references which are inside of a loop condition.
303 * Then reports the references which are outside of groups.
Tim van der Lippec8f6ffd2020-04-06 13:42:00 +0100304 * @param {eslint-scope.Variable} variable A variable to report.
Yang Guo4fd355c2019-09-19 10:59:03 +0200305 * @returns {void}
306 */
307 function checkReferences(variable) {
308
309 // Gets references that exist in loop conditions.
310 const conditions = variable
311 .references
312 .map(toLoopCondition)
313 .filter(Boolean);
314
315 if (conditions.length === 0) {
316 return;
317 }
318
319 // Registers the conditions to belonging groups.
320 registerConditionsToGroup(conditions);
321
322 // Check the conditions are modified.
323 const modifiers = variable.references.filter(isWriteReference);
324
325 if (modifiers.length > 0) {
326 updateModifiedFlag(conditions, modifiers);
327 }
328
329 /*
330 * Reports the conditions which are not belonging to groups.
331 * Others will be reported after all variables are done.
332 */
333 conditions
334 .filter(isUnmodifiedAndNotBelongToGroup)
335 .forEach(report);
336 }
337
338 return {
339 "Program:exit"() {
340 const queue = [context.getScope()];
341
342 groupMap = new Map();
343
344 let scope;
345
346 while ((scope = queue.pop())) {
347 queue.push(...scope.childScopes);
348 scope.variables.forEach(checkReferences);
349 }
350
351 groupMap.forEach(checkConditionsInGroup);
352 groupMap = null;
353 }
354 };
355 }
356};