Skip to content

Commit

Permalink
Fix: support Async iteration (fixes #9891) (#9957)
Browse files Browse the repository at this point in the history
* update keyword-spacing

* update no-await-in-loop

* update object-shorthand

* update require-await

* update tests of generator-star-spacing

* update tests of no-useless-computed-key
  • Loading branch information
mysticatea authored and not-an-aardvark committed Feb 10, 2018
1 parent 74fa253 commit f012b8c
Show file tree
Hide file tree
Showing 10 changed files with 234 additions and 71 deletions.
7 changes: 6 additions & 1 deletion lib/rules/keyword-spacing.js
Expand Up @@ -436,7 +436,12 @@ module.exports = {
* @returns {void}
*/
function checkSpacingForForOfStatement(node) {
checkSpacingAroundFirstToken(node);
if (node.await) {
checkSpacingBefore(sourceCode.getFirstToken(node, 0));
checkSpacingAfter(sourceCode.getFirstToken(node, 1));
} else {
checkSpacingAroundFirstToken(node);
}
checkSpacingAround(sourceCode.getTokenBefore(node.right, astUtils.isNotOpeningParenToken));
}

Expand Down
127 changes: 71 additions & 56 deletions lib/rules/no-await-in-loop.js
Expand Up @@ -4,24 +4,54 @@
*/
"use strict";

// Node types which are considered loops.
const loopTypes = new Set([
"ForStatement",
"ForOfStatement",
"ForInStatement",
"WhileStatement",
"DoWhileStatement"
]);
/**
* Check whether it should stop traversing ancestors at the given node.
* @param {ASTNode} node A node to check.
* @returns {boolean} `true` if it should stop traversing.
*/
function isBoundary(node) {
const t = node.type;

return (
t === "FunctionDeclaration" ||
t === "FunctionExpression" ||
t === "ArrowFunctionExpression" ||

/*
* 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.
/*
* Don't report the await expressions on for-await-of loop since it's
* asynchronous iteration intentionally.
*/
(t === "ForOfStatement" && node.await === true)
);
}

/**
* Check whether the given node is in loop.
* @param {ASTNode} node A node to check.
* @param {ASTNode} parent A parent node to check.
* @returns {boolean} `true` if the node is in loop.
*/
const boundaryTypes = new Set([
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression"
]);
function isLooped(node, parent) {
switch (parent.type) {
case "ForStatement":
return (
node === parent.test ||
node === parent.update ||
node === parent.body
);

case "ForOfStatement":
case "ForInStatement":
return node === parent.body;

case "WhileStatement":
case "DoWhileStatement":
return node === parent.test || node === parent.body;

default:
return false;
}
}

module.exports = {
meta: {
Expand All @@ -37,51 +67,36 @@ module.exports = {
}
},
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)) {
/**
* Validate an await expression.
* @param {ASTNode} awaitNode An AwaitExpression or ForOfStatement node to validate.
* @returns {void}
*/
function validate(awaitNode) {
if (awaitNode.type === "ForOfStatement" && !awaitNode.await) {
return;
}

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

/*
* 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,
messageId: "unexpectedAwait"
});
return;
}
}
while (parent && !isBoundary(parent)) {
if (isLooped(node, parent)) {
context.report({
node,
messageId: "unexpectedAwait"
});
return;
}
node = parent;
parent = parent.parent;
}
}

return {
AwaitExpression: validate,
ForOfStatement: validate
};
}
};
14 changes: 8 additions & 6 deletions lib/rules/object-shorthand.js
Expand Up @@ -233,10 +233,11 @@ module.exports = {
const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
let keyPrefix = "";

if (node.value.async) {
keyPrefix += "async ";
}
if (node.value.generator) {
keyPrefix = "*";
} else if (node.value.async) {
keyPrefix = "async ";
keyPrefix += "*";
}

if (node.value.type === "FunctionExpression") {
Expand Down Expand Up @@ -273,10 +274,11 @@ module.exports = {
const keyText = sourceCode.text.slice(firstKeyToken.range[0], lastKeyToken.range[1]);
let functionHeader = "function";

if (node.value.async) {
functionHeader = `async ${functionHeader}`;
}
if (node.value.generator) {
functionHeader = "function*";
} else if (node.value.async) {
functionHeader = "async function";
functionHeader = `${functionHeader}*`;
}

return fixer.replaceTextRange([node.range[0], lastKeyToken.range[1]], `${keyText}: ${functionHeader}`);
Expand Down
5 changes: 5 additions & 0 deletions lib/rules/require-await.js
Expand Up @@ -90,6 +90,11 @@ module.exports = {

AwaitExpression() {
scopeInfo.hasAwait = true;
},
ForOfStatement(node) {
if (node.await) {
scopeInfo.hasAwait = true;
}
}
};
}
Expand Down
76 changes: 75 additions & 1 deletion tests/lib/rules/generator-star-spacing.js
Expand Up @@ -16,7 +16,7 @@ const rule = require("../../../lib/rules/generator-star-spacing"),
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });

ruleTester.run("generator-star-spacing", rule, {

Expand Down Expand Up @@ -1336,6 +1336,80 @@ ruleTester.run("generator-star-spacing", rule, {
message: "Missing space before *.",
type: "Punctuator"
}]
},

// async generators
{
code: "({ async * foo(){} })",
output: "({ async*foo(){} })",
options: [{ before: false, after: false }],
errors: [{
message: "Unexpected space before *.",
type: "Punctuator"
}, {
message: "Unexpected space after *.",
type: "Punctuator"
}]
},
{
code: "({ async*foo(){} })",
output: "({ async * foo(){} })",
options: [{ before: true, after: true }],
errors: [{
message: "Missing space before *.",
type: "Punctuator"
}, {
message: "Missing space after *.",
type: "Punctuator"
}]
},
{
code: "class Foo { async * foo(){} }",
output: "class Foo { async*foo(){} }",
options: [{ before: false, after: false }],
errors: [{
message: "Unexpected space before *.",
type: "Punctuator"
}, {
message: "Unexpected space after *.",
type: "Punctuator"
}]
},
{
code: "class Foo { async*foo(){} }",
output: "class Foo { async * foo(){} }",
options: [{ before: true, after: true }],
errors: [{
message: "Missing space before *.",
type: "Punctuator"
}, {
message: "Missing space after *.",
type: "Punctuator"
}]
},
{
code: "class Foo { static async * foo(){} }",
output: "class Foo { static async*foo(){} }",
options: [{ before: false, after: false }],
errors: [{
message: "Unexpected space before *.",
type: "Punctuator"
}, {
message: "Unexpected space after *.",
type: "Punctuator"
}]
},
{
code: "class Foo { static async*foo(){} }",
output: "class Foo { static async * foo(){} }",
options: [{ before: true, after: true }],
errors: [{
message: "Missing space before *.",
type: "Punctuator"
}, {
message: "Missing space after *.",
type: "Punctuator"
}]
}

]
Expand Down
32 changes: 32 additions & 0 deletions tests/lib/rules/keyword-spacing.js
Expand Up @@ -180,6 +180,10 @@ ruleTester.run("keyword-spacing", rule, {
{ code: "a[ async function foo() {}]", options: [NEITHER], parserOptions: { ecmaVersion: 8 } },
{ code: "({[ async function foo() {}]: 0})", options: [NEITHER], parserOptions: { ecmaVersion: 8 } },

// not conflict with `generator-star-spacing`
{ code: "({ async* foo() {} })", parserOptions: { ecmaVersion: 2018 } },
{ code: "({ async *foo() {} })", options: [NEITHER], parserOptions: { ecmaVersion: 2018 } },

// not conflict with `key-spacing`
{ code: "({a:async function foo() {} })", parserOptions: { ecmaVersion: 8 } },
{ code: "({a: async function foo() {} })", options: [NEITHER], parserOptions: { ecmaVersion: 8 } },
Expand Down Expand Up @@ -1530,6 +1534,34 @@ ruleTester.run("keyword-spacing", rule, {
errors: unexpectedBefore("await")
},

{
code: "async function wrap() { for await(x of xs); }",
output: "async function wrap() { for await (x of xs); }",
parserOptions: { ecmaVersion: 2018 },
errors: expectedAfter("await")
},
{
code: "async function wrap() { for await (x of xs); }",
output: "async function wrap() { for await(x of xs); }",
options: [NEITHER],
parserOptions: { ecmaVersion: 2018 },
errors: unexpectedAfter("await")
},
{
code: "async function wrap() { for await(x of xs); }",
output: "async function wrap() { for await (x of xs); }",
options: [override("await", BOTH)],
parserOptions: { ecmaVersion: 2018 },
errors: expectedAfter("await")
},
{
code: "async function wrap() { for await (x of xs); }",
output: "async function wrap() { for await(x of xs); }",
options: [override("await", NEITHER)],
parserOptions: { ecmaVersion: 2018 },
errors: unexpectedAfter("await")
},

//----------------------------------------------------------------------
// break
//----------------------------------------------------------------------
Expand Down
13 changes: 10 additions & 3 deletions tests/lib/rules/no-await-in-loop.js
Expand Up @@ -10,13 +10,14 @@ const rule = require("../../../lib/rules/no-await-in-loop"),

const error = { messageId: "unexpectedAwait" };

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

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 await (var bar of await baz) { } }",
"async function foo() { for (var bar = await baz in qux) {} }",

// While loops
Expand All @@ -35,14 +36,17 @@ ruleTester.run("no-await-in-loop", rule, {
"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; } } } }"
"async function foo() { while (true) { class Foo { async foo() { await bar; } } } }",

// Asynchronous iteration intentionally
"async function foo() { for await (var x of xs) { await f(x) } }"
],
invalid: [

// While loops
{ code: "async function foo() { while (baz) { await bar; } }", errors: [error] },
{ code: "async function foo() { while (await foo()) { } }", errors: [error] },
{ code: "async function foo() { while (baz) { for await (x of xs); } }", errors: [error] },

// For of loops
{ code: "async function foo() { for (var bar of baz) { await bar; } }", errors: [error] },
Expand All @@ -64,6 +68,9 @@ ruleTester.run("no-await-in-loop", rule, {
{ code: "async function foo() { while (true) { if (bar) { foo(await bar); } } }", errors: [error] },

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

// In a nested loop of for-await-of
{ code: "async function foo() { for await (var x of xs) { while (1) await f(x) } }", errors: [error] }
]
});

0 comments on commit f012b8c

Please sign in to comment.