UNPKG

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