You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

332 lines
11 KiB

/**
* @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) {
if (reference.resolved === null) {
continue;
}
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);
}
/**
* Represents segment information.
*/
class SegmentInfo {
constructor() {
this.info = new WeakMap();
}
/**
* Initialize the segment information.
* @param {PathSegment} segment The segment to initialize.
* @returns {void}
*/
initialize(segment) {
const outdatedReadVariables = new Set();
const freshReadVariables = new Set();
for (const prevSegment of segment.prevSegments) {
const info = this.info.get(prevSegment);
if (info) {
info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables);
info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables);
}
}
this.info.set(segment, { outdatedReadVariables, freshReadVariables });
}
/**
* Mark a given variable as read on given segments.
* @param {PathSegment[]} segments The segments that it read the variable on.
* @param {Variable} variable The variable to be read.
* @returns {void}
*/
markAsRead(segments, variable) {
for (const segment of segments) {
const info = this.info.get(segment);
if (info) {
info.freshReadVariables.add(variable);
// If a variable is freshly read again, then it's no more out-dated.
info.outdatedReadVariables.delete(variable);
}
}
}
/**
* Move `freshReadVariables` to `outdatedReadVariables`.
* @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.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables);
info.freshReadVariables.clear();
}
}
}
/**
* Check if a given variable is outdated on the current segments.
* @param {PathSegment[]} segments The current segments.
* @param {Variable} variable The variable to check.
* @returns {boolean} `true` if the variable is outdated on the segments.
*/
isOutdated(segments, variable) {
for (const segment of segments) {
const info = this.info.get(segment);
if (info && info.outdatedReadVariables.has(variable)) {
return true;
}
}
return false;
}
}
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('../shared/types').Rule} */
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
recommended: false,
url: "https://eslint.org/docs/latest/rules/require-atomic-updates"
},
fixable: null,
schema: [{
type: "object",
properties: {
allowProperties: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],
messages: {
nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.",
nonAtomicObjectUpdate: "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`."
}
},
create(context) {
const allowProperties = !!context.options[0] && context.options[0].allowProperties;
const sourceCode = context.sourceCode;
const assignmentReferences = new Map();
const segmentInfo = new SegmentInfo();
let stack = null;
return {
onCodePathStart(codePath, node) {
const scope = sourceCode.getScope(node);
const shouldVerify =
scope.type === "function" &&
(scope.block.async || scope.block.generator);
stack = {
upper: stack,
codePath,
referenceMap: shouldVerify ? createReferenceMap(scope) : null,
currentSegments: new Set()
};
},
onCodePathEnd() {
stack = stack.upper;
},
// Initialize the segment information.
onCodePathSegmentStart(segment) {
segmentInfo.initialize(segment);
stack.currentSegments.add(segment);
},
onUnreachableCodePathSegmentStart(segment) {
stack.currentSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
stack.currentSegments.delete(segment);
},
onCodePathSegmentEnd(segment) {
stack.currentSegments.delete(segment);
},
// Handle references to prepare verification.
Identifier(node) {
const { referenceMap } = stack;
const reference = referenceMap && referenceMap.get(node);
// Ignore if this is not a valid variable reference.
if (!reference) {
return;
}
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(stack.currentSegments, variable);
}
/*
* 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 closure.
*/
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 `outdatedReadVariables` list, report it.
*/
":expression:exit"(node) {
// referenceMap exists if this is in a resumable function scope.
if (!stack.referenceMap) {
return;
}
// Mark the read variables on this code path as outdated.
if (node.type === "AwaitExpression" || node.type === "YieldExpression") {
segmentInfo.makeOutdated(stack.currentSegments);
}
// Verify.
const references = assignmentReferences.get(node);
if (references) {
assignmentReferences.delete(node);
for (const reference of references) {
const variable = reference.resolved;
if (segmentInfo.isOutdated(stack.currentSegments, variable)) {
if (node.parent.left === reference.identifier) {
context.report({
node: node.parent,
messageId: "nonAtomicUpdate",
data: {
value: variable.name
}
});
} else if (!allowProperties) {
context.report({
node: node.parent,
messageId: "nonAtomicObjectUpdate",
data: {
value: sourceCode.getText(node.parent.left),
object: variable.name
}
});
}
}
}
}
}
};
}
};