UNPKG

11.2 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("../util/traverser"),
13 astUtils = require("../ast-utils");
14
15//------------------------------------------------------------------------------
16// Helpers
17//------------------------------------------------------------------------------
18
19const pushAll = Function.apply.bind(Array.prototype.push);
20const SENTINEL_PATTERN = /(?:(?:Call|Class|Function|Member|New|Yield)Expression|Statement|Declaration)$/;
21const LOOP_PATTERN = /^(?:DoWhile|For|While)Statement$/; // for-in/of statements don't have `test` property.
22const GROUP_PATTERN = /^(?:BinaryExpression|ConditionalExpression)$/;
23const SKIP_PATTERN = /^(?:ArrowFunction|Class|Function)Expression$/;
24const DYNAMIC_PATTERN = /^(?:Call|Member|New|TaggedTemplate|Yield)Expression$/;
25
26/**
27 * @typedef {Object} LoopConditionInfo
28 * @property {escope.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 {escope.Reference} reference - A reference to check.
41 * @returns {boolean} `true` if the reference is a write reference.
42 */
43function 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 */
61function 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 */
72function 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 {escope.Reference} reference - A reference to check.
81 * @returns {boolean} `true` if the reference is inside of the node.
82 */
83function 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 {escope.Reference} reference - A reference to check.
95 * @returns {boolean} `true` if the reference is inside of the loop node's
96 * condition.
97 */
98const 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 * Checks whether or not a given group node has any dynamic elements.
111 *
112 * @param {ASTNode} root - A node to check.
113 * This node is one of BinaryExpression or ConditionalExpression.
114 * @returns {boolean} `true` if the node is dynamic.
115 */
116function hasDynamicExpressions(root) {
117 let retv = false;
118 const traverser = new Traverser();
119
120 traverser.traverse(root, {
121 enter(node) {
122 if (DYNAMIC_PATTERN.test(node.type)) {
123 retv = true;
124 this.break();
125 } else if (SKIP_PATTERN.test(node.type)) {
126 this.skip();
127 }
128 }
129 });
130
131 return retv;
132}
133
134/**
135 * Creates the loop condition information from a given reference.
136 *
137 * @param {escope.Reference} reference - A reference to create.
138 * @returns {LoopConditionInfo|null} Created loop condition info, or null.
139 */
140function toLoopCondition(reference) {
141 if (reference.init) {
142 return null;
143 }
144
145 let group = null;
146 let child = reference.identifier;
147 let node = child.parent;
148
149 while (node) {
150 if (SENTINEL_PATTERN.test(node.type)) {
151 if (LOOP_PATTERN.test(node.type) && node.test === child) {
152
153 // This reference is inside of a loop condition.
154 return {
155 reference,
156 group,
157 isInLoop: isInLoop[node.type].bind(null, node),
158 modified: false
159 };
160 }
161
162 // This reference is outside of a loop condition.
163 break;
164 }
165
166 /*
167 * If it's inside of a group, OK if either operand is modified.
168 * So stores the group this reference belongs to.
169 */
170 if (GROUP_PATTERN.test(node.type)) {
171
172 // If this expression is dynamic, no need to check.
173 if (hasDynamicExpressions(node)) {
174 break;
175 } else {
176 group = node;
177 }
178 }
179
180 child = node;
181 node = node.parent;
182 }
183
184 return null;
185}
186
187/**
188 * Gets the function which encloses a given reference.
189 * This supports only FunctionDeclaration.
190 *
191 * @param {escope.Reference} reference - A reference to get.
192 * @returns {ASTNode|null} The function node or null.
193 */
194function getEncloseFunctionDeclaration(reference) {
195 let node = reference.identifier;
196
197 while (node) {
198 if (node.type === "FunctionDeclaration") {
199 return node.id ? node : null;
200 }
201
202 node = node.parent;
203 }
204
205 return null;
206}
207
208/**
209 * Updates the "modified" flags of given loop conditions with given modifiers.
210 *
211 * @param {LoopConditionInfo[]} conditions - The loop conditions to be updated.
212 * @param {escope.Reference[]} modifiers - The references to update.
213 * @returns {void}
214 */
215function updateModifiedFlag(conditions, modifiers) {
216 let funcNode, funcVar;
217
218 for (let i = 0; i < conditions.length; ++i) {
219 const condition = conditions[i];
220
221 for (let j = 0; !condition.modified && j < modifiers.length; ++j) {
222 const modifier = modifiers[j];
223
224 /*
225 * Besides checking for the condition being in the loop, we want to
226 * check the function that this modifier is belonging to is called
227 * in the loop.
228 * FIXME: This should probably be extracted to a function.
229 */
230 const inLoop = condition.isInLoop(modifier) || Boolean(
231 (funcNode = getEncloseFunctionDeclaration(modifier)) &&
232 (funcVar = astUtils.getVariableByName(modifier.from.upper, funcNode.id.name)) &&
233 funcVar.references.some(condition.isInLoop)
234 );
235
236 condition.modified = inLoop;
237 }
238 }
239}
240
241//------------------------------------------------------------------------------
242// Rule Definition
243//------------------------------------------------------------------------------
244
245module.exports = {
246 meta: {
247 docs: {
248 description: "disallow unmodified loop conditions",
249 category: "Best Practices",
250 recommended: false
251 },
252
253 schema: []
254 },
255
256 create(context) {
257 let groupMap = null;
258
259 /**
260 * Reports a given condition info.
261 *
262 * @param {LoopConditionInfo} condition - A loop condition info to report.
263 * @returns {void}
264 */
265 function report(condition) {
266 const node = condition.reference.identifier;
267
268 context.report({
269 node,
270 message: "'{{name}}' is not modified in this loop.",
271 data: node
272 });
273 }
274
275 /**
276 * Registers given conditions to the group the condition belongs to.
277 *
278 * @param {LoopConditionInfo[]} conditions - A loop condition info to
279 * register.
280 * @returns {void}
281 */
282 function registerConditionsToGroup(conditions) {
283 for (let i = 0; i < conditions.length; ++i) {
284 const condition = conditions[i];
285
286 if (condition.group) {
287 let group = groupMap.get(condition.group);
288
289 if (!group) {
290 group = [];
291 groupMap.set(condition.group, group);
292 }
293 group.push(condition);
294 }
295 }
296 }
297
298 /**
299 * Reports references which are inside of unmodified groups.
300 *
301 * @param {LoopConditionInfo[]} conditions - A loop condition info to report.
302 * @returns {void}
303 */
304 function checkConditionsInGroup(conditions) {
305 if (conditions.every(isUnmodified)) {
306 conditions.forEach(report);
307 }
308 }
309
310 /**
311 * Finds unmodified references which are inside of a loop condition.
312 * Then reports the references which are outside of groups.
313 *
314 * @param {escope.Variable} variable - A variable to report.
315 * @returns {void}
316 */
317 function checkReferences(variable) {
318
319 // Gets references that exist in loop conditions.
320 const conditions = variable
321 .references
322 .map(toLoopCondition)
323 .filter(Boolean);
324
325 if (conditions.length === 0) {
326 return;
327 }
328
329 // Registers the conditions to belonging groups.
330 registerConditionsToGroup(conditions);
331
332 // Check the conditions are modified.
333 const modifiers = variable.references.filter(isWriteReference);
334
335 if (modifiers.length > 0) {
336 updateModifiedFlag(conditions, modifiers);
337 }
338
339 /*
340 * Reports the conditions which are not belonging to groups.
341 * Others will be reported after all variables are done.
342 */
343 conditions
344 .filter(isUnmodifiedAndNotBelongToGroup)
345 .forEach(report);
346 }
347
348 return {
349 "Program:exit"() {
350 const queue = [context.getScope()];
351
352 groupMap = new Map();
353
354 let scope;
355
356 while ((scope = queue.pop())) {
357 pushAll(queue, scope.childScopes);
358 scope.variables.forEach(checkReferences);
359 }
360
361 groupMap.forEach(checkConditionsInGroup);
362 groupMap = null;
363 }
364 };
365 }
366};