.gitignore added
[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         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 };