UNPKG

12.6 kBJavaScriptView Raw
1/**
2 * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
3 * @author Teddy Katz
4 */
5"use strict";
6
7const astUtils = require("../util/ast-utils");
8
9//------------------------------------------------------------------------------
10// Rule Definition
11//------------------------------------------------------------------------------
12
13module.exports = {
14 meta: {
15 type: "problem",
16
17 docs: {
18 description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
19 category: "Possible Errors",
20 recommended: false,
21 url: "https://eslint.org/docs/rules/require-atomic-updates"
22 },
23
24 fixable: null,
25 schema: [],
26
27 messages: {
28 nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
29 }
30 },
31
32 create(context) {
33 const sourceCode = context.getSourceCode();
34 const identifierToSurroundingFunctionMap = new WeakMap();
35 const expressionsByCodePathSegment = new Map();
36
37 //----------------------------------------------------------------------
38 // Helpers
39 //----------------------------------------------------------------------
40
41 const resolvedVariableCache = new WeakMap();
42
43 /**
44 * Gets the variable scope around this variable reference
45 * @param {ASTNode} identifier An `Identifier` AST node
46 * @returns {Scope|null} An escope Scope
47 */
48 function getScope(identifier) {
49 for (let currentNode = identifier; currentNode; currentNode = currentNode.parent) {
50 const scope = sourceCode.scopeManager.acquire(currentNode, true);
51
52 if (scope) {
53 return scope;
54 }
55 }
56 return null;
57 }
58
59 /**
60 * Resolves a given identifier to a given scope
61 * @param {ASTNode} identifier An `Identifier` AST node
62 * @param {Scope} scope An escope Scope
63 * @returns {Variable|null} An escope Variable corresponding to the given identifier
64 */
65 function resolveVariableInScope(identifier, scope) {
66 return scope.variables.find(variable => variable.name === identifier.name) ||
67 (scope.upper ? resolveVariableInScope(identifier, scope.upper) : null);
68 }
69
70 /**
71 * Resolves an identifier to a variable
72 * @param {ASTNode} identifier An identifier node
73 * @returns {Variable|null} The escope Variable that uses this identifier
74 */
75 function resolveVariable(identifier) {
76 if (!resolvedVariableCache.has(identifier)) {
77 const surroundingScope = getScope(identifier);
78
79 if (surroundingScope) {
80 resolvedVariableCache.set(identifier, resolveVariableInScope(identifier, surroundingScope));
81 } else {
82 resolvedVariableCache.set(identifier, null);
83 }
84 }
85
86 return resolvedVariableCache.get(identifier);
87 }
88
89 /**
90 * Checks if an expression is a variable that can only be observed within the given function.
91 * @param {ASTNode} expression The expression to check
92 * @param {ASTNode} surroundingFunction The function node
93 * @returns {boolean} `true` if the expression is a variable which is local to the given function, and is never
94 * referenced in a closure.
95 */
96 function isLocalVariableWithoutEscape(expression, surroundingFunction) {
97 if (expression.type !== "Identifier") {
98 return false;
99 }
100
101 const variable = resolveVariable(expression);
102
103 if (!variable) {
104 return false;
105 }
106
107 return variable.references.every(reference => identifierToSurroundingFunctionMap.get(reference.identifier) === surroundingFunction) &&
108 variable.defs.every(def => identifierToSurroundingFunctionMap.get(def.name) === surroundingFunction);
109 }
110
111 /**
112 * Reports an AssignmentExpression node that has a non-atomic update
113 * @param {ASTNode} assignmentExpression The assignment that is potentially unsafe
114 * @returns {void}
115 */
116 function reportAssignment(assignmentExpression) {
117 context.report({
118 node: assignmentExpression,
119 messageId: "nonAtomicUpdate",
120 data: {
121 value: sourceCode.getText(assignmentExpression.left)
122 }
123 });
124 }
125
126 const alreadyReportedAssignments = new WeakSet();
127
128 class AssignmentTrackerState {
129 constructor({ openAssignmentsWithoutReads = new Set(), openAssignmentsWithReads = new Set() } = {}) {
130 this.openAssignmentsWithoutReads = openAssignmentsWithoutReads;
131 this.openAssignmentsWithReads = openAssignmentsWithReads;
132 }
133
134 copy() {
135 return new AssignmentTrackerState({
136 openAssignmentsWithoutReads: new Set(this.openAssignmentsWithoutReads),
137 openAssignmentsWithReads: new Set(this.openAssignmentsWithReads)
138 });
139 }
140
141 merge(other) {
142 const initialAssignmentsWithoutReadsCount = this.openAssignmentsWithoutReads.size;
143 const initialAssignmentsWithReadsCount = this.openAssignmentsWithReads.size;
144
145 other.openAssignmentsWithoutReads.forEach(assignment => this.openAssignmentsWithoutReads.add(assignment));
146 other.openAssignmentsWithReads.forEach(assignment => this.openAssignmentsWithReads.add(assignment));
147
148 return this.openAssignmentsWithoutReads.size > initialAssignmentsWithoutReadsCount ||
149 this.openAssignmentsWithReads.size > initialAssignmentsWithReadsCount;
150 }
151
152 enterAssignment(assignmentExpression) {
153 (assignmentExpression.operator === "=" ? this.openAssignmentsWithoutReads : this.openAssignmentsWithReads).add(assignmentExpression);
154 }
155
156 exitAssignment(assignmentExpression) {
157 this.openAssignmentsWithoutReads.delete(assignmentExpression);
158 this.openAssignmentsWithReads.delete(assignmentExpression);
159 }
160
161 exitAwaitOrYield(node, surroundingFunction) {
162 return [...this.openAssignmentsWithReads]
163 .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction))
164 .forEach(assignment => {
165 if (!alreadyReportedAssignments.has(assignment)) {
166 reportAssignment(assignment);
167 alreadyReportedAssignments.add(assignment);
168 }
169 });
170 }
171
172 exitIdentifierOrMemberExpression(node) {
173 [...this.openAssignmentsWithoutReads]
174 .filter(assignment => (
175 assignment.left !== node &&
176 assignment.left.type === node.type &&
177 astUtils.equalTokens(assignment.left, node, sourceCode)
178 ))
179 .forEach(assignment => {
180 this.openAssignmentsWithoutReads.delete(assignment);
181 this.openAssignmentsWithReads.add(assignment);
182 });
183 }
184 }
185
186 /**
187 * If the control flow graph of a function enters an assignment expression, then does the
188 * both of the following steps in order (possibly with other steps in between) before exiting the
189 * assignment expression, then the assignment might be using an outdated value.
190 * 1. Enters a read of the variable or property assigned in the expression (not necessary if operator assignment is used)
191 * 2. Exits an `await` or `yield` expression
192 *
193 * This function checks for the outdated values and reports them.
194 * @param {CodePathSegment} codePathSegment The current code path segment to traverse
195 * @param {ASTNode} surroundingFunction The function node containing the code path segment
196 * @returns {void}
197 */
198 function findOutdatedReads(
199 codePathSegment,
200 surroundingFunction,
201 {
202 stateBySegmentStart = new WeakMap(),
203 stateBySegmentEnd = new WeakMap()
204 } = {}
205 ) {
206 if (!stateBySegmentStart.has(codePathSegment)) {
207 stateBySegmentStart.set(codePathSegment, new AssignmentTrackerState());
208 }
209
210 const currentState = stateBySegmentStart.get(codePathSegment).copy();
211
212 expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => {
213 if (node.type === "AssignmentExpression") {
214 if (entering) {
215 currentState.enterAssignment(node);
216 } else {
217 currentState.exitAssignment(node);
218 }
219 } else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) {
220 currentState.exitAwaitOrYield(node, surroundingFunction);
221 } else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) {
222 currentState.exitIdentifierOrMemberExpression(node);
223 }
224 });
225
226 stateBySegmentEnd.set(codePathSegment, currentState);
227
228 codePathSegment.nextSegments.forEach(nextSegment => {
229 if (stateBySegmentStart.has(nextSegment)) {
230 if (!stateBySegmentStart.get(nextSegment).merge(currentState)) {
231
232 /*
233 * This segment has already been processed with the given set of inputs;
234 * no need to do it again. After no new state is available to process
235 * for any control flow segment in the graph, the analysis reaches a fixpoint and
236 * traversal stops.
237 */
238 return;
239 }
240 } else {
241 stateBySegmentStart.set(nextSegment, currentState.copy());
242 }
243 findOutdatedReads(
244 nextSegment,
245 surroundingFunction,
246 { stateBySegmentStart, stateBySegmentEnd }
247 );
248 });
249 }
250
251 //----------------------------------------------------------------------
252 // Public
253 //----------------------------------------------------------------------
254
255 const currentCodePathSegmentStack = [];
256 let currentCodePathSegment = null;
257 const functionStack = [];
258
259 return {
260 onCodePathStart() {
261 currentCodePathSegmentStack.push(currentCodePathSegment);
262 },
263
264 onCodePathEnd(codePath, node) {
265 currentCodePathSegment = currentCodePathSegmentStack.pop();
266
267 if (astUtils.isFunction(node) && (node.async || node.generator)) {
268 findOutdatedReads(codePath.initialSegment, node);
269 }
270 },
271
272 onCodePathSegmentStart(segment) {
273 currentCodePathSegment = segment;
274 expressionsByCodePathSegment.set(segment, []);
275 },
276
277 "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression"(node) {
278 expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: true, node });
279 },
280
281 "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression:exit"(node) {
282 expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: false, node });
283 },
284
285 ":function"(node) {
286 functionStack.push(node);
287 },
288
289 ":function:exit"() {
290 functionStack.pop();
291 },
292
293 Identifier(node) {
294 if (functionStack.length) {
295 identifierToSurroundingFunctionMap.set(node, functionStack[functionStack.length - 1]);
296 }
297 }
298 };
299 }
300};