@development-environment/linting
Version:
Automatically catches many issues as they happen
240 lines (206 loc) • 9.9 kB
JavaScript
/**
* @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
* @author Teddy Katz
*/
;
const astUtils = require("../util/ast-utils");
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
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)
}
});
}
/**
* 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,
{
seenSegments = new Set(),
openAssignmentsWithoutReads = new Set(),
openAssignmentsWithReads = new Set()
} = {}
) {
if (seenSegments.has(codePathSegment)) {
// An AssignmentExpression can't contain loops, so it's not necessary to reenter them with new state.
return;
}
expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => {
if (node.type === "AssignmentExpression") {
if (entering) {
(node.operator === "=" ? openAssignmentsWithoutReads : openAssignmentsWithReads).add(node);
} else {
openAssignmentsWithoutReads.delete(node);
openAssignmentsWithReads.delete(node);
}
} else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) {
[...openAssignmentsWithReads]
.filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction))
.forEach(reportAssignment);
openAssignmentsWithReads.clear();
} else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) {
[...openAssignmentsWithoutReads]
.filter(assignment => (
assignment.left !== node &&
assignment.left.type === node.type &&
astUtils.equalTokens(assignment.left, node, sourceCode)
))
.forEach(assignment => {
openAssignmentsWithoutReads.delete(assignment);
openAssignmentsWithReads.add(assignment);
});
}
});
codePathSegment.nextSegments.forEach(nextSegment => {
findOutdatedReads(
nextSegment,
surroundingFunction,
{
seenSegments: new Set(seenSegments).add(codePathSegment),
openAssignmentsWithoutReads: new Set(openAssignmentsWithoutReads),
openAssignmentsWithReads: new Set(openAssignmentsWithReads)
}
);
});
}
//----------------------------------------------------------------------
// 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]);
}
}
};
}
};