UNPKG

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