Skip to content

Commit 81c3823

Browse files
authored
Fix: require-atomic-updates reports parameters (fixes #11723) (#11774)
1 parent aef8ea1 commit 81c3823

File tree

2 files changed

+95
-84
lines changed

2 files changed

+95
-84
lines changed

lib/rules/require-atomic-updates.js

+63-84
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,51 @@ function createReferenceMap(scope, outReferenceMap = new Map()) {
2424
return outReferenceMap;
2525
}
2626

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+
2756
/**
2857
* Checks if an expression is a variable that can only be observed within the given function.
29-
* @param {escope.Variable} variable The variable to check
58+
* @param {Variable|null} variable The variable to check
59+
* @param {boolean} isMemberAccess If `true` then this is a member access.
3060
* @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure.
3161
*/
32-
function isLocalVariableWithoutEscape(variable) {
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+
3372
const functionScope = variable.scope.variableScope;
3473

3574
return variable.references.every(reference =>
@@ -47,67 +86,64 @@ class SegmentInfo {
4786
* @returns {void}
4887
*/
4988
initialize(segment) {
50-
const outdatedReadVariables = new Set();
51-
const freshReadVariables = new Set();
89+
const outdatedReadVariableNames = new Set();
90+
const freshReadVariableNames = new Set();
5291

5392
for (const prevSegment of segment.prevSegments) {
5493
const info = this.info.get(prevSegment);
5594

5695
if (info) {
57-
info.outdatedReadVariables.forEach(Set.prototype.add, outdatedReadVariables);
58-
info.freshReadVariables.forEach(Set.prototype.add, freshReadVariables);
96+
info.outdatedReadVariableNames.forEach(Set.prototype.add, outdatedReadVariableNames);
97+
info.freshReadVariableNames.forEach(Set.prototype.add, freshReadVariableNames);
5998
}
6099
}
61100

62-
this.info.set(segment, { outdatedReadVariables, freshReadVariables });
101+
this.info.set(segment, { outdatedReadVariableNames, freshReadVariableNames });
63102
}
64103

65104
/**
66105
* Mark a given variable as read on given segments.
67106
* @param {PathSegment[]} segments The segments that it read the variable on.
68-
* @param {escope.Variable} variable The variable to be read.
107+
* @param {string} variableName The variable name to be read.
69108
* @returns {void}
70109
*/
71-
markAsRead(segments, variable) {
110+
markAsRead(segments, variableName) {
72111
for (const segment of segments) {
73112
const info = this.info.get(segment);
74113

75114
if (info) {
76-
info.freshReadVariables.add(variable);
115+
info.freshReadVariableNames.add(variableName);
77116
}
78117
}
79118
}
80119

81120
/**
82-
* Move `freshReadVariables` to `outdatedReadVariables`.
121+
* Move `freshReadVariableNames` to `outdatedReadVariableNames`.
83122
* @param {PathSegment[]} segments The segments to process.
84123
* @returns {void}
85124
*/
86125
makeOutdated(segments) {
87-
const vars = new Set();
88-
89126
for (const segment of segments) {
90127
const info = this.info.get(segment);
91128

92129
if (info) {
93-
info.freshReadVariables.forEach(Set.prototype.add, info.outdatedReadVariables);
94-
info.freshReadVariables.forEach(Set.prototype.add, vars);
95-
info.freshReadVariables.clear();
130+
info.freshReadVariableNames.forEach(Set.prototype.add, info.outdatedReadVariableNames);
131+
info.freshReadVariableNames.clear();
96132
}
97133
}
98134
}
99135

100136
/**
101137
* Check if a given variable is outdated on the current segments.
102138
* @param {PathSegment[]} segments The current segments.
103-
* @param {escope.Variable} variable The variable to check.
139+
* @param {string} variableName The variable name to check.
104140
* @returns {boolean} `true` if the variable is outdated on the segments.
105141
*/
106-
isOutdated(segments, variable) {
142+
isOutdated(segments, variableName) {
107143
for (const segment of segments) {
108144
const info = this.info.get(segment);
109145

110-
if (info && info.outdatedReadVariables.has(variable)) {
146+
if (info && info.outdatedReadVariableNames.has(variableName)) {
111147
return true;
112148
}
113149
}
@@ -140,69 +176,10 @@ module.exports = {
140176

141177
create(context) {
142178
const sourceCode = context.getSourceCode();
143-
const globalScope = context.getScope();
144-
const dummyVariables = new Map();
145179
const assignmentReferences = new Map();
146180
const segmentInfo = new SegmentInfo();
147181
let stack = null;
148182

149-
/**
150-
* Get the variable of a given reference.
151-
* If it's not defined, returns a dummy object.
152-
* @param {escope.Reference} reference The reference to get.
153-
* @returns {escope.Variable} The variable of the reference.
154-
*/
155-
function getVariable(reference) {
156-
if (reference.resolved) {
157-
return reference.resolved;
158-
}
159-
160-
// Get or create a dummy.
161-
const name = reference.identifier.name;
162-
let variable = dummyVariables.get(name);
163-
164-
if (!variable) {
165-
variable = {
166-
name,
167-
scope: globalScope,
168-
references: []
169-
};
170-
dummyVariables.set(name, variable);
171-
}
172-
variable.references.push(reference);
173-
174-
return variable;
175-
}
176-
177-
/**
178-
* Get `reference.writeExpr` of a given reference.
179-
* If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a`
180-
* @param {escope.Reference} reference The reference to get.
181-
* @returns {Expression|null} The `reference.writeExpr`.
182-
*/
183-
function getWriteExpr(reference) {
184-
if (reference.writeExpr) {
185-
return reference.writeExpr;
186-
}
187-
let node = reference.identifier;
188-
189-
while (node) {
190-
const t = node.parent.type;
191-
192-
if (t === "AssignmentExpression" && node.parent.left === node) {
193-
return node.parent.right;
194-
}
195-
if (t === "MemberExpression" && node.parent.object === node) {
196-
node = node.parent;
197-
continue;
198-
}
199-
200-
break;
201-
}
202-
203-
return null;
204-
}
205-
206183
return {
207184
onCodePathStart(codePath) {
208185
const scope = context.getScope();
@@ -234,12 +211,14 @@ module.exports = {
234211
if (!reference) {
235212
return;
236213
}
237-
const variable = getVariable(reference);
214+
const name = reference.identifier.name;
215+
const variable = reference.resolved;
238216
const writeExpr = getWriteExpr(reference);
217+
const isMemberAccess = reference.identifier.parent.type === "MemberExpression";
239218

240219
// Add a fresh read variable.
241220
if (reference.isRead() && !(writeExpr && writeExpr.parent.operator === "=")) {
242-
segmentInfo.markAsRead(codePath.currentSegments, variable);
221+
segmentInfo.markAsRead(codePath.currentSegments, name);
243222
}
244223

245224
/*
@@ -248,7 +227,7 @@ module.exports = {
248227
*/
249228
if (writeExpr &&
250229
writeExpr.parent.right === writeExpr && // ← exclude variable declarations.
251-
!isLocalVariableWithoutEscape(variable)
230+
!isLocalVariableWithoutEscape(variable, isMemberAccess)
252231
) {
253232
let refs = assignmentReferences.get(writeExpr);
254233

@@ -263,7 +242,7 @@ module.exports = {
263242

264243
/*
265244
* Verify assignments.
266-
* If the reference exists in `outdatedReadVariables` list, report it.
245+
* If the reference exists in `outdatedReadVariableNames` list, report it.
267246
*/
268247
":expression:exit"(node) {
269248
const { codePath, referenceMap } = stack;
@@ -285,9 +264,9 @@ module.exports = {
285264
assignmentReferences.delete(node);
286265

287266
for (const reference of references) {
288-
const variable = getVariable(reference);
267+
const name = reference.identifier.name;
289268

290-
if (segmentInfo.isOutdated(codePath.currentSegments, variable)) {
269+
if (segmentInfo.isOutdated(codePath.currentSegments, name)) {
291270
context.report({
292271
node: node.parent,
293272
messageId: "nonAtomicUpdate",

tests/lib/rules/require-atomic-updates.js

+32
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,27 @@ ruleTester.run("require-atomic-updates", rule, {
129129
await doElse();
130130
}
131131
}
132+
`,
133+
134+
// https://github.com/eslint/eslint/issues/11723
135+
`
136+
async function f(foo) {
137+
let bar = await get(foo.id);
138+
bar.prop = foo.prop;
139+
}
140+
`,
141+
`
142+
async function f(foo) {
143+
let bar = await get(foo.id);
144+
foo = bar.prop;
145+
}
146+
`,
147+
`
148+
async function f() {
149+
let foo = {}
150+
let bar = await get(foo.id);
151+
foo.prop = bar.prop;
152+
}
132153
`
133154
],
134155

@@ -228,6 +249,17 @@ ruleTester.run("require-atomic-updates", rule, {
228249
{
229250
code: "let foo = 0; async function x() { foo = (a ? b ? c ? d ? foo : e : f : g : h) + await bar; if (baz); }",
230251
errors: [VARIABLE_ERROR]
252+
},
253+
254+
// https://github.com/eslint/eslint/issues/11723
255+
{
256+
code: `
257+
async function f(foo) {
258+
let buz = await get(foo.id);
259+
foo.bar = buz.bar;
260+
}
261+
`,
262+
errors: [STATIC_PROPERTY_ERROR]
231263
}
232264
]
233265
});

0 commit comments

Comments
 (0)