]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/require-atomic-updates.js
bump version to 7.12.1-1
[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) {
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};