| /** |
| * @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) |
| } |
| }); |
| } |
| } |
| } |
| } |
| }; |
| } |
| }; |