Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add allowProperties option to require-atomic-updates #15238

Merged
merged 8 commits into from Nov 21, 2021
Merged
129 changes: 107 additions & 22 deletions docs/rules/require-atomic-updates.md
Expand Up @@ -38,32 +38,50 @@ Promise.all([getPageLength(1), getPageLength(2)]).then(pageLengths => {

## Rule Details

This rule aims to report assignments to variables or properties where all of the following are true:
This rule aims to report assignments to variables or properties in cases where the assignments may be based on outdated values.

* A variable or property is reassigned to a new value which is based on its old value.
* A `yield` or `await` expression interrupts the assignment after the old value is read, and before the new value is set.
* The rule cannot easily verify that the assignment is safe (e.g. if an assigned variable is local and would not be readable from anywhere else while the function is paused).
### Variables

This rule reports an assignment to a variable when it detects the following execution flow in a generator or async function:

1. The variable is read.
2. A `yield` or `await` pauses the function.
3. After the function is resumed, a value is assigned to the variable.
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

The assignment in step 3 is reported because this flow indicates that the assignment is based on the possibly outdated value of the variable from step 1, as the variable may have been reassigned elsewhere while the function was paused in step 2.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this a bit:

Suggested change
The assignment in step 3 is reported because this flow indicates that the assignment is based on the possibly outdated value of the variable from step 1, as the variable may have been reassigned elsewhere while the function was paused in step 2.
The assignment in step 3 is reported because it may be incorrectly calculated because the value of the variable from step 1 may have changed between steps 2 and 3.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It may also be helpful to explicitly call out the difference between local and global variables hazards with regards to this rule.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it may be incorrectly calculated

I wanted to avoid the word "calculated", as that may be interpreted as if there must be a non-trivial expression that calculates the new value, and that the old value must be directly used in that expression (for example, result = result + await something). We had complaints that assignments such as x = "foo" should never be reported because there is no calculation, and the old value does not contribute to the new value. This rule reports any read x -> await -> write x flow, even if the write is a trivial x = "foo", because the old value may have been used in a condition that determines whether or not x = "foo" should be executed.

may have changed between steps 2 and 3

This could be interpreted as:

x; // step 1

await something; // step 2

/*
    some code between step 2 and step 3, `x` is changed here
*/

x = y; // step 3

I wanted to emphasize that x may have changed outside of this execution flow, while it was awaiting in step 2.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe “resolved” then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated this paragraph now, please take a look.


Note that the rule does not report the aforementioned assignment in any of the following cases:
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

* If the variable is read again between steps 2 and 3.
* If the variable cannot be accessed while the function is paused (e.g., if it's a local variable).
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved

Examples of **incorrect** code for this rule:

```js
/* eslint require-atomic-updates: error */

let result;
async function foo() {
result += await somethingElse;

result = result + await somethingElse;
async function foo() {
result += await something;
}

result = result + doSomething(await somethingElse);
async function bar() {
result = result + await something;
}

function* bar() {
result += yield;
async function baz() {
result = result + doSomething(await somethingElse);
}

result = result + (yield somethingElse);
async function qux() {
if (!result) {
result = await initialize();
}
}

result = result + doSomething(yield somethingElse);
function* generator() {
result += yield;
}
```

Expand All @@ -73,22 +91,89 @@ Examples of **correct** code for this rule:
/* eslint require-atomic-updates: error */

let result;
async function foo() {
result = await somethingElse + result;

let tmp = await somethingElse;
result += tmp;
async function foobar() {
result = await something + result;
}

async function baz() {
const tmp = doSomething(await somethingElse);
result += tmp;
}

async function qux() {
if (!result) {
const tmp = await initialize();
if (!result) {
result = tmp;
}
}
}

async function quux() {
let localVariable = 0;
localVariable += await something;
}

function* generator() {
result = (yield) + result;
}
```

### Properties

This rule reports an assignment to a property through a variable when it detects the following execution flow in a generator or async function:

1. The variable is read. This can be reading a property through the variable.
mdjermanovic marked this conversation as resolved.
Show resolved Hide resolved
2. A `yield` or `await` pauses the function.
3. After the function is resumed, a value is assigned to a property through the variable.

let localVariable = 0;
localVariable += await somethingElse;
This logic is similar to the logic for variables, but stricter because the property in step 3 doesn't have to be the same as the property in step 1. It is assumed that the flow depends on the state of the object as a whole.

Example of **incorrect** code for this rule:

```js
/* eslint require-atomic-updates: error */

async function foo(obj) {
if (!obj.done) {
obj.something = await getSomething();
}
}
```

function* bar() {
result = (yield) + result;
Example of **correct** code for this rule:

result = (yield somethingElse) + result;
```js
/* eslint require-atomic-updates: error */

async function foo(obj) {
if (!obj.done) {
const tmp = await getSomething();
if (!obj.done) {
obj.something = tmp;
}
}
}
```

## Options

This rule has an object option:

* `"allowProperties"`: When set to `true`, the rule does not report assignments to properties. Default is `false`.

### allowProperties

Example of **correct** code for this rule with the `{ "allowProperties": true }` option:

```js
/* eslint require-atomic-updates: ["error", { "allowProperties": true }] */

result = doSomething(yield somethingElse, result);
async function foo(obj) {
if (!obj.done) {
obj.something = await getSomething();
}
}
```

Expand Down
16 changes: 14 additions & 2 deletions lib/rules/require-atomic-updates.js
Expand Up @@ -176,7 +176,17 @@ module.exports = {
},

fixable: null,
schema: [],

schema: [{
type: "object",
properties: {
allowProperties: {
type: "boolean",
default: false
}
},
additionalProperties: false
}],

messages: {
nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.",
Expand All @@ -185,6 +195,8 @@ module.exports = {
},

create(context) {
const allowProperties = !!context.options[0] && context.options[0].allowProperties;

const sourceCode = context.getSourceCode();
const assignmentReferences = new Map();
const segmentInfo = new SegmentInfo();
Expand Down Expand Up @@ -284,7 +296,7 @@ module.exports = {
value: variable.name
}
});
} else {
} else if (!allowProperties) {
context.report({
node: node.parent,
messageId: "nonAtomicObjectUpdate",
Expand Down
117 changes: 116 additions & 1 deletion tests/lib/rules/require-atomic-updates.js
Expand Up @@ -214,7 +214,29 @@ ruleTester.run("require-atomic-updates", rule, {
await a;
b = 1;
}
`
`,

// allowProperties
{
code: `
async function a(foo) {
if (foo.bar) {
foo.bar = await something;
}
}
`,
options: [{ allowProperties: true }]
},
{
code: `
function* g(foo) {
baz = foo.bar;
yield something;
foo.bar = 1;
}
`,
options: [{ allowProperties: true }]
}
],

invalid: [
Expand Down Expand Up @@ -358,6 +380,99 @@ ruleTester.run("require-atomic-updates", rule, {
line: 8
}
]
},

// allowProperties
{
code: `
async function a(foo) {
if (foo.bar) {
foo.bar = await something;
}
}
`,
errors: [STATIC_PROPERTY_ERROR]
},
{
code: `
function* g(foo) {
baz = foo.bar;
yield something;
foo.bar = 1;
}
`,
errors: [STATIC_PROPERTY_ERROR]
},
{
code: `
async function a(foo) {
if (foo.bar) {
foo.bar = await something;
}
}
`,
options: [{}],
errors: [STATIC_PROPERTY_ERROR]

},
{
code: `
function* g(foo) {
baz = foo.bar;
yield something;
foo.bar = 1;
}
`,
options: [{}],
errors: [STATIC_PROPERTY_ERROR]
},
{
code: `
async function a(foo) {
if (foo.bar) {
foo.bar = await something;
}
}
`,
options: [{ allowProperties: false }],
errors: [STATIC_PROPERTY_ERROR]

},
{
code: `
function* g(foo) {
baz = foo.bar;
yield something;
foo.bar = 1;
}
`,
options: [{ allowProperties: false }],
errors: [STATIC_PROPERTY_ERROR]
},
{
code: `
let foo;
async function a() {
if (foo) {
foo = await something;
}
}
`,
options: [{ allowProperties: true }],
errors: [VARIABLE_ERROR]

},
{
code: `
let foo;
function* g() {
baz = foo;
yield something;
foo = 1;
}
`,
options: [{ allowProperties: true }],
errors: [VARIABLE_ERROR]
}
]
});