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