1/** 2 * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield` 3 * @author Teddy Katz 4 * @author Toru Nagashima 5 */ 6"use strict"; 7 8/** 9 * Make the map from identifiers to each reference. 10 * @param {escope.Scope} scope The scope to get references. 11 * @param {Map<Identifier, escope.Reference>} [outReferenceMap] The map from identifier nodes to each reference object. 12 * @returns {Map<Identifier, escope.Reference>} `referenceMap`. 13 */ 14function createReferenceMap(scope, outReferenceMap = new Map()) { 15 for (const reference of scope.references) { 16 outReferenceMap.set(reference.identifier, reference); 17 } 18 for (const childScope of scope.childScopes) { 19 if (childScope.type !== "function") { 20 createReferenceMap(childScope, outReferenceMap); 21 } 22 } 23 24 return outReferenceMap; 25} 26 27/** 28 * Get `reference.writeExpr` of a given reference. 29 * If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a` 30 * @param {escope.Reference} reference The reference to get. 31 * @returns {Expression|null} The `reference.writeExpr`. 32 */ 33function getWriteExpr(reference) { 34 if (reference.writeExpr) { 35 return reference.writeExpr; 36 } 37 let node = reference.identifier; 38 39 while (node) { 40 const t = node.parent.type; 41 42 if (t === "AssignmentExpression" && node.parent.left === node) { 43 return node.parent.right; 44 } 45 if (t === "MemberExpression" && node.parent.object === node) { 46 node = node.parent; 47 continue; 48 } 49 50 break; 51 } 52 53 return null; 54} 55 56/** 57 * Checks if an expression is a variable that can only be observed within the given function. 58 * @param {Variable|null} variable The variable to check 59 * @param {boolean} isMemberAccess If `true` then this is a member access. 60 * @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure. 61 */ 62function isLocalVariableWithoutEscape(variable, isMemberAccess) { 63 if (!variable) { 64 return false; // A global variable which was not defined. 65 } 66 67 // If the reference is a property access and the variable is a parameter, it handles the variable is not local. 68 if (isMemberAccess && variable.defs.some(d => d.type === "Parameter")) { 69 return false; 70 } 71 72 const functionScope = variable.scope.variableScope; 73 74 return variable.references.every(reference => 75 reference.from.variableScope === functionScope); 76} 77 78class SegmentInfo { 79 constructor() { 80 this.info = new WeakMap(); 81 } 82 83 /** 84 * Initialize the segment information. 85 * @param {PathSegment} segment The segment to initialize. 86 * @returns {void} 87 */ 88 initialize(segment) { 89 const outdatedReadVariableNames = new Set(); 90 const freshReadVariableNames = new Set(); 91 92 for (const prevSegment of segment.prevSegments) { 93 const info = this.info.get(prevSegment); 94 95 if (info) { 96 info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames); 97 info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames); 98 } 99 } 100 101 this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames }); 102 } 103 104 /** 105 * Mark a given variable as read on given segments. 106 * @param {PathSegment[]} segments The segments that it read the variable on. 107 * @param {string} variableName The variable name to be read. 108 * @returns {void} 109 */ 110 markAsRead(segments, variableName) { 111 for (const segment of segments) { 112 const info = this.info.get(segment); 113 114 if (info) { 115 info.freshReadVariableNames.add(variableName); 116 } 117 } 118 } 119 120 /** 121 * Move `freshReadVariableNames` to `outdatedReadVariableNames`. 122 * @param {PathSegment[]} segments The segments to process. 123 * @returns {void} 124 */ 125 makeOutdated(segments) { 126 for (const segment of segments) { 127 const info = this.info.get(segment); 128 129 if (info) { 130 info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames); 131 info.freshReadVariableNames.clear(); 132 } 133 } 134 } 135 136 /** 137 * Check if a given variable is outdated on the current segments. 138 * @param {PathSegment[]} segments The current segments. 139 * @param {string} variableName The variable name to check. 140 * @returns {boolean} `true` if the variable is outdated on the segments. 141 */ 142 isOutdated(segments, variableName) { 143 for (const segment of segments) { 144 const info = this.info.get(segment); 145 146 if (info && info.outdatedReadVariableNames.has(variableName)) { 147 return true; 148 } 149 } 150 return false; 151 } 152} 153 154//------------------------------------------------------------------------------ 155// Rule Definition 156//------------------------------------------------------------------------------ 157 158module.exports = { 159 meta: { 160 type: "problem", 161 162 docs: { 163 description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`", 164 category: "Possible Errors", 165 recommended: false, 166 url: "https://eslint.org/docs/rules/require-atomic-updates" 167 }, 168 169 fixable: null, 170 schema: [], 171 172 messages: { 173 nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`." 174 } 175 }, 176 177 create(context) { 178 const sourceCode = context.getSourceCode(); 179 const assignmentReferences = new Map(); 180 const segmentInfo = new SegmentInfo(); 181 let stack = null; 182 183 return { 184 onCodePathStart(codePath) { 185 const scope = context.getScope(); 186 const shouldVerify = 187 scope.type === "function" && 188 (scope.block.async || scope.block.generator); 189 190 stack = { 191 upper: stack, 192 codePath, 193 referenceMap: shouldVerify ? createReferenceMap(scope) : null 194 }; 195 }, 196 onCodePathEnd() { 197 stack = stack.upper; 198 }, 199 200 // Initialize the segment information. 201 onCodePathSegmentStart(segment) { 202 segmentInfo.initialize(segment); 203 }, 204 205 // Handle references to prepare verification. 206 Identifier(node) { 207 const { codePath, referenceMap } = stack; 208 const reference = referenceMap && referenceMap.get(node); 209 210 // Ignore if this is not a valid variable reference. 211 if (!reference) { 212 return; 213 } 214 const name = reference.identifier.name; 215 const variable = reference.resolved; 216 const writeExpr = getWriteExpr(reference); 217 const isMemberAccess = reference.identifier.parent.type === "MemberExpression"; 218 219 // Add a fresh read variable. 220 if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) { 221 segmentInfo.markAsRead(codePath.currentSegments, name); 222 } 223 224 /* 225 * Register the variable to verify after ESLint traversed the `writeExpr` node 226 * if this reference is an assignment to a variable which is referred from other closure. 227 */ 228 if (writeExpr && 229 writeExpr.parent.right === writeExpr && // ← exclude variable declarations. 230 !isLocalVariableWithoutEscape(variable, isMemberAccess) 231 ) { 232 let refs = assignmentReferences.get(writeExpr); 233 234 if (!refs) { 235 refs = []; 236 assignmentReferences.set(writeExpr, refs); 237 } 238 239 refs.push(reference); 240 } 241 }, 242 243 /* 244 * Verify assignments. 245 * If the reference exists in `outdatedReadVariableNames` list, report it. 246 */ 247 ":expression:exit"(node) { 248 const { codePath, referenceMap } = stack; 249 250 // referenceMap exists if this is in a resumable function scope. 251 if (!referenceMap) { 252 return; 253 } 254 255 // Mark the read variables on this code path as outdated. 256 if (node.type === "AwaitExpression" || node.type === "YieldExpression") { 257 segmentInfo.makeOutdated(codePath.currentSegments); 258 } 259 260 // Verify. 261 const references = assignmentReferences.get(node); 262 263 if (references) { 264 assignmentReferences.delete(node); 265 266 for (const reference of references) { 267 const name = reference.identifier.name; 268 269 if (segmentInfo.isOutdated(codePath.currentSegments, name)) { 270 context.report({ 271 node: node.parent, 272 messageId: "nonAtomicUpdate", 273 data: { 274 value: sourceCode.getText(node.parent.left) 275 } 276 }); 277 } 278 } 279 } 280 } 281 }; 282 } 283}; 284