no-loop-func.js 6.9 KB

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