UNPKG

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