2 * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
4 * @author Toru Nagashima
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`.
14 function createReferenceMap(scope, outReferenceMap = new Map()) {
15 for (const reference of scope.references) {
16 if (reference.resolved === null) {
20 outReferenceMap.set(reference.identifier, reference);
22 for (const childScope of scope.childScopes) {
23 if (childScope.type !== "function") {
24 createReferenceMap(childScope, outReferenceMap);
28 return outReferenceMap;
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`.
37 function getWriteExpr(reference) {
38 if (reference.writeExpr) {
39 return reference.writeExpr;
41 let node = reference.identifier;
44 const t = node.parent.type;
46 if (t === "AssignmentExpression" && node.parent.left === node) {
47 return node.parent.right;
49 if (t === "MemberExpression" && node.parent.object === node) {
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.
66 function isLocalVariableWithoutEscape(variable, isMemberAccess) {
68 return false; // A global variable which was not defined.
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")) {
76 const functionScope = variable.scope.variableScope;
78 return variable.references.every(reference =>
79 reference.from.variableScope === functionScope);
84 this.info = new WeakMap();
88 * Initialize the segment information.
89 * @param {PathSegment} segment The segment to initialize.
93 const outdatedReadVariables = new Set();
94 const freshReadVariables = new Set();
96 for (const prevSegment of segment.prevSegments) {
97 const info = this.info.get(prevSegment);
100 info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables);
101 info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables);
105 this.info.set(segment, { outdatedReadVariables, freshReadVariables });
109 * Mark a given variable as read on given segments.
110 * @param {PathSegment[]} segments The segments that it read the variable on.
111 * @param {Variable} variable The variable to be read.
114 markAsRead(segments, variable) {
115 for (const segment of segments) {
116 const info = this.info.get(segment);
119 info.freshReadVariables.add(variable);
121 // If a variable is freshly read again, then it's no more out-dated.
122 info.outdatedReadVariables.delete(variable);
128 * Move `freshReadVariables` to `outdatedReadVariables`.
129 * @param {PathSegment[]} segments The segments to process.
132 makeOutdated(segments) {
133 for (const segment of segments) {
134 const info = this.info.get(segment);
137 info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables);
138 info.freshReadVariables.clear();
144 * Check if a given variable is outdated on the current segments.
145 * @param {PathSegment[]} segments The current segments.
146 * @param {Variable} variable The variable to check.
147 * @returns {boolean} `true` if the variable is outdated on the segments.
149 isOutdated(segments, variable) {
150 for (const segment of segments) {
151 const info = this.info.get(segment);
153 if (info && info.outdatedReadVariables.has(variable)) {
161 //------------------------------------------------------------------------------
163 //------------------------------------------------------------------------------
170 description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
171 category: "Possible Errors",
173 url: "https://eslint.org/docs/rules/require-atomic-updates"
180 nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
185 const sourceCode = context.getSourceCode();
186 const assignmentReferences = new Map();
187 const segmentInfo = new SegmentInfo();
191 onCodePathStart(codePath) {
192 const scope = context.getScope();
194 scope.type === "function" &&
195 (scope.block.async || scope.block.generator);
200 referenceMap: shouldVerify ? createReferenceMap(scope) : null
207 // Initialize the segment information.
208 onCodePathSegmentStart(segment) {
209 segmentInfo.initialize(segment);
212 // Handle references to prepare verification.
214 const { codePath, referenceMap } = stack;
215 const reference = referenceMap && referenceMap.get(node);
217 // Ignore if this is not a valid variable reference.
221 const variable = reference.resolved;
222 const writeExpr = getWriteExpr(reference);
223 const isMemberAccess = reference.identifier.parent.type === "MemberExpression";
225 // Add a fresh read variable.
226 if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) {
227 segmentInfo.markAsRead(codePath.currentSegments, variable);
231 * Register the variable to verify after ESLint traversed the `writeExpr` node
232 * if this reference is an assignment to a variable which is referred from other closure.
235 writeExpr.parent.right === writeExpr && // ← exclude variable declarations.
236 !isLocalVariableWithoutEscape(variable, isMemberAccess)
238 let refs = assignmentReferences.get(writeExpr);
242 assignmentReferences.set(writeExpr, refs);
245 refs.push(reference);
250 * Verify assignments.
251 * If the reference exists in `outdatedReadVariables` list, report it.
253 ":expression:exit"(node) {
254 const { codePath, referenceMap } = stack;
256 // referenceMap exists if this is in a resumable function scope.
261 // Mark the read variables on this code path as outdated.
262 if (node.type === "AwaitExpression" || node.type === "YieldExpression") {
263 segmentInfo.makeOutdated(codePath.currentSegments);
267 const references = assignmentReferences.get(node);
270 assignmentReferences.delete(node);
272 for (const reference of references) {
273 const variable = reference.resolved;
275 if (segmentInfo.isOutdated(codePath.currentSegments, variable)) {
278 messageId: "nonAtomicUpdate",
280 value: sourceCode.getText(node.parent.left)