--- /dev/null
+/**
+ * @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: false,
+ 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)
+ }
+ });
+ }
+ }
+ }
+ }
+ };
+ }
+};