| /** | |
| * @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) { | |
| 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); | |
| } | |
| class SegmentInfo { | |
| constructor() { | |
| this.info = new WeakMap(); | |
| } | |
| /** | |
| * Initialize the segment information. | |
| * @param {PathSegment} segment The segment to initialize. | |
| * @returns {void} | |
| */ | |
| initialize(segment) { | |
| const outdatedReadVariableNames = new Set(); | |
| const freshReadVariableNames = new Set(); | |
| for (const prevSegment of segment.prevSegments) { | |
| const info = this.info.get(prevSegment); | |
| if (info) { | |
| info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames); | |
| info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames); | |
| } | |
| } | |
| this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames }); | |
| } | |
| /** | |
| * Mark a given variable as read on given segments. | |
| * @param {PathSegment[]} segments The segments that it read the variable on. | |
| * @param {string} variableName The variable name to be read. | |
| * @returns {void} | |
| */ | |
| markAsRead(segments, variableName) { | |
| for (const segment of segments) { | |
| const info = this.info.get(segment); | |
| if (info) { | |
| info.freshReadVariableNames.add(variableName); | |
| } | |
| } | |
| } | |
| /** | |
| * Move `freshReadVariableNames` to `outdatedReadVariableNames`. | |
| * @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.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames); | |
| info.freshReadVariableNames.clear(); | |
| } | |
| } | |
| } | |
| /** | |
| * Check if a given variable is outdated on the current segments. | |
| * @param {PathSegment[]} segments The current segments. | |
| * @param {string} variableName The variable name to check. | |
| * @returns {boolean} `true` if the variable is outdated on the segments. | |
| */ | |
| isOutdated(segments, variableName) { | |
| for (const segment of segments) { | |
| const info = this.info.get(segment); | |
| if (info && info.outdatedReadVariableNames.has(variableName)) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } | |
| //------------------------------------------------------------------------------ | |
| // Rule Definition | |
| //------------------------------------------------------------------------------ | |
| module.exports = { | |
| meta: { | |
| type: "problem", | |
| docs: { | |
| description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`", | |
| category: "Possible Errors", | |
| recommended: true, | |
| url: "https://eslint.org/docs/rules/require-atomic-updates" | |
| }, | |
| fixable: null, | |
| schema: [], | |
| messages: { | |
| nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`." | |
| } | |
| }, | |
| create(context) { | |
| const sourceCode = context.getSourceCode(); | |
| const assignmentReferences = new Map(); | |
| const segmentInfo = new SegmentInfo(); | |
| let stack = null; | |
| return { | |
| onCodePathStart(codePath) { | |
| const scope = context.getScope(); | |
| const shouldVerify = | |
| scope.type === "function" && | |
| (scope.block.async || scope.block.generator); | |
| stack = { | |
| upper: stack, | |
| codePath, | |
| referenceMap: shouldVerify ? createReferenceMap(scope) : null | |
| }; | |
| }, | |
| onCodePathEnd() { | |
| stack = stack.upper; | |
| }, | |
| // Initialize the segment information. | |
| onCodePathSegmentStart(segment) { | |
| segmentInfo.initialize(segment); | |
| }, | |
| // Handle references to prepare verification. | |
| Identifier(node) { | |
| const { codePath, referenceMap } = stack; | |
| const reference = referenceMap && referenceMap.get(node); | |
| // Ignore if this is not a valid variable reference. | |
| if (!reference) { | |
| return; | |
| } | |
| const name = reference.identifier.name; | |
| 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(codePath.currentSegments, name); | |
| } | |
| /* | |
| * 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 clausure. | |
| */ | |
| 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 `outdatedReadVariableNames` list, report it. | |
| */ | |
| ":expression:exit"(node) { | |
| const { codePath, referenceMap } = stack; | |
| // referenceMap exists if this is in a resumable function scope. | |
| if (!referenceMap) { | |
| return; | |
| } | |
| // Mark the read variables on this code path as outdated. | |
| if (node.type === "AwaitExpression" || node.type === "YieldExpression") { | |
| segmentInfo.makeOutdated(codePath.currentSegments); | |
| } | |
| // Verify. | |
| const references = assignmentReferences.get(node); | |
| if (references) { | |
| assignmentReferences.delete(node); | |
| for (const reference of references) { | |
| const name = reference.identifier.name; | |
| if (segmentInfo.isOutdated(codePath.currentSegments, name)) { | |
| context.report({ | |
| node: node.parent, | |
| messageId: "nonAtomicUpdate", | |
| data: { | |
| value: sourceCode.getText(node.parent.left) | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| }; | |
| } | |
| }; |