]>
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 | ||
34eeec05 | 168 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
169 | module.exports = { |
170 | meta: { | |
171 | type: "problem", | |
172 | ||
173 | docs: { | |
8f9d1d4d | 174 | description: "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", |
eb39fafa | 175 | recommended: false, |
f2a92ac6 | 176 | url: "https://eslint.org/docs/latest/rules/require-atomic-updates" |
eb39fafa DC |
177 | }, |
178 | ||
179 | fixable: null, | |
609c276f TL |
180 | |
181 | schema: [{ | |
182 | type: "object", | |
183 | properties: { | |
184 | allowProperties: { | |
185 | type: "boolean", | |
186 | default: false | |
187 | } | |
188 | }, | |
189 | additionalProperties: false | |
190 | }], | |
eb39fafa DC |
191 | |
192 | messages: { | |
609c276f TL |
193 | nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.", |
194 | nonAtomicObjectUpdate: "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`." | |
eb39fafa DC |
195 | } |
196 | }, | |
197 | ||
198 | create(context) { | |
609c276f TL |
199 | const allowProperties = !!context.options[0] && context.options[0].allowProperties; |
200 | ||
f2a92ac6 | 201 | const sourceCode = context.sourceCode; |
eb39fafa DC |
202 | const assignmentReferences = new Map(); |
203 | const segmentInfo = new SegmentInfo(); | |
204 | let stack = null; | |
205 | ||
206 | return { | |
f2a92ac6 DC |
207 | onCodePathStart(codePath, node) { |
208 | const scope = sourceCode.getScope(node); | |
eb39fafa DC |
209 | const shouldVerify = |
210 | scope.type === "function" && | |
211 | (scope.block.async || scope.block.generator); | |
212 | ||
213 | stack = { | |
214 | upper: stack, | |
215 | codePath, | |
216 | referenceMap: shouldVerify ? createReferenceMap(scope) : null | |
217 | }; | |
218 | }, | |
219 | onCodePathEnd() { | |
220 | stack = stack.upper; | |
221 | }, | |
222 | ||
223 | // Initialize the segment information. | |
224 | onCodePathSegmentStart(segment) { | |
225 | segmentInfo.initialize(segment); | |
226 | }, | |
227 | ||
228 | // Handle references to prepare verification. | |
229 | Identifier(node) { | |
230 | const { codePath, referenceMap } = stack; | |
231 | const reference = referenceMap && referenceMap.get(node); | |
232 | ||
233 | // Ignore if this is not a valid variable reference. | |
234 | if (!reference) { | |
235 | return; | |
236 | } | |
eb39fafa DC |
237 | const variable = reference.resolved; |
238 | const writeExpr = getWriteExpr(reference); | |
239 | const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; | |
240 | ||
241 | // Add a fresh read variable. | |
242 | if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { | |
5422a9cc | 243 | segmentInfo.markAsRead(codePath.currentSegments, variable); |
eb39fafa DC |
244 | } |
245 | ||
246 | /* | |
247 | * Register the variable to verify after ESLint traversed the `writeExpr` node | |
248 | * if this reference is an assignment to a variable which is referred from other closure. | |
249 | */ | |
250 | if (writeExpr && | |
251 | writeExpr.parent.right === writeExpr && // ← exclude variable declarations. | |
252 | !isLocalVariableWithoutEscape(variable, isMemberAccess) | |
253 | ) { | |
254 | let refs = assignmentReferences.get(writeExpr); | |
255 | ||
256 | if (!refs) { | |
257 | refs = []; | |
258 | assignmentReferences.set(writeExpr, refs); | |
259 | } | |
260 | ||
261 | refs.push(reference); | |
262 | } | |
263 | }, | |
264 | ||
265 | /* | |
266 | * Verify assignments. | |
5422a9cc | 267 | * If the reference exists in `outdatedReadVariables` list, report it. |
eb39fafa DC |
268 | */ |
269 | ":expression:exit"(node) { | |
270 | const { codePath, referenceMap } = stack; | |
271 | ||
272 | // referenceMap exists if this is in a resumable function scope. | |
273 | if (!referenceMap) { | |
274 | return; | |
275 | } | |
276 | ||
277 | // Mark the read variables on this code path as outdated. | |
278 | if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | |
279 | segmentInfo.makeOutdated(codePath.currentSegments); | |
280 | } | |
281 | ||
282 | // Verify. | |
283 | const references = assignmentReferences.get(node); | |
284 | ||
285 | if (references) { | |
286 | assignmentReferences.delete(node); | |
287 | ||
288 | for (const reference of references) { | |
5422a9cc | 289 | const variable = reference.resolved; |
eb39fafa | 290 | |
5422a9cc | 291 | if (segmentInfo.isOutdated(codePath.currentSegments, variable)) { |
609c276f TL |
292 | if (node.parent.left === reference.identifier) { |
293 | context.report({ | |
294 | node: node.parent, | |
295 | messageId: "nonAtomicUpdate", | |
296 | data: { | |
297 | value: variable.name | |
298 | } | |
299 | }); | |
300 | } else if (!allowProperties) { | |
301 | context.report({ | |
302 | node: node.parent, | |
303 | messageId: "nonAtomicObjectUpdate", | |
304 | data: { | |
305 | value: sourceCode.getText(node.parent.left), | |
306 | object: variable.name | |
307 | } | |
308 | }); | |
309 | } | |
310 | ||
eb39fafa DC |
311 | } |
312 | } | |
313 | } | |
314 | } | |
315 | }; | |
316 | } | |
317 | }; |