UNPKG

5.84 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} loopNode - A containing loop node.
92 * @param {eslint-scope.Reference} reference - A reference to check.
93 * @returns {boolean} `true` if the reference is safe or not.
94 */
95function isSafe(loopNode, reference) {
96 const variable = reference.resolved;
97 const definition = variable && variable.defs[0];
98 const declaration = definition && definition.parent;
99 const kind = (declaration && declaration.type === "VariableDeclaration")
100 ? declaration.kind
101 : "";
102
103 // Variables which are declared by `const` is safe.
104 if (kind === "const") {
105 return true;
106 }
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 */
112 if (kind === "let" &&
113 declaration.range[0] > loopNode.range[0] &&
114 declaration.range[1] < loopNode.range[1]
115 ) {
116 return true;
117 }
118
119 /*
120 * WriteReferences which exist after this border are unsafe because those
121 * can modify the variable.
122 */
123 const border = getTopLoopNode(
124 loopNode,
125 (kind === "let") ? declaration : null
126 ).range[0];
127
128 /**
129 * Checks whether a given reference is safe or not.
130 * The reference is every reference of the upper scope's variable we are
131 * looking now.
132 *
133 * It's safeafe if the reference matches one of the following condition.
134 * - is readonly.
135 * - doesn't exist inside a local function and after the border.
136 *
137 * @param {eslint-scope.Reference} upperRef - A reference to check.
138 * @returns {boolean} `true` if the reference is safe.
139 */
140 function isSafeReference(upperRef) {
141 const id = upperRef.identifier;
142
143 return (
144 !upperRef.isWrite() ||
145 variable.scope.variableScope === upperRef.from.variableScope &&
146 id.range[0] < border
147 );
148 }
149
150 return Boolean(variable) && variable.references.every(isSafeReference);
151}
152
153//------------------------------------------------------------------------------
154// Rule Definition
155//------------------------------------------------------------------------------
156
157module.exports = {
158 meta: {
159 docs: {
160 description: "disallow `function` declarations and expressions inside loop statements",
161 category: "Best Practices",
162 recommended: false,
163 url: "https://eslint.org/docs/rules/no-loop-func"
164 },
165
166 schema: []
167 },
168
169 create(context) {
170
171 /**
172 * Reports functions which match the following condition:
173 *
174 * - has a loop node in ancestors.
175 * - has any references which refers to an unsafe variable.
176 *
177 * @param {ASTNode} node The AST node to check.
178 * @returns {boolean} Whether or not the node is within a loop.
179 */
180 function checkForLoops(node) {
181 const loopNode = getContainingLoopNode(node);
182
183 if (!loopNode) {
184 return;
185 }
186
187 const references = context.getScope().through;
188
189 if (references.length > 0 &&
190 !references.every(isSafe.bind(null, loopNode))
191 ) {
192 context.report({ node, message: "Don't make functions within a loop." });
193 }
194 }
195
196 return {
197 ArrowFunctionExpression: checkForLoops,
198 FunctionExpression: checkForLoops,
199 FunctionDeclaration: checkForLoops
200 };
201 }
202};