UNPKG

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