| /** | |
|  * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield` | |
|  * @author Teddy Katz | |
|  * @author Toru Nagashima | |
|  */ | |
| "use strict"; | |
| 
 | |
| /** | |
|  * Make the map from identifiers to each reference. | |
|  * @param {escope.Scope} scope The scope to get references. | |
|  * @param {Map<Identifier, escope.Reference>} [outReferenceMap] The map from identifier nodes to each reference object. | |
|  * @returns {Map<Identifier, escope.Reference>} `referenceMap`. | |
|  */ | |
| function createReferenceMap(scope, outReferenceMap = new Map()) { | |
|     for (const reference of scope.references) { | |
|         if (reference.resolved === null) { | |
|             continue; | |
|         } | |
| 
 | |
|         outReferenceMap.set(reference.identifier, reference); | |
|     } | |
|     for (const childScope of scope.childScopes) { | |
|         if (childScope.type !== "function") { | |
|             createReferenceMap(childScope, outReferenceMap); | |
|         } | |
|     } | |
| 
 | |
|     return outReferenceMap; | |
| } | |
| 
 | |
| /** | |
|  * Get `reference.writeExpr` of a given reference. | |
|  * If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a` | |
|  * @param {escope.Reference} reference The reference to get. | |
|  * @returns {Expression|null} The `reference.writeExpr`. | |
|  */ | |
| function getWriteExpr(reference) { | |
|     if (reference.writeExpr) { | |
|         return reference.writeExpr; | |
|     } | |
|     let node = reference.identifier; | |
| 
 | |
|     while (node) { | |
|         const t = node.parent.type; | |
| 
 | |
|         if (t === "AssignmentExpression" && node.parent.left === node) { | |
|             return node.parent.right; | |
|         } | |
|         if (t === "MemberExpression" && node.parent.object === node) { | |
|             node = node.parent; | |
|             continue; | |
|         } | |
| 
 | |
|         break; | |
|     } | |
| 
 | |
|     return null; | |
| } | |
| 
 | |
| /** | |
|  * Checks if an expression is a variable that can only be observed within the given function. | |
|  * @param {Variable|null} variable The variable to check | |
|  * @param {boolean} isMemberAccess If `true` then this is a member access. | |
|  * @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure. | |
|  */ | |
| function isLocalVariableWithoutEscape(variable, isMemberAccess) { | |
|     if (!variable) { | |
|         return false; // A global variable which was not defined. | |
|     } | |
| 
 | |
|     // If the reference is a property access and the variable is a parameter, it handles the variable is not local. | |
|     if (isMemberAccess && variable.defs.some(d => d.type === "Parameter")) { | |
|         return false; | |
|     } | |
| 
 | |
|     const functionScope = variable.scope.variableScope; | |
| 
 | |
|     return variable.references.every(reference => | |
|         reference.from.variableScope === functionScope); | |
| } | |
| 
 | |
| /** | |
|  * Represents segment information. | |
|  */ | |
| class SegmentInfo { | |
|     constructor() { | |
|         this.info = new WeakMap(); | |
|     } | |
| 
 | |
|     /** | |
|      * Initialize the segment information. | |
|      * @param {PathSegment} segment The segment to initialize. | |
|      * @returns {void} | |
|      */ | |
|     initialize(segment) { | |
|         const outdatedReadVariables = new Set(); | |
|         const freshReadVariables = new Set(); | |
| 
 | |
|         for (const prevSegment of segment.prevSegments) { | |
|             const info = this.info.get(prevSegment); | |
| 
 | |
|             if (info) { | |
|                 info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables); | |
|                 info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables); | |
|             } | |
|         } | |
| 
 | |
|         this.info.set(segment, { outdatedReadVariables, freshReadVariables }); | |
|     } | |
| 
 | |
|     /** | |
|      * Mark a given variable as read on given segments. | |
|      * @param {PathSegment[]} segments The segments that it read the variable on. | |
|      * @param {Variable} variable The variable to be read. | |
|      * @returns {void} | |
|      */ | |
|     markAsRead(segments, variable) { | |
|         for (const segment of segments) { | |
|             const info = this.info.get(segment); | |
| 
 | |
|             if (info) { | |
|                 info.freshReadVariables.add(variable); | |
| 
 | |
|                 // If a variable is freshly read again, then it's no more out-dated. | |
|                 info.outdatedReadVariables.delete(variable); | |
|             } | |
|         } | |
|     } | |
| 
 | |
|     /** | |
|      * Move `freshReadVariables` to `outdatedReadVariables`. | |
|      * @param {PathSegment[]} segments The segments to process. | |
|      * @returns {void} | |
|      */ | |
|     makeOutdated(segments) { | |
|         for (const segment of segments) { | |
|             const info = this.info.get(segment); | |
| 
 | |
|             if (info) { | |
|                 info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables); | |
|                 info.freshReadVariables.clear(); | |
|             } | |
|         } | |
|     } | |
| 
 | |
|     /** | |
|      * Check if a given variable is outdated on the current segments. | |
|      * @param {PathSegment[]} segments The current segments. | |
|      * @param {Variable} variable The variable to check. | |
|      * @returns {boolean} `true` if the variable is outdated on the segments. | |
|      */ | |
|     isOutdated(segments, variable) { | |
|         for (const segment of segments) { | |
|             const info = this.info.get(segment); | |
| 
 | |
|             if (info && info.outdatedReadVariables.has(variable)) { | |
|                 return true; | |
|             } | |
|         } | |
|         return false; | |
|     } | |
| } | |
| 
 | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
