1 |
|
2 |
|
3 |
|
4 |
|
5 | "use strict";
|
6 |
|
7 | const astUtils = require("../util/ast-utils");
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | module.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 |
|
39 |
|
40 |
|
41 | const resolvedVariableCache = new WeakMap();
|
42 |
|
43 | |
44 |
|
45 |
|
46 |
|
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 |
|
61 |
|
62 |
|
63 |
|
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 |
|
72 |
|
73 |
|
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 |
|
91 |
|
92 |
|
93 |
|
94 |
|
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 |
|
113 |
|
114 |
|
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 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
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 |
|
234 |
|
235 |
|
236 |
|
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 |
|
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 | };
|