• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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