• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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};
284