UNPKG

6.31 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 type: "suggestion",
158
159 docs: {
160 description: "disallow function declarations that contain unsafe references 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 messages: {
169 unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
170 }
171 },
172
173 create(context) {
174
175 /**
176 * Reports functions which match the following condition:
177 *
178 * - has a loop node in ancestors.
179 * - has any references which refers to an unsafe variable.
180 *
181 * @param {ASTNode} node The AST node to check.
182 * @returns {boolean} Whether or not the node is within a loop.
183 */
184 function checkForLoops(node) {
185 const loopNode = getContainingLoopNode(node);
186
187 if (!loopNode) {
188 return;
189 }
190
191 const references = context.getScope().through;
192 const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name);
193
194 if (unsafeRefs.length > 0) {
195 context.report({
196 node,
197 messageId: "unsafeRefs",
198 data: { varNames: `'${unsafeRefs.join("', '")}'` }
199 });
200 }
201 }
202
203 return {
204 ArrowFunctionExpression: checkForLoops,
205 FunctionExpression: checkForLoops,
206 FunctionDeclaration: checkForLoops
207 };
208 }
209};