Skip to content

Commit

Permalink
New: Add no-await-in-loop rule (#7563)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmote authored and kaicataldo committed Dec 9, 2016
1 parent 2cdfb4e commit 1d0d61d
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
1 change: 1 addition & 0 deletions conf/eslint.json
Expand Up @@ -4,6 +4,7 @@
"rules": {
"no-alert": "off",
"no-array-constructor": "off",
"no-await-in-loop": "off",
"no-bitwise": "off",
"no-caller": "off",
"no-case-declarations": "error",
Expand Down
74 changes: 74 additions & 0 deletions docs/rules/no-await-in-loop.md
@@ -0,0 +1,74 @@
# Disallow `await` inside of loops (no-await-in-loop)

Performing an operation on each element of an iterable is a common task. However, performing an
`await` as part of each operation is an indication that the program is not taking full advantage of
the parallelization benefits of `async`/`await`.

Usually, the code should be refactored to create all the promises at once, then get access to the
results using `Promise.all()`. Otherwise, each successive operation will not start until the
previous one has completed.

Concretely, the following function should be refactored as shown:

```js
async function foo(things) {
const results = [];
for (const thing of things) {
// Bad: each loop iteration is delayed until the entire asynchronous operation completes
results.push(await bar(thing));
}
return baz(results);
}
```

```js
async function foo(things) {
const results = [];
for (const thing of things) {
// Good: all asynchronous operations are immediately started.
results.push(bar(thing));
}
// Now that all the asynchronous operations are running, here we wait until they all complete.
return baz(await Promise.all(results));
}
```

## Rule Details

This rule disallows the use of `await` within loop bodies.

## Examples

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

```js
async function foo(things) {
const results = [];
for (const thing of things) {
// Good: all asynchronous operations are immediately started.
results.push(bar(thing));
}
// Now that all the asynchronous operations are running, here we wait until they all complete.
return baz(await Promise.all(results));
}
```

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

```js
async function foo(things) {
const results = [];
for (const thing of things) {
// Bad: each loop iteration is delayed until the entire asynchronous operation completes
results.push(await bar(thing));
}
return baz(results);
}
```

## When Not To Use It

In many cases the iterations of a loop are not actually independent of each-other. For example, the
output of one iteration might be used as the input to another. Or, loops may be used to retry
asynchronous operations that were unsuccessful. In such cases it makes sense to use `await` within a
loop and it is recommended to disable the rule via a standard ESLint disable comment.
75 changes: 75 additions & 0 deletions lib/rules/no-await-in-loop.js
@@ -0,0 +1,75 @@
/**
* @fileoverview Rule to disallow uses of await inside of loops.
* @author Nat Mote (nmote)
*/
"use strict";

// Node types which are considered loops.
const loopTypes = new Set([
"ForStatement",
"ForOfStatement",
"ForInStatement",
"WhileStatement",
"DoWhileStatement",
]);

// Node types at which we should stop looking for loops. For example, it is fine to declare an async
// function within a loop, and use await inside of that.
const boundaryTypes = new Set([
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
]);

module.exports = {
meta: {
docs: {
description: "disallow `await` inside of loops",
category: "Possible Errors",
recommended: false,
},
schema: [],
},
create(context) {
return {
AwaitExpression(node) {
const ancestors = context.getAncestors();

// Reverse so that we can traverse from the deepest node upwards.
ancestors.reverse();

// Create a set of all the ancestors plus this node so that we can check
// if this use of await appears in the body of the loop as opposed to
// the right-hand side of a for...of, for example.
const ancestorSet = new Set(ancestors).add(node);

for (let i = 0; i < ancestors.length; i++) {
const ancestor = ancestors[i];

if (boundaryTypes.has(ancestor.type)) {

// Short-circuit out if we encounter a boundary type. Loops above
// this do not matter.
return;
}
if (loopTypes.has(ancestor.type)) {

// Only report if we are actually in the body or another part that gets executed on
// every iteration.
if (
ancestorSet.has(ancestor.body) ||
ancestorSet.has(ancestor.test) ||
ancestorSet.has(ancestor.update)
) {
context.report({
node,
message: "Unexpected `await` inside a loop."
});
return;
}
}
}
},
};
}
};
69 changes: 69 additions & 0 deletions tests/lib/rules/no-await-in-loop.js
@@ -0,0 +1,69 @@
/**
* @fileoverview Tests for no-await-in-loop.
* @author Nat Mote (nmote)
*/

"use strict";

const rule = require("../../../lib/rules/no-await-in-loop"),
RuleTester = require("../../../lib/testers/rule-tester");

const message = "Unexpected `await` inside a loop.";

const ruleTester = new RuleTester({parserOptions: {ecmaVersion: "2017"}});

ruleTester.run("no-await-in-loop", rule, {
valid: [
"async function foo() { await bar; }",
"async function foo() { for (var bar in await baz) { } }",
"async function foo() { for (var bar of await baz) { } }",
"async function foo() { for (var bar = await baz in qux) {} }",

// While loops
"async function foo() { while (true) { async function foo() { await bar; } } }", // Blocked by a function declaration
// For loops
"async function foo() { for (var i = await bar; i < n; i++) { } }",

// Do while loops
"async function foo() { do { } while (bar); }",

// Blocked by a function expression
"async function foo() { while (true) { var y = async function() { await bar; } } }",

// Blocked by an arrow function
"async function foo() { while (true) { var y = async () => await foo; } }",
"async function foo() { while (true) { var y = async () => { await foo; } } }",

// Blocked by a class method
"async function foo() { while (true) { class Foo { async foo() { await bar; } } } }",

],
invalid: [

// While loops
{code: "async function foo() { while (baz) { await bar; } }", errors: [message]},
{code: "async function foo() { while (await foo()) { } }", errors: [message]},

// For of loops
{code: "async function foo() { for (var bar of baz) { await bar; } }", errors: [message]},
{code: "async function foo() { for (var bar of baz) await bar; }", errors: [message]},

// For in loops
{code: "async function foo() { for (var bar in baz) { await bar; } }", errors: [message]},

// For loops
{code: "async function foo() { for (var i; i < n; i++) { await bar; } }", errors: [message]},
{code: "async function foo() { for (var i; await foo(i); i++) { } }", errors: [message]},
{code: "async function foo() { for (var i; i < n; i = await bar) { } }", errors: [message]},

// Do while loops
{code: "async function foo() { do { await bar; } while (baz); }", errors: [message]},
{code: "async function foo() { do { } while (await bar); }", errors: [message]},

// Deep in a loop body
{code: "async function foo() { while (true) { if (bar) { foo(await bar); } } }", errors: [message]},

// Deep in a loop condition
{code: "async function foo() { while (xyz || 5 > await x) { } }", errors: [message]},
],
});

0 comments on commit 1d0d61d

Please sign in to comment.