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

Fix: support Async iteration (fixes #9891) #9957

Merged
merged 6 commits into from Feb 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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] }
]
});