]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/no-loop-func.js
import 8.3.0 source
[pve-eslint.git] / eslint / lib / rules / no-loop-func.js
CommitLineData
eb39fafa
DC
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",
eb39fafa
DC
157 recommended: false,
158 url: "https://eslint.org/docs/rules/no-loop-func"
159 },
160
161 schema: [],
162
163 messages: {
164 unsafeRefs: "Function declared in a loop contains unsafe references to variable(s) {{ varNames }}."
165 }
166 },
167
168 create(context) {
169
170 /**
171 * Reports functions which match the following condition:
172 *
173 * - has a loop node in ancestors.
174 * - has any references which refers to an unsafe variable.
175 * @param {ASTNode} node The AST node to check.
609c276f 176 * @returns {void}
eb39fafa
DC
177 */
178 function checkForLoops(node) {
179 const loopNode = getContainingLoopNode(node);
180
181 if (!loopNode) {
182 return;
183 }
184
185 const references = context.getScope().through;
186 const unsafeRefs = references.filter(r => !isSafe(loopNode, r)).map(r => r.identifier.name);
187
188 if (unsafeRefs.length > 0) {
189 context.report({
190 node,
191 messageId: "unsafeRefs",
192 data: { varNames: `'${unsafeRefs.join("', '")}'` }
193 });
194 }
195 }
196
197 return {
198 ArrowFunctionExpression: checkForLoops,
199 FunctionExpression: checkForLoops,
200 FunctionDeclaration: checkForLoops
201 };
202 }
203};