]>
Commit | Line | Data |
---|---|---|
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 | ||
12 | const Traverser = require("../shared/traverser"), | |
13 | astUtils = require("./utils/ast-utils"); | |
14 | ||
15 | //------------------------------------------------------------------------------ | |
16 | // Helpers | |
17 | //------------------------------------------------------------------------------ | |
18 | ||
19 | const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/u; | |
20 | const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/u; // for-in/of statements don't have `test` property. | |
21 | const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/u; | |
22 | const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/u; | |
23 | const 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 | */ | |
41 | function 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 | */ | |
58 | function 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 | */ | |
68 | function 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 | */ | |
78 | function 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 | */ | |
92 | const 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 | */ | |
109 | function 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 | */ | |
129 | function 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 |
160 | module.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 | }; |