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 | ;
|
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 | */
|
14 | function 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 | */
|
33 | function 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 | */
|
62 | function 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 |
|
78 | class 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 | // If a variable is freshly read again, then it's no more out-dated.
|
118 | info.outdatedReadVariableNames.delete(variableName);
|
119 | }
|
120 | }
|
121 | }
|
122 |
|
123 | /**
|
124 | * Move `freshReadVariableNames` to `outdatedReadVariableNames`.
|
125 | * @param {PathSegment[]} segments The segments to process.
|
126 | * @returns {void}
|
127 | */
|
128 | makeOutdated(segments) {
|
129 | for (const segment of segments) {
|
130 | const info = this.info.get(segment);
|
131 |
|
132 | if (info) {
|
133 | info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames);
|
134 | info.freshReadVariableNames.clear();
|
135 | }
|
136 | }
|
137 | }
|
138 |
|
139 | /**
|
140 | * Check if a given variable is outdated on the current segments.
|
141 | * @param {PathSegment[]} segments The current segments.
|
142 | * @param {string} variableName The variable name to check.
|
143 | * @returns {boolean} `true` if the variable is outdated on the segments.
|
144 | */
|
145 | isOutdated(segments, variableName) {
|
146 | for (const segment of segments) {
|
147 | const info = this.info.get(segment);
|
148 |
|
149 | if (info && info.outdatedReadVariableNames.has(variableName)) {
|
150 | return true;
|
151 | }
|
152 | }
|
153 | return false;
|
154 | }
|
155 | }
|
156 |
|
157 | //------------------------------------------------------------------------------
|
158 | // Rule Definition
|
159 | //------------------------------------------------------------------------------
|
160 |
|
161 | module.exports = {
|
162 | meta: {
|
163 | type: "problem",
|
164 |
|
165 | docs: {
|
166 | description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
|
167 | category: "Possible Errors",
|
168 | recommended: false,
|
169 | url: "https://eslint.org/docs/rules/require-atomic-updates"
|
170 | },
|
171 |
|
172 | fixable: null,
|
173 | schema: [],
|
174 |
|
175 | messages: {
|
176 | nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
|
177 | }
|
178 | },
|
179 |
|
180 | create(context) {
|
181 | const sourceCode = context.getSourceCode();
|
182 | const assignmentReferences = new Map();
|
183 | const segmentInfo = new SegmentInfo();
|
184 | let stack = null;
|
185 |
|
186 | return {
|
187 | onCodePathStart(codePath) {
|
188 | const scope = context.getScope();
|
189 | const shouldVerify =
|
190 | scope.type === "function" &&
|
191 | (scope.block.async || scope.block.generator);
|
192 |
|
193 | stack = {
|
194 | upper: stack,
|
195 | codePath,
|
196 | referenceMap: shouldVerify ? createReferenceMap(scope) : null
|
197 | };
|
198 | },
|
199 | onCodePathEnd() {
|
200 | stack = stack.upper;
|
201 | },
|
202 |
|
203 | // Initialize the segment information.
|
204 | onCodePathSegmentStart(segment) {
|
205 | segmentInfo.initialize(segment);
|
206 | },
|
207 |
|
208 | // Handle references to prepare verification.
|
209 | Identifier(node) {
|
210 | const { codePath, referenceMap } = stack;
|
211 | const reference = referenceMap && referenceMap.get(node);
|
212 |
|
213 | // Ignore if this is not a valid variable reference.
|
214 | if (!reference) {
|
215 | return;
|
216 | }
|
217 | const name = reference.identifier.name;
|
218 | const variable = reference.resolved;
|
219 | const writeExpr = getWriteExpr(reference);
|
220 | const isMemberAccess = reference.identifier.parent.type === "MemberExpression";
|
221 |
|
222 | // Add a fresh read variable.
|
223 | if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) {
|
224 | segmentInfo.markAsRead(codePath.currentSegments, name);
|
225 | }
|
226 |
|
227 | /*
|
228 | * Register the variable to verify after ESLint traversed the `writeExpr` node
|
229 | * if this reference is an assignment to a variable which is referred from other closure.
|
230 | */
|
231 | if (writeExpr &&
|
232 | writeExpr.parent.right === writeExpr && // ← exclude variable declarations.
|
233 | !isLocalVariableWithoutEscape(variable, isMemberAccess)
|
234 | ) {
|
235 | let refs = assignmentReferences.get(writeExpr);
|
236 |
|
237 | if (!refs) {
|
238 | refs = [];
|
239 | assignmentReferences.set(writeExpr, refs);
|
240 | }
|
241 |
|
242 | refs.push(reference);
|
243 | }
|
244 | },
|
245 |
|
246 | /*
|
247 | * Verify assignments.
|
248 | * If the reference exists in `outdatedReadVariableNames` list, report it.
|
249 | */
|
250 | ":expression:exit"(node) {
|
251 | const { codePath, referenceMap } = stack;
|
252 |
|
253 | // referenceMap exists if this is in a resumable function scope.
|
254 | if (!referenceMap) {
|
255 | return;
|
256 | }
|
257 |
|
258 | // Mark the read variables on this code path as outdated.
|
259 | if (node.type === "AwaitExpression" || node.type === "YieldExpression") {
|
260 | segmentInfo.makeOutdated(codePath.currentSegments);
|
261 | }
|
262 |
|
263 | // Verify.
|
264 | const references = assignmentReferences.get(node);
|
265 |
|
266 | if (references) {
|
267 | assignmentReferences.delete(node);
|
268 |
|
269 | for (const reference of references) {
|
270 | const name = reference.identifier.name;
|
271 |
|
272 | if (segmentInfo.isOutdated(codePath.currentSegments, name)) {
|
273 | context.report({
|
274 | node: node.parent,
|
275 | messageId: "nonAtomicUpdate",
|
276 | data: {
|
277 | value: sourceCode.getText(node.parent.left)
|
278 | }
|
279 | });
|
280 | }
|
281 | }
|
282 | }
|
283 | }
|
284 | };
|
285 | }
|
286 | };
|