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}; 205