diff --git a/packages/eslint-plugin/src/rules/require-await.ts b/packages/eslint-plugin/src/rules/require-await.ts index e7b6a5f5959..99c455cd0f6 100644 --- a/packages/eslint-plugin/src/rules/require-await.ts +++ b/packages/eslint-plugin/src/rules/require-await.ts @@ -11,6 +11,8 @@ interface ScopeInfo { upper: ScopeInfo | null; hasAwait: boolean; hasAsync: boolean; + isGen: boolean; + isAsyncYield: boolean; } type FunctionNode = | TSESTree.FunctionDeclaration @@ -49,6 +51,8 @@ export default util.createRule({ upper: scopeInfo, hasAwait: false, hasAsync: node.async, + isGen: node.generator || false, + isAsyncYield: false, }; } @@ -62,7 +66,12 @@ export default util.createRule({ return; } - if (node.async && !scopeInfo.hasAwait && !isEmptyFunction(node)) { + if ( + node.async && + !scopeInfo.hasAwait && + !isEmptyFunction(node) && + !(scopeInfo.isGen && scopeInfo.isAsyncYield) + ) { context.report({ node, loc: getFunctionHeadLoc(node, sourceCode), @@ -92,10 +101,34 @@ export default util.createRule({ if (!scopeInfo) { return; } - scopeInfo.hasAwait = true; } + /** + * mark `scopeInfo.isAsyncYield` to `true` if its a generator + * function and the delegate is `true` + */ + function markAsHasDelegateGen(node: TSESTree.YieldExpression): void { + if (!scopeInfo || !scopeInfo.isGen || !node.argument) { + return; + } + + if (node?.argument?.type === AST_NODE_TYPES.Literal) { + // making this `false` as for literals we don't need to check the definition + // eg : async function* run() { yield* 1 } + scopeInfo.isAsyncYield = false; + } + + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node?.argument); + const type = checker.getTypeAtLocation(tsNode); + const symbol = type.getSymbol(); + + // async function* test1() {yield* asyncGenerator() } + if (symbol?.getName() === 'AsyncGenerator') { + scopeInfo.isAsyncYield = true; + } + } + return { FunctionDeclaration: enterFunction, FunctionExpression: enterFunction, @@ -106,6 +139,7 @@ export default util.createRule({ AwaitExpression: markAsHasAwait, 'ForOfStatement[await = true]': markAsHasAwait, + 'YieldExpression[delegate = true]': markAsHasDelegateGen, // check body-less async arrow function. // ignore `async () => await foo` because it's obviously correct diff --git a/packages/eslint-plugin/tests/rules/require-await.test.ts b/packages/eslint-plugin/tests/rules/require-await.test.ts index 66db5497980..caea93fbfb9 100644 --- a/packages/eslint-plugin/tests/rules/require-await.test.ts +++ b/packages/eslint-plugin/tests/rules/require-await.test.ts @@ -16,102 +16,130 @@ ruleTester.run('require-await', rule, { valid: [ // Non-async function declaration `function numberOne(): number { - return 1; - }`, + return 1; + }`, // Non-async function expression `const numberOne = function(): number { - return 1; - }`, + return 1; + }`, // Non-async arrow function expression (concise-body) `const numberOne = (): number => 1;`, // Non-async arrow function expression (block-body) `const numberOne = (): number => { - return 1; - };`, + return 1; + };`, // Non-async function that returns a promise // https://github.com/typescript-eslint/typescript-eslint/issues/1226 ` -function delay() { - return Promise.resolve(); -} - `, + function delay() { + return Promise.resolve(); + } + `, ` -const delay = () => { - return Promise.resolve(); -} - `, + const delay = () => { + return Promise.resolve(); + } + `, `const delay = () => Promise.resolve();`, // Async function declaration with await `async function numberOne(): Promise { - return await 1; - }`, + return await 1; + }`, // Async function expression with await `const numberOne = async function(): Promise { - return await 1; - }`, + return await 1; + }`, // Async arrow function expression with await (concise-body) `const numberOne = async (): Promise => await 1;`, // Async arrow function expression with await (block-body) `const numberOne = async (): Promise => { - return await 1; - };`, + return await 1; + };`, // Async function declaration with promise return `async function numberOne(): Promise { - return Promise.resolve(1); - }`, + return Promise.resolve(1); + }`, // Async function expression with promise return `const numberOne = async function(): Promise { - return Promise.resolve(1); - }`, + return Promise.resolve(1); + }`, // Async arrow function with promise return (concise-body) `const numberOne = async (): Promise => Promise.resolve(1);`, // Async arrow function with promise return (block-body) `const numberOne = async (): Promise => { - return Promise.resolve(1); - };`, + return Promise.resolve(1); + };`, // Async function declaration with async function return `async function numberOne(): Promise { - return getAsyncNumber(1); - } - async function getAsyncNumber(x: number): Promise { - return Promise.resolve(x); - }`, + return getAsyncNumber(1); + } + async function getAsyncNumber(x: number): Promise { + return Promise.resolve(x); + }`, // Async function expression with async function return `const numberOne = async function(): Promise { - return getAsyncNumber(1); - } - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, + return getAsyncNumber(1); + } + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, // Async arrow function with async function return (concise-body) `const numberOne = async (): Promise => getAsyncNumber(1); - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, // Async arrow function with async function return (block-body) `const numberOne = async (): Promise => { - return getAsyncNumber(1); - }; - const getAsyncNumber = async function(x: number): Promise { - return Promise.resolve(x); - }`, + return getAsyncNumber(1); + }; + const getAsyncNumber = async function(x: number): Promise { + return Promise.resolve(x); + }`, // https://github.com/typescript-eslint/typescript-eslint/issues/1188 ` -async function testFunction(): Promise { - await Promise.all([1, 2, 3].map( - // this should not trigger an error on the parent function - async value => Promise.resolve(value) - )) -} + async function testFunction(): Promise { + await Promise.all([1, 2, 3].map( + // this should not trigger an error on the parent function + async value => Promise.resolve(value) + )) + } + `, + ` + async function* run() { + await new Promise(resolve => setTimeout(resolve, 100)); + yield 'Hello'; + console.log('World'); + } + `, + 'async function* run() { }', + 'async function* asyncGenerator() { await Promise.resolve(); yield 1 }', + 'function* test6() { yield* syncGenerator() }', + 'function* test8() { yield syncGenerator() }', + 'function* syncGenerator() { yield 1 }', + ` + async function* asyncGenerator() { + await Promise.resolve() + yield 1 + } + async function* test1() {yield* asyncGenerator() } `, + `async function* foo() { + await Promise.resolve() + yield 1 + } + async function* bar() { + yield* foo() + }`, + 'const foo : () => void = async function *(){}', + 'async function* foo() : Promise { return new Promise((res) => res(`hello`)) }', ], invalid: [ { // Async function declaration with no await code: `async function numberOne(): Promise { - return 1; - }`, + return 1; + }`, errors: [ { messageId: 'missingAwait', @@ -124,8 +152,8 @@ async function testFunction(): Promise { { // Async function expression with no await code: `const numberOne = async function(): Promise { - return 1; - }`, + return 1; + }`, errors: [ { messageId: 'missingAwait', @@ -160,9 +188,85 @@ async function testFunction(): Promise { }, ], }, + { + code: 'async function* foo() : void { doSomething() }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'foo'", + }, + }, + ], + }, + { + code: 'async function* foo() { yield 1 }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'foo'", + }, + }, + ], + }, + { + code: 'async function* run() { console.log("bar") }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'run'", + }, + }, + ], + }, + { + code: 'const foo = async function *(){ console.log("bar") }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: 'Async generator function', + }, + }, + ], + }, + { + code: 'async function* syncGenerator() { yield 1 }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'syncGenerator'", + }, + }, + ], + }, + { + code: 'async function* test3() { yield asyncGenerator() }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'test3'", + }, + }, + ], + }, + { + code: 'async function* test7() { yield syncGenerator() }', + errors: [ + { + messageId: 'missingAwait', + data: { + name: "Async generator function 'test7'", + }, + }, + ], + }, ], }); - // base eslint tests // https://github.com/eslint/eslint/blob/03a69dbe86d5b5768a310105416ae726822e3c1c/tests/lib/rules/require-await.js#L25-L132 ruleTester.run('require-await', rule, { @@ -175,27 +279,23 @@ ruleTester.run('require-await', rule, { 'class A { async foo() { await doSomething() } }', '(class { async foo() { await doSomething() } })', 'async function foo() { await (async () => { await doSomething() }) }', - // empty functions are ok. 'async function foo() {}', 'async () => {}', - // normal functions are ok. 'function foo() { doSomething() }', - // for-await-of 'async function foo() { for await (x of xs); }', - // global await { code: 'await foo()', }, { code: ` - for await (let num of asyncIterable) { - console.log(num); - } - `, + for await (let num of asyncIterable) { + console.log(num); + } + `, }, ], invalid: [ @@ -289,5 +389,23 @@ ruleTester.run('require-await', rule, { }, ], }, + { + code: 'async function* run() { yield * anotherAsyncGenerator() }', + errors: [ + { + messageId: 'missingAwait', + data: { name: "Async generator function 'run'" }, + }, + ], + }, + { + code: 'async function* run() { yield* 1 }', + errors: [ + { + messageId: 'missingAwait', + data: { name: "Async generator function 'run'" }, + }, + ], + }, ], });