|  | |
| /** @type {import('../shared/types').Rule} */ | |
| module.exports = { | |
|     meta: { | |
|         type: "problem", | |
| 
 | |
|         docs: { | |
|             description: "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`", | |
|             recommended: false, | |
|             url: "https://eslint.org/docs/latest/rules/require-atomic-updates" | |
|         }, | |
| 
 | |
|         fixable: null, | |
| 
 | |
|         schema: [{ | |
|             type: "object", | |
|             properties: { | |
|                 allowProperties: { | |
|                     type: "boolean", | |
|                     default: false | |
|                 } | |
|             }, | |
|             additionalProperties: false | |
|         }], | |
| 
 | |
|         messages: { | |
|             nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.", | |
|             nonAtomicObjectUpdate: "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`." | |
|         } | |
|     }, | |
| 
 | |
|     create(context) { | |
|         const allowProperties = !!context.options[0] && context.options[0].allowProperties; | |
| 
 | |
|         const sourceCode = context.sourceCode; | |
|         const assignmentReferences = new Map(); | |
|         const segmentInfo = new SegmentInfo(); | |
|         let stack = null; | |
| 
 | |
|         return { | |
|             onCodePathStart(codePath, node) { | |
|                 const scope = sourceCode.getScope(node); | |
|                 const shouldVerify = | |
|                     scope.type === "function" && | |
|                     (scope.block.async || scope.block.generator); | |
| 
 | |
|                 stack = { | |
|                     upper: stack, | |
|                     codePath, | |
|                     referenceMap: shouldVerify ? createReferenceMap(scope) : null, | |
|                     currentSegments: new Set() | |
|                 }; | |
|             }, | |
|             onCodePathEnd() { | |
|                 stack = stack.upper; | |
|             }, | |
| 
 | |
|             // Initialize the segment information. | |
|             onCodePathSegmentStart(segment) { | |
|                 segmentInfo.initialize(segment); | |
|                 stack.currentSegments.add(segment); | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentStart(segment) { | |
|                 stack.currentSegments.add(segment); | |
|             }, | |
| 
 | |
|             onUnreachableCodePathSegmentEnd(segment) { | |
|                 stack.currentSegments.delete(segment); | |
|             }, | |
| 
 | |
|             onCodePathSegmentEnd(segment) { | |
|                 stack.currentSegments.delete(segment); | |
|             }, | |
| 
 | |
| 
 | |
|             // Handle references to prepare verification. | |
|             Identifier(node) { | |
|                 const { referenceMap } = stack; | |
|                 const reference = referenceMap && referenceMap.get(node); | |
| 
 | |
|                 // Ignore if this is not a valid variable reference. | |
|                 if (!reference) { | |
|                     return; | |
|                 } | |
|                 const variable = reference.resolved; | |
|                 const writeExpr = getWriteExpr(reference); | |
|                 const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; | |
| 
 | |
|                 // Add a fresh read variable. | |
|                 if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { | |
|                     segmentInfo.markAsRead(stack.currentSegments, variable); | |
|                 } | |
| 
 | |
|                 /* | |
|                  * Register the variable to verify after ESLint traversed the `writeExpr` node | |
|                  * if this reference is an assignment to a variable which is referred from other closure. | |
|                  */ | |
|                 if (writeExpr && | |
|                     writeExpr.parent.right === writeExpr && // ← exclude variable declarations. | |
|                     !isLocalVariableWithoutEscape(variable, isMemberAccess) | |
|                 ) { | |
|                     let refs = assignmentReferences.get(writeExpr); | |
| 
 | |
|                     if (!refs) { | |
|                         refs = []; | |
|                         assignmentReferences.set(writeExpr, refs); | |
|                     } | |
| 
 | |
|                     refs.push(reference); | |
|                 } | |
|             }, | |
| 
 | |
|             /* | |
|              * Verify assignments. | |
|              * If the reference exists in `outdatedReadVariables` list, report it. | |
|              */ | |
|             ":expression:exit"(node) { | |
| 
 | |
|                 // referenceMap exists if this is in a resumable function scope. | |
|                 if (!stack.referenceMap) { | |
|                     return; | |
|                 } | |
| 
 | |
|                 // Mark the read variables on this code path as outdated. | |
|                 if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | |
|                     segmentInfo.makeOutdated(stack.currentSegments); | |
|                 } | |
| 
 | |
|                 // Verify. | |
|                 const references = assignmentReferences.get(node); | |
| 
 | |
|                 if (references) { | |
|                     assignmentReferences.delete(node); | |
| 
 | |
|                     for (const reference of references) { | |
|                         const variable = reference.resolved; | |
| 
 | |
|                         if (segmentInfo.isOutdated(stack.currentSegments, variable)) { | |
|                             if (node.parent.left === reference.identifier) { | |
|                                 context.report({ | |
|                                     node: node.parent, | |
|                                     messageId: "nonAtomicUpdate", | |
|                                     data: { | |
|                                         value: variable.name | |
|                                     } | |
|                                 }); | |
|                             } else if (!allowProperties) { | |
|                                 context.report({ | |
|                                     node: node.parent, | |
|                                     messageId: "nonAtomicObjectUpdate", | |
|                                     data: { | |
|                                         value: sourceCode.getText(node.parent.left), | |
|                                         object: variable.name | |
|                                     } | |
|                                 }); | |
|                             } | |
| 
 | |
|                         } | |
|                     } | |
|                 } | |
|             } | |
|         }; | |
|     } | |
| };
 |