]>
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); | |
116 | } | |
117 | } | |
118 | } | |
119 | ||
120 | /** | |
121 | * Move `freshReadVariableNames` to `outdatedReadVariableNames`. | |
122 | * @param {PathSegment[]} segments The segments to process. | |
123 | * @returns {void} | |
124 | */ | |
125 | makeOutdated(segments) { | |
126 | for (const segment of segments) { | |
127 | const info = this.info.get(segment); | |
128 | ||
129 | if (info) { | |
130 | info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames); | |
131 | info.freshReadVariableNames.clear(); | |
132 | } | |
133 | } | |
134 | } | |
135 | ||
136 | /** | |
137 | * Check if a given variable is outdated on the current segments. | |
138 | * @param {PathSegment[]} segments The current segments. | |
139 | * @param {string} variableName The variable name to check. | |
140 | * @returns {boolean} `true` if the variable is outdated on the segments. | |
141 | */ | |
142 | isOutdated(segments, variableName) { | |
143 | for (const segment of segments) { | |
144 | const info = this.info.get(segment); | |
145 | ||
146 | if (info && info.outdatedReadVariableNames.has(variableName)) { | |
147 | return true; | |
148 | } | |
149 | } | |
150 | return false; | |
151 | } | |
152 | } | |
153 | ||
154 | //------------------------------------------------------------------------------ | |
155 | // Rule Definition | |
156 | //------------------------------------------------------------------------------ | |
157 | ||
158 | module.exports = { | |
159 | meta: { | |
160 | type: "problem", | |
161 | ||
162 | docs: { | |
163 | description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`", | |
164 | category: "Possible Errors", | |
165 | recommended: false, | |
166 | url: "https://eslint.org/docs/rules/require-atomic-updates" | |
167 | }, | |
168 | ||
169 | fixable: null, | |
170 | schema: [], | |
171 | ||
172 | messages: { | |
173 | nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`." | |
174 | } | |
175 | }, | |
176 | ||
177 | create(context) { | |
178 | const sourceCode = context.getSourceCode(); | |
179 | const assignmentReferences = new Map(); | |
180 | const segmentInfo = new SegmentInfo(); | |
181 | let stack = null; | |
182 | ||
183 | return { | |
184 | onCodePathStart(codePath) { | |
185 | const scope = context.getScope(); | |
186 | const shouldVerify = | |
187 | scope.type === "function" && | |
188 | (scope.block.async || scope.block.generator); | |
189 | ||
190 | stack = { | |
191 | upper: stack, | |
192 | codePath, | |
193 | referenceMap: shouldVerify ? createReferenceMap(scope) : null | |
194 | }; | |
195 | }, | |
196 | onCodePathEnd() { | |
197 | stack = stack.upper; | |
198 | }, | |
199 | ||
200 | // Initialize the segment information. | |
201 | onCodePathSegmentStart(segment) { | |
202 | segmentInfo.initialize(segment); | |
203 | }, | |
204 | ||
205 | // Handle references to prepare verification. | |
206 | Identifier(node) { | |
207 | const { codePath, referenceMap } = stack; | |
208 | const reference = referenceMap && referenceMap.get(node); | |
209 | ||
210 | // Ignore if this is not a valid variable reference. | |
211 | if (!reference) { | |
212 | return; | |
213 | } | |
214 | const name = reference.identifier.name; | |
215 | const variable = reference.resolved; | |
216 | const writeExpr = getWriteExpr(reference); | |
217 | const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; | |
218 | ||
219 | // Add a fresh read variable. | |
220 | if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { | |
221 | segmentInfo.markAsRead(codePath.currentSegments, name); | |
222 | } | |
223 | ||
224 | /* | |
225 | * Register the variable to verify after ESLint traversed the `writeExpr` node | |
226 | * if this reference is an assignment to a variable which is referred from other closure. | |
227 | */ | |
228 | if (writeExpr && | |
229 | writeExpr.parent.right === writeExpr && // ← exclude variable declarations. | |
230 | !isLocalVariableWithoutEscape(variable, isMemberAccess) | |
231 | ) { | |
232 | let refs = assignmentReferences.get(writeExpr); | |
233 | ||
234 | if (!refs) { | |
235 | refs = []; | |
236 | assignmentReferences.set(writeExpr, refs); | |
237 | } | |
238 | ||
239 | refs.push(reference); | |
240 | } | |
241 | }, | |
242 | ||
243 | /* | |
244 | * Verify assignments. | |
245 | * If the reference exists in `outdatedReadVariableNames` list, report it. | |
246 | */ | |
247 | ":expression:exit"(node) { | |
248 | const { codePath, referenceMap } = stack; | |
249 | ||
250 | // referenceMap exists if this is in a resumable function scope. | |
251 | if (!referenceMap) { | |
252 | return; | |
253 | } | |
254 | ||
255 | // Mark the read variables on this code path as outdated. | |
256 | if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | |
257 | segmentInfo.makeOutdated(codePath.currentSegments); | |
258 | } | |
259 | ||
260 | // Verify. | |
261 | const references = assignmentReferences.get(node); | |
262 | ||
263 | if (references) { | |
264 | assignmentReferences.delete(node); | |
265 | ||
266 | for (const reference of references) { | |
267 | const name = reference.identifier.name; | |
268 | ||
269 | if (segmentInfo.isOutdated(codePath.currentSegments, name)) { | |
270 | context.report({ | |
271 | node: node.parent, | |
272 | messageId: "nonAtomicUpdate", | |
273 | data: { | |
274 | value: sourceCode.getText(node.parent.left) | |
275 | } | |
276 | }); | |
277 | } | |
278 | } | |
279 | } | |
280 | } | |
281 | }; | |
282 | } | |
283 | }; |