2 * @fileoverview Rule to flag creation of function inside a loop
8 //------------------------------------------------------------------------------
10 //------------------------------------------------------------------------------
13 * Gets the containing loop node of a specified node.
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
21 function getContainingLoopNode(node) {
22 for (let currentNode = node; currentNode.parent; currentNode = currentNode.parent) {
23 const parent = currentNode.parent;
25 switch (parent.type) {
26 case "WhileStatement":
27 case "DoWhileStatement":
32 // `init` is outside of the loop.
33 if (parent.init !== currentNode) {
38 case "ForInStatement":
39 case "ForOfStatement":
41 // `right` is outside of the loop.
42 if (parent.right !== currentNode) {
47 case "ArrowFunctionExpression":
48 case "FunctionExpression":
49 case "FunctionDeclaration":
51 // We don't need to check nested functions.
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
68 * @returns {ASTNode} The most outer loop node.
70 function getTopLoopNode(node, excludedNode) {
71 const border = excludedNode ? excludedNode.range[1] : 0;
73 let containingLoopNode = node;
75 while (containingLoopNode && containingLoopNode.range[0] >= border) {
76 retv = containingLoopNode;
77 containingLoopNode = getContainingLoopNode(containingLoopNode);
84 * Checks whether a given reference which refers to an upper scope's variable is
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.
90 function 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")
98 // Variables which are declared by `const` is safe.
99 if (kind === "const") {
104 * Variables which are declared by `let` in the loop is safe.
105 * It's a different instance from the next loop step's.
107 if (kind === "let" &&
108 declaration.range[0] > loopNode.range[0] &&
109 declaration.range[1] < loopNode.range[1]
115 * WriteReferences which exist after this border are unsafe because those
116 * can modify the variable.
118 const border = getTopLoopNode(
120 (kind === "let") ? declaration : null
124 * Checks whether a given reference is safe or not.
125 * The reference is every reference of the upper scope's variable we are
128 * It's safeafe if the reference matches one of the following condition.
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.
134 function isSafeReference(upperRef) {
135 const id = upperRef.identifier;
138 !upperRef.isWrite() ||
139 variable.scope.variableScope === upperRef.from.variableScope &&
144 return Boolean(variable) && variable.references.every(isSafeReference);
147 //------------------------------------------------------------------------------
149 //------------------------------------------------------------------------------
156 description: "disallow function declarations that contain unsafe references inside loop statements",
157 category: "Best Practices",
159 url: "https://eslint.org/docs/rules/no-loop-func"
165 unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
172 * Reports functions which match the following condition:
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.
179 function checkForLoops(node) {
180 const loopNode = getContainingLoopNode(node);
186 const references = context.getScope().through;
187 const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name);
189 if (unsafeRefs.length > 0) {
192 messageId: "unsafeRefs",
193 data: { varNames: `'${unsafeRefs.join("', '")}'` }
199 ArrowFunctionExpression: checkForLoops,
200 FunctionExpression: checkForLoops,
201 FunctionDeclaration: checkForLoops