1 | /**
|
2 | * @fileoverview Rule to disallow use of unmodified expressions in loop conditions
|
3 | * @author Toru Nagashima
|
4 | */
|
5 |
|
6 | ;
|
7 |
|
8 | //------------------------------------------------------------------------------
|
9 | // Requirements
|
10 | //------------------------------------------------------------------------------
|
11 |
|
12 | const Traverser = require("../util/traverser"),
|
13 | astUtils = require("../ast-utils");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | const pushAll = Function.apply.bind(Array.prototype.push);
|
20 | const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/;
|
21 | const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property.
|
22 | const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/;
|
23 | const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/;
|
24 | const 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 | */
|
43 | function 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 | */
|
61 | function 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 | */
|
72 | function 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 | */
|
83 | function 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 | */
|
98 | const 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 | */
|
116 | function 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 | */
|
137 | function 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 |
|
167 | module.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 | };
|