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