no-loop-func.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * @fileoverview Rule to flag creation of function inside a loop
  3. * @author Ilya Volodin
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. /**
  10. * Identifies is a node is a FunctionExpression which is part of an IIFE
  11. * @param {ASTNode} node Node to test
  12. * @returns {boolean} True if it's an IIFE
  13. */
  14. function isIIFE(node) {
  15. return (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.parent && node.parent.type === "CallExpression" && node.parent.callee === node;
  16. }
  17. //------------------------------------------------------------------------------
  18. // Rule Definition
  19. //------------------------------------------------------------------------------
  20. /** @type {import('../shared/types').Rule} */
  21. module.exports = {
  22. meta: {
  23. type: "suggestion",
  24. docs: {
  25. description: "Disallow function declarations that contain unsafe references inside loop statements",
  26. recommended: false,
  27. url: "https://eslint.org/docs/latest/rules/no-loop-func"
  28. },
  29. schema: [],
  30. messages: {
  31. unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
  32. }
  33. },
  34. create(context) {
  35. const SKIPPED_IIFE_NODES = new Set();
  36. const sourceCode = context.sourceCode;
  37. /**
  38. * Gets the containing loop node of a specified node.
  39. *
  40. * We don't need to check nested functions, so this ignores those, with the exception of IIFE.
  41. * `Scope.through` contains references of nested functions.
  42. * @param {ASTNode} node An AST node to get.
  43. * @returns {ASTNode|null} The containing loop node of the specified node, or
  44. * `null`.
  45. */
  46. function getContainingLoopNode(node) {
  47. for (let currentNode = node; currentNode.parent; currentNode = currentNode.parent) {
  48. const parent = currentNode.parent;
  49. switch (parent.type) {
  50. case "WhileStatement":
  51. case "DoWhileStatement":
  52. return parent;
  53. case "ForStatement":
  54. // `init` is outside of the loop.
  55. if (parent.init !== currentNode) {
  56. return parent;
  57. }
  58. break;
  59. case "ForInStatement":
  60. case "ForOfStatement":
  61. // `right` is outside of the loop.
  62. if (parent.right !== currentNode) {
  63. return parent;
  64. }
  65. break;
  66. case "ArrowFunctionExpression":
  67. case "FunctionExpression":
  68. case "FunctionDeclaration":
  69. // We need to check nested functions only in case of IIFE.
  70. if (SKIPPED_IIFE_NODES.has(parent)) {
  71. break;
  72. }
  73. return null;
  74. default:
  75. break;
  76. }
  77. }
  78. return null;
  79. }
  80. /**
  81. * Gets the containing loop node of a given node.
  82. * If the loop was nested, this returns the most outer loop.
  83. * @param {ASTNode} node A node to get. This is a loop node.
  84. * @param {ASTNode|null} excludedNode A node that the result node should not
  85. * include.
  86. * @returns {ASTNode} The most outer loop node.
  87. */
  88. function getTopLoopNode(node, excludedNode) {
  89. const border = excludedNode ? excludedNode.range[1] : 0;
  90. let retv = node;
  91. let containingLoopNode = node;
  92. while (containingLoopNode && containingLoopNode.range[0] >= border) {
  93. retv = containingLoopNode;
  94. containingLoopNode = getContainingLoopNode(containingLoopNode);
  95. }
  96. return retv;
  97. }
  98. /**
  99. * Checks whether a given reference which refers to an upper scope's variable is
  100. * safe or not.
  101. * @param {ASTNode} loopNode A containing loop node.
  102. * @param {eslint-scope.Reference} reference A reference to check.
  103. * @returns {boolean} `true` if the reference is safe or not.
  104. */
  105. function isSafe(loopNode, reference) {
  106. const variable = reference.resolved;
  107. const definition = variable && variable.defs[0];
  108. const declaration = definition && definition.parent;
  109. const kind = (declaration && declaration.type === "VariableDeclaration")
  110. ? declaration.kind
  111. : "";
  112. // Variables which are declared by `const` is safe.
  113. if (kind === "const") {
  114. return true;
  115. }
  116. /*
  117. * Variables which are declared by `let` in the loop is safe.
  118. * It's a different instance from the next loop step's.
  119. */
  120. if (kind === "let" &&
  121. declaration.range[0] > loopNode.range[0] &&
  122. declaration.range[1] < loopNode.range[1]
  123. ) {
  124. return true;
  125. }
  126. /*
  127. * WriteReferences which exist after this border are unsafe because those
  128. * can modify the variable.
  129. */
  130. const border = getTopLoopNode(
  131. loopNode,
  132. (kind === "let") ? declaration : null
  133. ).range[0];
  134. /**
  135. * Checks whether a given reference is safe or not.
  136. * The reference is every reference of the upper scope's variable we are
  137. * looking now.
  138. *
  139. * It's safe if the reference matches one of the following condition.
  140. * - is readonly.
  141. * - doesn't exist inside a local function and after the border.
  142. * @param {eslint-scope.Reference} upperRef A reference to check.
  143. * @returns {boolean} `true` if the reference is safe.
  144. */
  145. function isSafeReference(upperRef) {
  146. const id = upperRef.identifier;
  147. return (
  148. !upperRef.isWrite() ||
  149. variable.scope.variableScope === upperRef.from.variableScope &&
  150. id.range[0] < border
  151. );
  152. }
  153. return Boolean(variable) && variable.references.every(isSafeReference);
  154. }
  155. /**
  156. * Reports functions which match the following condition:
  157. *
  158. * - has a loop node in ancestors.
  159. * - has any references which refers to an unsafe variable.
  160. * @param {ASTNode} node The AST node to check.
  161. * @returns {void}
  162. */
  163. function checkForLoops(node) {
  164. const loopNode = getContainingLoopNode(node);
  165. if (!loopNode) {
  166. return;
  167. }
  168. const references = sourceCode.getScope(node).through;
  169. // Check if the function is not asynchronous or a generator function
  170. if (!(node.async || node.generator)) {
  171. if (isIIFE(node)) {
  172. const isFunctionExpression = node.type === "FunctionExpression";
  173. // Check if the function is referenced elsewhere in the code
  174. const isFunctionReferenced = isFunctionExpression && node.id ? references.some(r => r.identifier.name === node.id.name) : false;
  175. if (!isFunctionReferenced) {
  176. SKIPPED_IIFE_NODES.add(node);
  177. return;
  178. }
  179. }
  180. }
  181. const unsafeRefs = references.filter(r => r.resolved && !isSafe(loopNode, r)).map(r => r.identifier.name);
  182. if (unsafeRefs.length > 0) {
  183. context.report({
  184. node,
  185. messageId: "unsafeRefs",
  186. data: { varNames: `'${unsafeRefs.join("', '")}'` }
  187. });
  188. }
  189. }
  190. return {
  191. ArrowFunctionExpression: checkForLoops,
  192. FunctionExpression: checkForLoops,
  193. FunctionDeclaration: checkForLoops
  194. };
  195. }
  196. };