123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- /**
- * @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/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.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 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, 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) {
- 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 variable = reference.resolved;
- if (segmentInfo.isOutdated(codePath.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
- }
- });
- }
- }
- }
- }
- }
- };
- }
- };
|