300 lines
12 KiB
JavaScript
300 lines
12 KiB
JavaScript
/**
|
|
* @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]);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|