284 lines
9.5 KiB
JavaScript
284 lines
9.5 KiB
JavaScript
|
/**
|
||
|
* @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)
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
};
|