]>
Commit | Line | Data |
---|---|---|
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 | */ | |
21 | function 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 | */ | |
70 | function 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 | */ | |
90 | function 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 | ||
151 | module.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 | }; |