]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/no-unmodified-loop-condition.js
change from CLIEngine to ESLint
[pve-eslint.git] / eslint / lib / rules / no-unmodified-loop-condition.js
CommitLineData
eb39fafa
DC
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 * @param {eslint-scope.Reference} reference A reference to check.
39 * @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.
55 * @param {LoopConditionInfo} condition A loop condition info to check.
56 * @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.
65 * @param {LoopConditionInfo} condition A loop condition info to check.
66 * @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.
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.
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.
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
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.
106 * @param {eslint-scope.Reference} reference A reference to get.
107 * @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.
125 * @param {LoopConditionInfo[]} conditions The loop conditions to be updated.
126 * @param {eslint-scope.Reference[]} modifiers The references to update.
127 * @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",
eb39fafa
DC
165 recommended: false,
166 url: "https://eslint.org/docs/rules/no-unmodified-loop-condition"
167 },
168
169 schema: [],
170
171 messages: {
172 loopConditionNotModified: "'{{name}}' is not modified in this loop."
173 }
174 },
175
176 create(context) {
177 const sourceCode = context.getSourceCode();
178 let groupMap = null;
179
180 /**
181 * Reports a given condition info.
182 * @param {LoopConditionInfo} condition A loop condition info to report.
183 * @returns {void}
184 */
185 function report(condition) {
186 const node = condition.reference.identifier;
187
188 context.report({
189 node,
190 messageId: "loopConditionNotModified",
191 data: node
192 });
193 }
194
195 /**
196 * Registers given conditions to the group the condition belongs to.
197 * @param {LoopConditionInfo[]} conditions A loop condition info to
198 * register.
199 * @returns {void}
200 */
201 function registerConditionsToGroup(conditions) {
202 for (let i = 0; i < conditions.length; ++i) {
203 const condition = conditions[i];
204
205 if (condition.group) {
206 let group = groupMap.get(condition.group);
207
208 if (!group) {
209 group = [];
210 groupMap.set(condition.group, group);
211 }
212 group.push(condition);
213 }
214 }
215 }
216
217 /**
218 * Reports references which are inside of unmodified groups.
219 * @param {LoopConditionInfo[]} conditions A loop condition info to report.
220 * @returns {void}
221 */
222 function checkConditionsInGroup(conditions) {
223 if (conditions.every(isUnmodified)) {
224 conditions.forEach(report);
225 }
226 }
227
228 /**
229 * Checks whether or not a given group node has any dynamic elements.
230 * @param {ASTNode} root A node to check.
231 * This node is one of BinaryExpression or ConditionalExpression.
232 * @returns {boolean} `true` if the node is dynamic.
233 */
234 function hasDynamicExpressions(root) {
235 let retv = false;
236
237 Traverser.traverse(root, {
238 visitorKeys: sourceCode.visitorKeys,
239 enter(node) {
240 if (DYNAMIC_PATTERN.test(node.type)) {
241 retv = true;
242 this.break();
243 } else if (SKIP_PATTERN.test(node.type)) {
244 this.skip();
245 }
246 }
247 });
248
249 return retv;
250 }
251
252 /**
253 * Creates the loop condition information from a given reference.
254 * @param {eslint-scope.Reference} reference A reference to create.
255 * @returns {LoopConditionInfo|null} Created loop condition info, or null.
256 */
257 function toLoopCondition(reference) {
258 if (reference.init) {
259 return null;
260 }
261
262 let group = null;
263 let child = reference.identifier;
264 let node = child.parent;
265
266 while (node) {
267 if (SENTINEL_PATTERN.test(node.type)) {
268 if (LOOP_PATTERN.test(node.type) && node.test === child) {
269
270 // This reference is inside of a loop condition.
271 return {
272 reference,
273 group,
274 isInLoop: isInLoop[node.type].bind(null, node),
275 modified: false
276 };
277 }
278
279 // This reference is outside of a loop condition.
280 break;
281 }
282
283 /*
284 * If it's inside of a group, OK if either operand is modified.
285 * So stores the group this reference belongs to.
286 */
287 if (GROUP_PATTERN.test(node.type)) {
288
289 // If this expression is dynamic, no need to check.
290 if (hasDynamicExpressions(node)) {
291 break;
292 } else {
293 group = node;
294 }
295 }
296
297 child = node;
298 node = node.parent;
299 }
300
301 return null;
302 }
303
304 /**
305 * Finds unmodified references which are inside of a loop condition.
306 * Then reports the references which are outside of groups.
307 * @param {eslint-scope.Variable} variable A variable to report.
308 * @returns {void}
309 */
310 function checkReferences(variable) {
311
312 // Gets references that exist in loop conditions.
313 const conditions = variable
314 .references
315 .map(toLoopCondition)
316 .filter(Boolean);
317
318 if (conditions.length === 0) {
319 return;
320 }
321
322 // Registers the conditions to belonging groups.
323 registerConditionsToGroup(conditions);
324
325 // Check the conditions are modified.
326 const modifiers = variable.references.filter(isWriteReference);
327
328 if (modifiers.length > 0) {
329 updateModifiedFlag(conditions, modifiers);
330 }
331
332 /*
333 * Reports the conditions which are not belonging to groups.
334 * Others will be reported after all variables are done.
335 */
336 conditions
337 .filter(isUnmodifiedAndNotBelongToGroup)
338 .forEach(report);
339 }
340
341 return {
342 "Program:exit"() {
343 const queue = [context.getScope()];
344
345 groupMap = new Map();
346
347 let scope;
348
349 while ((scope = queue.pop())) {
350 queue.push(...scope.childScopes);
351 scope.variables.forEach(checkReferences);
352 }
353
354 groupMap.forEach(checkConditionsInGroup);
355 groupMap = null;
356 }
357 };
358 }
359};