123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- /**
- * @fileoverview Rule to flag creation of function inside a loop
- * @author Ilya Volodin
- */
- "use strict";
- //------------------------------------------------------------------------------
- // Helpers
- //------------------------------------------------------------------------------
- /**
- * Gets the containing loop node of a specified node.
- *
- * We don't need to check nested functions, so this ignores those.
- * `Scope.through` contains references of nested functions.
- * @param {ASTNode} node An AST node to get.
- * @returns {ASTNode|null} The containing loop node of the specified node, or
- * `null`.
- */
- function getContainingLoopNode(node) {
- for (let currentNode = node; currentNode.parent; currentNode = currentNode.parent) {
- const parent = currentNode.parent;
- switch (parent.type) {
- case "WhileStatement":
- case "DoWhileStatement":
- return parent;
- case "ForStatement":
- // `init` is outside of the loop.
- if (parent.init !== currentNode) {
- return parent;
- }
- break;
- case "ForInStatement":
- case "ForOfStatement":
- // `right` is outside of the loop.
- if (parent.right !== currentNode) {
- return parent;
- }
- break;
- case "ArrowFunctionExpression":
- case "FunctionExpression":
- case "FunctionDeclaration":
- // We don't need to check nested functions.
- return null;
- default:
- break;
- }
- }
- return null;
- }
- /**
- * Gets the containing loop node of a given node.
- * If the loop was nested, this returns the most outer loop.
- * @param {ASTNode} node A node to get. This is a loop node.
- * @param {ASTNode|null} excludedNode A node that the result node should not
- * include.
- * @returns {ASTNode} The most outer loop node.
- */
- function getTopLoopNode(node, excludedNode) {
- const border = excludedNode ? excludedNode.range[1] : 0;
- let retv = node;
- let containingLoopNode = node;
- while (containingLoopNode && containingLoopNode.range[0] >= border) {
- retv = containingLoopNode;
- containingLoopNode = getContainingLoopNode(containingLoopNode);
- }
- return retv;
- }
- /**
- * Checks whether a given reference which refers to an upper scope's variable is
- * safe or not.
- * @param {ASTNode} loopNode A containing loop node.
- * @param {eslint-scope.Reference} reference A reference to check.
- * @returns {boolean} `true` if the reference is safe or not.
- */
- function isSafe(loopNode, reference) {
- const variable = reference.resolved;
- const definition = variable && variable.defs[0];
- const declaration = definition && definition.parent;
- const kind = (declaration && declaration.type === "VariableDeclaration")
- ? declaration.kind
- : "";
- // Variables which are declared by `const` is safe.
- if (kind === "const") {
- return true;
- }
- /*
- * Variables which are declared by `let` in the loop is safe.
- * It's a different instance from the next loop step's.
- */
- if (kind === "let" &&
- declaration.range[0] > loopNode.range[0] &&
- declaration.range[1] < loopNode.range[1]
- ) {
- return true;
- }
- /*
- * WriteReferences which exist after this border are unsafe because those
- * can modify the variable.
- */
- const border = getTopLoopNode(
- loopNode,
- (kind === "let") ? declaration : null
- ).range[0];
- /**
- * Checks whether a given reference is safe or not.
- * The reference is every reference of the upper scope's variable we are
- * looking now.
- *
- * It's safe if the reference matches one of the following condition.
- * - is readonly.
- * - doesn't exist inside a local function and after the border.
- * @param {eslint-scope.Reference} upperRef A reference to check.
- * @returns {boolean} `true` if the reference is safe.
- */
- function isSafeReference(upperRef) {
- const id = upperRef.identifier;
- return (
- !upperRef.isWrite() ||
- variable.scope.variableScope === upperRef.from.variableScope &&
- id.range[0] < border
- );
- }
- return Boolean(variable) && variable.references.every(isSafeReference);
- }
- //------------------------------------------------------------------------------
- // Rule Definition
- //------------------------------------------------------------------------------
- /** @type {import('../shared/types').Rule} */
- module.exports = {
- meta: {
- type: "suggestion",
- docs: {
- description: "Disallow function declarations that contain unsafe references inside loop statements",
- recommended: false,
- url: "https://eslint.org/docs/rules/no-loop-func"
- },
- schema: [],
- messages: {
- unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
- }
- },
- create(context) {
- /**
- * Reports functions which match the following condition:
- *
- * - has a loop node in ancestors.
- * - has any references which refers to an unsafe variable.
- * @param {ASTNode} node The AST node to check.
- * @returns {void}
- */
- function checkForLoops(node) {
- const loopNode = getContainingLoopNode(node);
- if (!loopNode) {
- return;
- }
- const references = context.getScope().through;
- const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name);
- if (unsafeRefs.length > 0) {
- context.report({
- node,
- messageId: "unsafeRefs",
- data: { varNames: `'${unsafeRefs.join("', '")}'` }
- });
- }
- }
- return {
- ArrowFunctionExpression: checkForLoops,
- FunctionExpression: checkForLoops,
- FunctionDeclaration: checkForLoops
- };
- }
- };
|