UNPKG

5.8 kBJavaScriptView Raw
1/**
2 * @fileoverview Rule to flag creation of function inside a loop
3 * @author Ilya Volodin
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12/**
13 * Gets the containing loop node of a specified node.
14 *
15 * We don't need to check nested functions, so this ignores those.
16 * `Scope.through` contains references of nested functions.
17 *
18 * @param {ASTNode} node - An AST node to get.
19 * @returns {ASTNode|null} The containing loop node of the specified node, or
20 * `null`.
21 */
22function getContainingLoopNode(node) {
23 let parent = node.parent;
24
25 while (parent) {
26 switch (parent.type) {
27 case "WhileStatement":
28 case "DoWhileStatement":
29 return parent;
30
31 case "ForStatement":
32
33 // `init` is outside of the loop.
34 if (parent.init !== node) {
35 return parent;
36 }
37 break;
38
39 case "ForInStatement":
40 case "ForOfStatement":
41
42 // `right` is outside of the loop.
43 if (parent.right !== node) {
44 return parent;
45 }
46 break;
47
48 case "ArrowFunctionExpression":
49 case "FunctionExpression":
50 case "FunctionDeclaration":
51
52 // We don't need to check nested functions.
53 return null;
54
55 default:
56 break;
57 }
58
59 node = parent;
60 parent = node.parent;
61 }
62
63 return null;
64}
65
66/**
67 * Gets the containing loop node of a given node.
68 * If the loop was nested, this returns the most outer loop.
69 *
70 * @param {ASTNode} node - A node to get. This is a loop node.
71 * @param {ASTNode|null} excludedNode - A node that the result node should not
72 * include.
73 * @returns {ASTNode} The most outer loop node.
74 */
75function getTopLoopNode(node, excludedNode) {
76 let retv = node;
77 const border = excludedNode ? excludedNode.range[1] : 0;
78
79 while (node && node.range[0] >= border) {
80 retv = node;
81 node = getContainingLoopNode(node);
82 }
83
84 return retv;
85}
86
87/**
88 * Checks whether a given reference which refers to an upper scope's variable is
89 * safe or not.
90 *
91 * @param {ASTNode} funcNode - A target function node.
92 * @param {ASTNode} loopNode - A containing loop node.
93 * @param {escope.Reference} reference - A reference to check.
94 * @returns {boolean} `true` if the reference is safe or not.
95 */
96function isSafe(funcNode, loopNode, reference) {
97 const variable = reference.resolved;
98 const definition = variable && variable.defs[0];
99 const declaration = definition && definition.parent;
100 const kind = (declaration && declaration.type === "VariableDeclaration")
101 ? declaration.kind
102 : "";
103
104 // Variables which are declared by `const` is safe.
105 if (kind === "const") {
106 return true;
107 }
108
109 // Variables which are declared by `let` in the loop is safe.
110 // It's a different instance from the next loop step's.
111 if (kind === "let" &&
112 declaration.range[0] > loopNode.range[0] &&
113 declaration.range[1] < loopNode.range[1]
114 ) {
115 return true;
116 }
117
118 // WriteReferences which exist after this border are unsafe because those
119 // can modify the variable.
120 const border = getTopLoopNode(
121 loopNode,
122 (kind === "let") ? declaration : null
123 ).range[0];
124
125 /**
126 * Checks whether a given reference is safe or not.
127 * The reference is every reference of the upper scope's variable we are
128 * looking now.
129 *
130 * It's safeafe if the reference matches one of the following condition.
131 * - is readonly.
132 * - doesn't exist inside a local function and after the border.
133 *
134 * @param {escope.Reference} upperRef - A reference to check.
135 * @returns {boolean} `true` if the reference is safe.
136 */
137 function isSafeReference(upperRef) {
138 const id = upperRef.identifier;
139
140 return (
141 !upperRef.isWrite() ||
142 variable.scope.variableScope === upperRef.from.variableScope &&
143 id.range[0] < border
144 );
145 }
146
147 return Boolean(variable) && variable.references.every(isSafeReference);
148}
149
150//------------------------------------------------------------------------------
151// Rule Definition
152//------------------------------------------------------------------------------
153
154module.exports = {
155 meta: {
156 docs: {
157 description: "disallow `function` declarations and expressions inside loop statements",
158 category: "Best Practices",
159 recommended: false
160 },
161
162 schema: []
163 },
164
165 create(context) {
166
167 /**
168 * Reports functions which match the following condition:
169 *
170 * - has a loop node in ancestors.
171 * - has any references which refers to an unsafe variable.
172 *
173 * @param {ASTNode} node The AST node to check.
174 * @returns {boolean} Whether or not the node is within a loop.
175 */
176 function checkForLoops(node) {
177 const loopNode = getContainingLoopNode(node);
178
179 if (!loopNode) {
180 return;
181 }
182
183 const references = context.getScope().through;
184
185 if (references.length > 0 &&
186 !references.every(isSafe.bind(null, node, loopNode))
187 ) {
188 context.report(node, "Don't make functions within a loop.");
189 }
190 }
191
192 return {
193 ArrowFunctionExpression: checkForLoops,
194 FunctionExpression: checkForLoops,
195 FunctionDeclaration: checkForLoops
196 };
197 }
198};