From dd8f357639f9c299dbd8558c77234a9260d309e9 Mon Sep 17 00:00:00 2001 From: Jason Quense Date: Sun, 1 Nov 2015 11:46:24 -0500 Subject: [PATCH] Merge pull request babel/eslint-plugin-babel#22 from nmote/await Add no-await-in-loop rule. --- eslint/babel-eslint-plugin/index.js | 2 + .../rules/no-await-in-loop.js | 72 ++++++++++++ .../tests/no-await-in-loop.js | 106 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 eslint/babel-eslint-plugin/rules/no-await-in-loop.js create mode 100644 eslint/babel-eslint-plugin/tests/no-await-in-loop.js diff --git a/eslint/babel-eslint-plugin/index.js b/eslint/babel-eslint-plugin/index.js index 5fde6a2cbc..9bb58310c1 100644 --- a/eslint/babel-eslint-plugin/index.js +++ b/eslint/babel-eslint-plugin/index.js @@ -7,6 +7,7 @@ module.exports = { 'object-curly-spacing': require('./rules/object-curly-spacing'), 'object-shorthand': require('./rules/object-shorthand'), 'arrow-parens': require('./rules/arrow-parens'), + 'no-await-in-loop': require('./rules/no-await-in-loop'), }, rulesConfig: { 'generator-star-spacing': 0, @@ -14,5 +15,6 @@ module.exports = { 'object-curly-spacing': 0, 'object-shorthand': 0, 'arrow-parens': 0, + 'no-await-in-loop': 0, } }; diff --git a/eslint/babel-eslint-plugin/rules/no-await-in-loop.js b/eslint/babel-eslint-plugin/rules/no-await-in-loop.js new file mode 100644 index 0000000000..cc3bb91af4 --- /dev/null +++ b/eslint/babel-eslint-plugin/rules/no-await-in-loop.js @@ -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; + } + } + } + } + }, + }; +} diff --git a/eslint/babel-eslint-plugin/tests/no-await-in-loop.js b/eslint/babel-eslint-plugin/tests/no-await-in-loop.js new file mode 100644 index 0000000000..ba82e4e78c --- /dev/null +++ b/eslint/babel-eslint-plugin/tests/no-await-in-loop.js @@ -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), +});