123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- /**
- * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
- * @author Teddy Katz
- */
- "use strict";
-
- const astUtils = require("../util/ast-utils");
-
- //------------------------------------------------------------------------------
- // 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 identifierToSurroundingFunctionMap = new WeakMap();
- const expressionsByCodePathSegment = new Map();
-
- //----------------------------------------------------------------------
- // Helpers
- //----------------------------------------------------------------------
-
- const resolvedVariableCache = new WeakMap();
-
- /**
- * Gets the variable scope around this variable reference
- * @param {ASTNode} identifier An `Identifier` AST node
- * @returns {Scope|null} An escope Scope
- */
- function getScope(identifier) {
- for (let currentNode = identifier; currentNode; currentNode = currentNode.parent) {
- const scope = sourceCode.scopeManager.acquire(currentNode, true);
-
- if (scope) {
- return scope;
- }
- }
- return null;
- }
-
- /**
- * Resolves a given identifier to a given scope
- * @param {ASTNode} identifier An `Identifier` AST node
- * @param {Scope} scope An escope Scope
- * @returns {Variable|null} An escope Variable corresponding to the given identifier
- */
- function resolveVariableInScope(identifier, scope) {
- return scope.variables.find(variable => variable.name === identifier.name) ||
- (scope.upper ? resolveVariableInScope(identifier, scope.upper) : null);
- }
-
- /**
- * Resolves an identifier to a variable
- * @param {ASTNode} identifier An identifier node
- * @returns {Variable|null} The escope Variable that uses this identifier
- */
- function resolveVariable(identifier) {
- if (!resolvedVariableCache.has(identifier)) {
- const surroundingScope = getScope(identifier);
-
- if (surroundingScope) {
- resolvedVariableCache.set(identifier, resolveVariableInScope(identifier, surroundingScope));
- } else {
- resolvedVariableCache.set(identifier, null);
- }
- }
-
- return resolvedVariableCache.get(identifier);
- }
-
- /**
- * Checks if an expression is a variable that can only be observed within the given function.
- * @param {ASTNode} expression The expression to check
- * @param {ASTNode} surroundingFunction The function node
- * @returns {boolean} `true` if the expression is a variable which is local to the given function, and is never
- * referenced in a closure.
- */
- function isLocalVariableWithoutEscape(expression, surroundingFunction) {
- if (expression.type !== "Identifier") {
- return false;
- }
-
- const variable = resolveVariable(expression);
-
- if (!variable) {
- return false;
- }
-
- return variable.references.every(reference => identifierToSurroundingFunctionMap.get(reference.identifier) === surroundingFunction) &&
- variable.defs.every(def => identifierToSurroundingFunctionMap.get(def.name) === surroundingFunction);
- }
-
- /**
- * Reports an AssignmentExpression node that has a non-atomic update
- * @param {ASTNode} assignmentExpression The assignment that is potentially unsafe
- * @returns {void}
- */
- function reportAssignment(assignmentExpression) {
- context.report({
- node: assignmentExpression,
- messageId: "nonAtomicUpdate",
- data: {
- value: sourceCode.getText(assignmentExpression.left)
- }
- });
- }
-
- const alreadyReportedAssignments = new WeakSet();
-
- class AssignmentTrackerState {
- constructor({ openAssignmentsWithoutReads = new Set(), openAssignmentsWithReads = new Set() } = {}) {
- this.openAssignmentsWithoutReads = openAssignmentsWithoutReads;
- this.openAssignmentsWithReads = openAssignmentsWithReads;
- }
-
- copy() {
- return new AssignmentTrackerState({
- openAssignmentsWithoutReads: new Set(this.openAssignmentsWithoutReads),
- openAssignmentsWithReads: new Set(this.openAssignmentsWithReads)
- });
- }
-
- merge(other) {
- const initialAssignmentsWithoutReadsCount = this.openAssignmentsWithoutReads.size;
- const initialAssignmentsWithReadsCount = this.openAssignmentsWithReads.size;
-
- other.openAssignmentsWithoutReads.forEach(assignment => this.openAssignmentsWithoutReads.add(assignment));
- other.openAssignmentsWithReads.forEach(assignment => this.openAssignmentsWithReads.add(assignment));
-
- return this.openAssignmentsWithoutReads.size > initialAssignmentsWithoutReadsCount ||
- this.openAssignmentsWithReads.size > initialAssignmentsWithReadsCount;
- }
-
- enterAssignment(assignmentExpression) {
- (assignmentExpression.operator === "=" ? this.openAssignmentsWithoutReads : this.openAssignmentsWithReads).add(assignmentExpression);
- }
-
- exitAssignment(assignmentExpression) {
- this.openAssignmentsWithoutReads.delete(assignmentExpression);
- this.openAssignmentsWithReads.delete(assignmentExpression);
- }
-
- exitAwaitOrYield(node, surroundingFunction) {
- return [...this.openAssignmentsWithReads]
- .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction))
- .forEach(assignment => {
- if (!alreadyReportedAssignments.has(assignment)) {
- reportAssignment(assignment);
- alreadyReportedAssignments.add(assignment);
- }
- });
- }
-
- exitIdentifierOrMemberExpression(node) {
- [...this.openAssignmentsWithoutReads]
- .filter(assignment => (
- assignment.left !== node &&
- assignment.left.type === node.type &&
- astUtils.equalTokens(assignment.left, node, sourceCode)
- ))
- .forEach(assignment => {
- this.openAssignmentsWithoutReads.delete(assignment);
- this.openAssignmentsWithReads.add(assignment);
- });
- }
- }
-
- /**
- * If the control flow graph of a function enters an assignment expression, then does the
- * both of the following steps in order (possibly with other steps in between) before exiting the
- * assignment expression, then the assignment might be using an outdated value.
- * 1. Enters a read of the variable or property assigned in the expression (not necessary if operator assignment is used)
- * 2. Exits an `await` or `yield` expression
- *
- * This function checks for the outdated values and reports them.
- * @param {CodePathSegment} codePathSegment The current code path segment to traverse
- * @param {ASTNode} surroundingFunction The function node containing the code path segment
- * @returns {void}
- */
- function findOutdatedReads(
- codePathSegment,
- surroundingFunction,
- {
- stateBySegmentStart = new WeakMap(),
- stateBySegmentEnd = new WeakMap()
- } = {}
- ) {
- if (!stateBySegmentStart.has(codePathSegment)) {
- stateBySegmentStart.set(codePathSegment, new AssignmentTrackerState());
- }
-
- const currentState = stateBySegmentStart.get(codePathSegment).copy();
-
- expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => {
- if (node.type === "AssignmentExpression") {
- if (entering) {
- currentState.enterAssignment(node);
- } else {
- currentState.exitAssignment(node);
- }
- } else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) {
- currentState.exitAwaitOrYield(node, surroundingFunction);
- } else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) {
- currentState.exitIdentifierOrMemberExpression(node);
- }
- });
-
- stateBySegmentEnd.set(codePathSegment, currentState);
-
- codePathSegment.nextSegments.forEach(nextSegment => {
- if (stateBySegmentStart.has(nextSegment)) {
- if (!stateBySegmentStart.get(nextSegment).merge(currentState)) {
-
- /*
- * This segment has already been processed with the given set of inputs;
- * no need to do it again. After no new state is available to process
- * for any control flow segment in the graph, the analysis reaches a fixpoint and
- * traversal stops.
- */
- return;
- }
- } else {
- stateBySegmentStart.set(nextSegment, currentState.copy());
- }
- findOutdatedReads(
- nextSegment,
- surroundingFunction,
- { stateBySegmentStart, stateBySegmentEnd }
- );
- });
- }
-
- //----------------------------------------------------------------------
- // Public
- //----------------------------------------------------------------------
-
- const currentCodePathSegmentStack = [];
- let currentCodePathSegment = null;
- const functionStack = [];
-
- return {
- onCodePathStart() {
- currentCodePathSegmentStack.push(currentCodePathSegment);
- },
-
- onCodePathEnd(codePath, node) {
- currentCodePathSegment = currentCodePathSegmentStack.pop();
-
- if (astUtils.isFunction(node) && (node.async || node.generator)) {
- findOutdatedReads(codePath.initialSegment, node);
- }
- },
-
- onCodePathSegmentStart(segment) {
- currentCodePathSegment = segment;
- expressionsByCodePathSegment.set(segment, []);
- },
-
- "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression"(node) {
- expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: true, node });
- },
-
- "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression:exit"(node) {
- expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: false, node });
- },
-
- ":function"(node) {
- functionStack.push(node);
- },
-
- ":function:exit"() {
- functionStack.pop();
- },
-
- Identifier(node) {
- if (functionStack.length) {
- identifierToSurroundingFunctionMap.set(node, functionStack[functionStack.length - 1]);
- }
- }
- };
- }
- };
|