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