]>
Commit | Line | Data |
---|---|---|
eb39fafa DC |
1 | /** |
2 | * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield` | |
3 | * @author Teddy Katz | |
4 | * @author Toru Nagashima | |
5 | */ | |
6 | "use strict"; | |
7 | ||
8 | /** | |
9 | * Make the map from identifiers to each reference. | |
10 | * @param {escope.Scope} scope The scope to get references. | |
11 | * @param {Map<Identifier, escope.Reference>} [outReferenceMap] The map from identifier nodes to each reference object. | |
12 | * @returns {Map<Identifier, escope.Reference>} `referenceMap`. | |
13 | */ | |
14 | function createReferenceMap(scope, outReferenceMap = new Map()) { | |
15 | for (const reference of scope.references) { | |
5422a9cc TL |
16 | if (reference.resolved === null) { |
17 | continue; | |
18 | } | |
19 | ||
eb39fafa DC |
20 | outReferenceMap.set(reference.identifier, reference); |
21 | } | |
22 | for (const childScope of scope.childScopes) { | |
23 | if (childScope.type !== "function") { | |
24 | createReferenceMap(childScope, outReferenceMap); | |
25 | } | |
26 | } | |
27 | ||
28 | return outReferenceMap; | |
29 | } | |
30 | ||
31 | /** | |
32 | * Get `reference.writeExpr` of a given reference. | |
33 | * If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a` | |
34 | * @param {escope.Reference} reference The reference to get. | |
35 | * @returns {Expression|null} The `reference.writeExpr`. | |
36 | */ | |
37 | function getWriteExpr(reference) { | |
38 | if (reference.writeExpr) { | |
39 | return reference.writeExpr; | |
40 | } | |
41 | let node = reference.identifier; | |
42 | ||
43 | while (node) { | |
44 | const t = node.parent.type; | |
45 | ||
46 | if (t === "AssignmentExpression" && node.parent.left === node) { | |
47 | return node.parent.right; | |
48 | } | |
49 | if (t === "MemberExpression" && node.parent.object === node) { | |
50 | node = node.parent; | |
51 | continue; | |
52 | } | |
53 | ||
54 | break; | |
55 | } | |
56 | ||
57 | return null; | |
58 | } | |
59 | ||
60 | /** | |
61 | * Checks if an expression is a variable that can only be observed within the given function. | |
62 | * @param {Variable|null} variable The variable to check | |
63 | * @param {boolean} isMemberAccess If `true` then this is a member access. | |
64 | * @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure. | |
65 | */ | |
66 | function isLocalVariableWithoutEscape(variable, isMemberAccess) { | |
67 | if (!variable) { | |
68 | return false; // A global variable which was not defined. | |
69 | } | |
70 | ||
71 | // If the reference is a property access and the variable is a parameter, it handles the variable is not local. | |
72 | if (isMemberAccess && variable.defs.some(d => d.type === "Parameter")) { | |
73 | return false; | |
74 | } | |
75 | ||
76 | const functionScope = variable.scope.variableScope; | |
77 | ||
78 | return variable.references.every(reference => | |
79 | reference.from.variableScope === functionScope); | |
80 | } | |
81 | ||
609c276f TL |
82 | /** |
83 | * Represents segment information. | |
84 | */ | |
eb39fafa DC |
85 | class SegmentInfo { |
86 | constructor() { | |
87 | this.info = new WeakMap(); | |
88 | } | |
89 | ||
90 | /** | |
91 | * Initialize the segment information. | |
92 | * @param {PathSegment} segment The segment to initialize. | |
93 | * @returns {void} | |
94 | */ | |
95 | initialize(segment) { | |
5422a9cc TL |
96 | const outdatedReadVariables = new Set(); |
97 | const freshReadVariables = new Set(); | |
eb39fafa DC |
98 | |
99 | for (const prevSegment of segment.prevSegments) { | |
100 | const info = this.info.get(prevSegment); | |
101 | ||
102 | if (info) { | |
5422a9cc TL |
103 | info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables); |
104 | info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables); | |
eb39fafa DC |
105 | } |
106 | } | |
107 | ||
5422a9cc | 108 | this.info.set(segment, { outdatedReadVariables, freshReadVariables }); |
eb39fafa DC |
109 | } |
110 | ||
111 | /** | |
112 | * Mark a given variable as read on given segments. | |
113 | * @param {PathSegment[]} segments The segments that it read the variable on. | |
5422a9cc | 114 | * @param {Variable} variable The variable to be read. |
eb39fafa DC |
115 | * @returns {void} |
116 | */ | |
5422a9cc | 117 | markAsRead(segments, variable) { |
eb39fafa DC |
118 | for (const segment of segments) { |
119 | const info = this.info.get(segment); | |
120 | ||
121 | if (info) { | |
5422a9cc | 122 | info.freshReadVariables.add(variable); |
456be15e TL |
123 | |
124 | // If a variable is freshly read again, then it's no more out-dated. | |
5422a9cc | 125 | info.outdatedReadVariables.delete(variable); |
eb39fafa DC |
126 | } |
127 | } | |
128 | } | |
129 | ||
130 | /** | |
5422a9cc | 131 | * Move `freshReadVariables` to `outdatedReadVariables`. |
eb39fafa DC |
132 | * @param {PathSegment[]} segments The segments to process. |
133 | * @returns {void} | |
134 | */ | |
135 | makeOutdated(segments) { | |
136 | for (const segment of segments) { | |
137 | const info = this.info.get(segment); | |
138 | ||
139 | if (info) { | |
5422a9cc TL |
140 | info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables); |
141 | info.freshReadVariables.clear(); | |
eb39fafa DC |
142 | } |
143 | } | |
144 | } | |
145 | ||
146 | /** | |
147 | * Check if a given variable is outdated on the current segments. | |
148 | * @param {PathSegment[]} segments The current segments. | |
5422a9cc | 149 | * @param {Variable} variable The variable to check. |
eb39fafa DC |
150 | * @returns {boolean} `true` if the variable is outdated on the segments. |
151 | */ | |
5422a9cc | 152 | isOutdated(segments, variable) { |
eb39fafa DC |
153 | for (const segment of segments) { |
154 | const info = this.info.get(segment); | |
155 | ||
5422a9cc | 156 | if (info && info.outdatedReadVariables.has(variable)) { |
eb39fafa DC |
157 | return true; |
158 | } | |
159 | } | |
160 | return false; | |
161 | } | |
162 | } | |
163 | ||
164 | //------------------------------------------------------------------------------ | |
165 | // Rule Definition | |
166 | //------------------------------------------------------------------------------ | |
167 | ||
168 | module.exports = { | |
169 | meta: { | |
170 | type: "problem", | |
171 | ||
172 | docs: { | |
173 | description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`", | |
eb39fafa DC |
174 | recommended: false, |
175 | url: "https://eslint.org/docs/rules/require-atomic-updates" | |
176 | }, | |
177 | ||
178 | fixable: null, | |
609c276f TL |
179 | |
180 | schema: [{ | |
181 | type: "object", | |
182 | properties: { | |
183 | allowProperties: { | |
184 | type: "boolean", | |
185 | default: false | |
186 | } | |
187 | }, | |
188 | additionalProperties: false | |
189 | }], | |
eb39fafa DC |
190 | |
191 | messages: { | |
609c276f TL |
192 | nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.", |
193 | nonAtomicObjectUpdate: "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`." | |
eb39fafa DC |
194 | } |
195 | }, | |
196 | ||
197 | create(context) { | |
609c276f TL |
198 | const allowProperties = !!context.options[0] && context.options[0].allowProperties; |
199 | ||
eb39fafa DC |
200 | const sourceCode = context.getSourceCode(); |
201 | const assignmentReferences = new Map(); | |
202 | const segmentInfo = new SegmentInfo(); | |
203 | let stack = null; | |
204 | ||
205 | return { | |
206 | onCodePathStart(codePath) { | |
207 | const scope = context.getScope(); | |
208 | const shouldVerify = | |
209 | scope.type === "function" && | |
210 | (scope.block.async || scope.block.generator); | |
211 | ||
212 | stack = { | |
213 | upper: stack, | |
214 | codePath, | |
215 | referenceMap: shouldVerify ? createReferenceMap(scope) : null | |
216 | }; | |
217 | }, | |
218 | onCodePathEnd() { | |
219 | stack = stack.upper; | |
220 | }, | |
221 | ||
222 | // Initialize the segment information. | |
223 | onCodePathSegmentStart(segment) { | |
224 | segmentInfo.initialize(segment); | |
225 | }, | |
226 | ||
227 | // Handle references to prepare verification. | |
228 | Identifier(node) { | |
229 | const { codePath, referenceMap } = stack; | |
230 | const reference = referenceMap && referenceMap.get(node); | |
231 | ||
232 | // Ignore if this is not a valid variable reference. | |
233 | if (!reference) { | |
234 | return; | |
235 | } | |
eb39fafa DC |
236 | const variable = reference.resolved; |
237 | const writeExpr = getWriteExpr(reference); | |
238 | const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; | |
239 | ||
240 | // Add a fresh read variable. | |
241 | if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { | |
5422a9cc | 242 | segmentInfo.markAsRead(codePath.currentSegments, variable); |
eb39fafa DC |
243 | } |
244 | ||
245 | /* | |
246 | * Register the variable to verify after ESLint traversed the `writeExpr` node | |
247 | * if this reference is an assignment to a variable which is referred from other closure. | |
248 | */ | |
249 | if (writeExpr && | |
250 | writeExpr.parent.right === writeExpr && // ← exclude variable declarations. | |
251 | !isLocalVariableWithoutEscape(variable, isMemberAccess) | |
252 | ) { | |
253 | let refs = assignmentReferences.get(writeExpr); | |
254 | ||
255 | if (!refs) { | |
256 | refs = []; | |
257 | assignmentReferences.set(writeExpr, refs); | |
258 | } | |
259 | ||
260 | refs.push(reference); | |
261 | } | |
262 | }, | |
263 | ||
264 | /* | |
265 | * Verify assignments. | |
5422a9cc | 266 | * If the reference exists in `outdatedReadVariables` list, report it. |
eb39fafa DC |
267 | */ |
268 | ":expression:exit"(node) { | |
269 | const { codePath, referenceMap } = stack; | |
270 | ||
271 | // referenceMap exists if this is in a resumable function scope. | |
272 | if (!referenceMap) { | |
273 | return; | |
274 | } | |
275 | ||
276 | // Mark the read variables on this code path as outdated. | |
277 | if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | |
278 | segmentInfo.makeOutdated(codePath.currentSegments); | |
279 | } | |
280 | ||
281 | // Verify. | |
282 | const references = assignmentReferences.get(node); | |
283 | ||
284 | if (references) { | |
285 | assignmentReferences.delete(node); | |
286 | ||
287 | for (const reference of references) { | |
5422a9cc | 288 | const variable = reference.resolved; |
eb39fafa | 289 | |
5422a9cc | 290 | if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { |
609c276f TL |
291 | if (node.parent.left === reference.identifier) { |
292 | context.report({ | |
293 | node: node.parent, | |
294 | messageId: "nonAtomicUpdate", | |
295 | data: { | |
296 | value: variable.name | |
297 | } | |
298 | }); | |
299 | } else if (!allowProperties) { | |
300 | context.report({ | |
301 | node: node.parent, | |
302 | messageId: "nonAtomicObjectUpdate", | |
303 | data: { | |
304 | value: sourceCode.getText(node.parent.left), | |
305 | object: variable.name | |
306 | } | |
307 | }); | |
308 | } | |
309 | ||
eb39fafa DC |
310 | } |
311 | } | |
312 | } | |
313 | } | |
314 | }; | |
315 | } | |
316 | }; |