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