-
-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request babel/eslint-plugin-babel#22 from nmote/await
Add no-await-in-loop rule.
- Loading branch information
Showing
3 changed files
with
180 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
/** | ||
* @fileoverview Rule to disallow uses of await inside of loops. | ||
* @author Nat Mote | ||
*/ | ||
"use strict"; | ||
|
||
// Node types which are considered loops. | ||
var loopTypes = { | ||
'ForStatement': true, | ||
'ForOfStatement': true, | ||
'ForInStatement': true, | ||
'WhileStatement': true, | ||
'DoWhileStatement': true, | ||
}; | ||
|
||
// 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. | ||
var boundaryTypes = { | ||
'FunctionDeclaration': true, | ||
'FunctionExpression': true, | ||
'ArrowFunctionExpression': true, | ||
}; | ||
|
||
module.exports = function(context) { | ||
return { | ||
// babel-eslint transpiles AwaitExpressions to YieldExpressions, but the actual node kind is | ||
// still available in _babelType. | ||
YieldExpression: function(node) { | ||
if (node._babelType === 'AwaitExpression') { | ||
var 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. | ||
// | ||
// Implement the set with an Array since there are likely to be very few | ||
// elements. An Object would not be appropriate since the elements are | ||
// not strings. | ||
var ancestorSet = [].concat(ancestors, [node]); | ||
var ancestorSetHas = function(element) { | ||
return ancestorSet.indexOf(element) !== -1; | ||
} | ||
for (var i = 0; i < ancestors.length; i++) { | ||
var ancestor = ancestors[i]; | ||
if (boundaryTypes.hasOwnProperty(ancestor.type)) { | ||
// Short-circuit out if we encounter a boundary type. Loops above | ||
// this do not matter. | ||
return; | ||
} | ||
if (loopTypes.hasOwnProperty(ancestor.type)) { | ||
// Only report if we are actually in the body or another part that gets executed on | ||
// every iteration. | ||
if ( | ||
ancestorSetHas(ancestor.body) || | ||
ancestorSetHas(ancestor.test) || | ||
ancestorSetHas(ancestor.update) | ||
) { | ||
context.report( | ||
node, | ||
'Avoid using await inside a loop. Consider refactoring to use Promise.all. If ' + | ||
'you are sure you want to do this, add `// eslint-disable-line ' + | ||
context.id + '` at the end of this line.' | ||
); | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
/** | ||
* @fileoverview Tests for no-await-in-loop. | ||
* @author Nat Mote | ||
*/ | ||
|
||
"use strict"; | ||
|
||
var rule = require("../rules/no-await-in-loop"), | ||
RuleTester = require('eslint').RuleTester; | ||
|
||
var features = { | ||
}; | ||
|
||
function test(code, errors){ | ||
var result = { | ||
code: code, | ||
parser: 'babel-eslint', | ||
ecmaFeatures: features, | ||
}; | ||
if (errors != null) { | ||
result.errors = errors; | ||
} | ||
return result; | ||
} | ||
|
||
var ruleName = 'babel/no-await-in-loop'; | ||
|
||
var message = 'Avoid using await inside a loop. Consider refactoring to use Promise.all. If ' + | ||
'you are sure you want to do this, add `// eslint-disable-line ' + | ||
ruleName + '` at the end of this line.' | ||
|
||
function ok(code) { | ||
return test(code); | ||
} | ||
|
||
function err(code) { | ||
return test(code, [message]); | ||
} | ||
|
||
// Construct an async function with the given body | ||
function fun(body) { | ||
return "async function foo() { " + body + " }"; | ||
} | ||
|
||
// Construct a loop | ||
function loop(kind, condition, body) { | ||
return kind + " (" + condition + ") { " + body + " }"; | ||
} | ||
|
||
// Construct a class with the given body | ||
function cls(body) { | ||
return "class Foo { " + body + " }"; | ||
} | ||
|
||
var cases = [ | ||
ok(fun("await bar;")), | ||
|
||
// While loops | ||
ok(fun(loop("while", "true", fun("await bar;")))), // Blocked by a function declaration | ||
err(fun(loop("while", "baz", "await bar;"))), | ||
err(fun(loop("while", "await foo()", ""))), | ||
|
||
// For of loops | ||
err(fun(loop("for", "var bar of baz", "await bar;"))), | ||
|
||
// For in loops | ||
err(fun(loop("for", "var bar in baz", "await bar;"))), | ||
|
||
// For loops | ||
ok(fun(loop("for", "var i = await bar; i < n; i++", ""))), | ||
err(fun(loop("for", "var i; i < n; i++", "await bar;"))), | ||
err(fun(loop("for", "var i; await foo(i); i++", ""))), | ||
err(fun(loop("for", "var i; i < n; i = await bar", ""))), | ||
|
||
// Do while loops | ||
ok(fun("do { } while (bar);")), | ||
err(fun("do { await bar; } while (baz);")), | ||
err(fun("do { } while (await bar);")), | ||
|
||
// Blocked by a function expression | ||
ok(fun(loop("while", "true", "var y = async function() { await bar; }"))), | ||
// Blocked by an arrow function | ||
ok(fun(loop("while", "true", "var y = async () => await foo;"))), | ||
ok(fun(loop("while", "true", "var y = async () => { await foo; }"))), | ||
// Blocked by a class method, | ||
ok(fun(loop("while", "true", cls("async foo() { await bar; }")))), | ||
|
||
// Deep in a loop body | ||
err(fun(loop("while", "true", "if (bar) { foo(await bar); }"))), | ||
// Deep in a loop condition | ||
err(fun(loop("while", "xyz || 5 > await x", ""))), | ||
]; | ||
|
||
function hasError(testCase) { | ||
return testCase.errors != null && testCase.errors.length > 0; | ||
} | ||
|
||
function hasNoError(testCase) { | ||
return !hasError(testCase); | ||
} | ||
|
||
var ruleTester = new RuleTester(); | ||
ruleTester.run(ruleName, rule, { | ||
valid: cases.filter(hasNoError), | ||
invalid: cases.filter(hasError), | ||
}); |