From 0cbc78e358ff25e97d482751d519be8e01e89282 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 20 Sep 2025 14:45:39 +0900 Subject: [PATCH 1/8] feat: create a fixer --- lib/rules/await-async-utils.ts | 52 +++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index ed6d21e0..62e38191 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -3,9 +3,11 @@ import { ASTUtils } from '@typescript-eslint/utils'; import { createTestingLibraryRule } from '../create-testing-library-rule'; import { findClosestCallExpressionNode, + findClosestFunctionExpressionNode, getDeepestIdentifierNode, getFunctionName, getInnermostReturningFunction, + getReferenceNode, getVariableReferences, isCallExpression, isObjectPattern, @@ -13,7 +15,7 @@ import { isProperty, } from '../node-utils'; -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; export const RULE_NAME = 'await-async-utils'; export type MessageIds = 'asyncUtilWrapper' | 'awaitAsyncUtil'; @@ -40,6 +42,7 @@ export default createTestingLibraryRule({ 'Promise returned from {{ name }} wrapper over async util must be handled', }, schema: [], + fixable: 'code', }, defaultOptions: [], @@ -90,6 +93,33 @@ export default createTestingLibraryRule({ } } } + function wrapWithFunctionExpressionFix( + fixer: TSESLint.RuleFixer, + ruleFix: TSESLint.RuleFix, + functionExpression: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | null + ) { + if (functionExpression && !functionExpression.async) { + /** + * Mutate the actual node so if other nodes exist in this + * function expression body they don't also try to fix it. + */ + functionExpression.async = true; + + return [ruleFix, fixer.insertTextBefore(functionExpression, 'async ')]; + } + return ruleFix; + } + + function insertAwaitBeforeNode( + fixer: TSESLint.RuleFixer, + node: TSESTree.Node + ) { + return fixer.insertTextBefore(node, 'await '); + } /* Either we report a direct usage of an async util or a usage of a wrapper @@ -155,6 +185,7 @@ export default createTestingLibraryRule({ context, closestCallExpression.parent ); + const functionExpression = findClosestFunctionExpressionNode(node); if (references.length === 0) { if (!isPromiseHandled(callExpressionIdentifier)) { @@ -164,6 +195,17 @@ export default createTestingLibraryRule({ data: { name: callExpressionIdentifier.name, }, + fix: (fixer) => { + const referenceNode = getReferenceNode( + callExpressionIdentifier + ); + const awaitFix = insertAwaitBeforeNode(fixer, referenceNode); + return wrapWithFunctionExpressionFix( + fixer, + awaitFix, + functionExpression + ); + }, }); } } else { @@ -176,6 +218,14 @@ export default createTestingLibraryRule({ data: { name: callExpressionIdentifier.name, }, + fix: (fixer) => { + const awaitFix = insertAwaitBeforeNode(fixer, referenceNode); + return wrapWithFunctionExpressionFix( + fixer, + awaitFix, + functionExpression + ); + }, }); return; } From 276b77091013cab45a5095f180fce64bf31d727f Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 20 Sep 2025 14:45:47 +0900 Subject: [PATCH 2/8] test: add tests --- tests/lib/rules/await-async-utils.test.ts | 201 ++++++++++++++++++++++ 1 file changed, 201 insertions(+) diff --git a/tests/lib/rules/await-async-utils.test.ts b/tests/lib/rules/await-async-utils.test.ts index fc14eda6..3a791b94 100644 --- a/tests/lib/rules/await-async-utils.test.ts +++ b/tests/lib/rules/await-async-utils.test.ts @@ -416,6 +416,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util not waited is invalid', async () => { + doSomethingElse(); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -433,6 +440,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util not waited is invalid', async () => { + doSomethingElse(); + const el = await ${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -450,6 +464,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import * as asyncUtil from '${testingFramework}'; + test('asyncUtil.${asyncUtil} util not handled is invalid', async () => { + doSomethingElse(); + await asyncUtil.${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -467,6 +488,13 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('${asyncUtil} util promise saved not handled is invalid', async () => { + doSomethingElse(); + const aPromise = await ${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -491,6 +519,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('several ${asyncUtil} utils not handled are invalid', async () => { + const aPromise = ${asyncUtil}(() => getByLabelText('username')); + doSomethingElse(await aPromise); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -515,6 +551,14 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from '${testingFramework}'; + test('unhandled expression that evaluates to promise is invalid', async () => { + const aPromise = ${asyncUtil}(() => getByLabelText('username')); + doSomethingElse(await aPromise); + await ${asyncUtil}(() => getByLabelText('email')); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -537,6 +581,18 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForSomethingAsync' }, }, ], + output: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + await waitForSomethingAsync() + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -560,6 +616,19 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForSomethingAsync' }, }, ], + output: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise in variable declaration from function wrapping ${asyncUtil} util is invalid', async () => { + render() + const result = waitForSomethingAsync() + expect(await result).toBe('foo') + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -579,6 +648,15 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import { ${asyncUtil} } from 'some-other-library'; // rather than ${testingFramework} + test( + 'aggressive reporting - util "${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + await ${asyncUtil}(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -601,6 +679,18 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForSomethingAsync' }, }, ], + output: ` + import { ${asyncUtil}, render } from '${testingFramework}'; + + function waitForSomethingAsync() { + return ${asyncUtil}(() => somethingAsync()) + } + + test('unhandled promise from function wrapping ${asyncUtil} util is invalid', async () => { + render() + const el = await waitForSomethingAsync() + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ @@ -621,6 +711,15 @@ ruleTester.run(RULE_NAME, rule, { data: { name: asyncUtil }, }, ], + output: ` + import * as asyncUtils from 'some-other-library'; // rather than ${testingFramework} + test( + 'aggressive reporting - util "asyncUtils.${asyncUtil}" which is not related to testing library is invalid', + async () => { + doSomethingElse(); + await asyncUtils.${asyncUtil}(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -648,6 +747,23 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + const { user, waitForAsyncUtil } = setup(); + await waitForAsyncUtil(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -676,6 +792,24 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + const { user, waitForAsyncUtil } = setup(); + const myAlias = waitForAsyncUtil; + await myAlias(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -703,6 +837,23 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + const { ...clone } = setup(); + await clone.waitForAsyncUtil(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -730,6 +881,23 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + const { waitForAsyncUtil: myAlias } = setup(); + await myAlias(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -756,6 +924,22 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'waitForAsyncUtil' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + await setup().waitForAsyncUtil(); + }); + `, })), ...ASYNC_UTILS.map((asyncUtil) => ({ code: ` @@ -783,6 +967,23 @@ ruleTester.run(RULE_NAME, rule, { data: { name: 'myAlias' }, }, ], + output: ` + // implicitly using ${testingFramework} + function setup() { + const utils = render(); + + const waitForAsyncUtil = () => { + return ${asyncUtil}(screen.queryByTestId('my-test-id')); + }; + + return { waitForAsyncUtil, ...utils }; + } + + test('unhandled promise from destructed property of async function wrapper is invalid', async () => { + const myAlias = setup().waitForAsyncUtil; + await myAlias(); + }); + `, })), ]), }); From a330a5693e5f8363eae1844b2ca0da03c1217f38 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Sat, 20 Sep 2025 14:46:30 +0900 Subject: [PATCH 3/8] docs: update docs --- README.md | 2 +- docs/rules/await-async-utils.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f1cf488..056fcf55 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ module.exports = [ | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------ | :-- | | [await-async-events](docs/rules/await-async-events.md) | Enforce promises from async event methods are handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | | [await-async-queries](docs/rules/await-async-queries.md) | Enforce promises from async queries to be handled | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | -| [await-async-utils](docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | | +| [await-async-utils](docs/rules/await-async-utils.md) | Enforce promises from async utils to be awaited properly | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | | [consistent-data-testid](docs/rules/consistent-data-testid.md) | Ensures consistent usage of `data-testid` | | | | | [no-await-sync-events](docs/rules/no-await-sync-events.md) | Disallow unnecessary `await` for sync events | ![badge-angular][] ![badge-dom][] ![badge-react][] | | | | [no-await-sync-queries](docs/rules/no-await-sync-queries.md) | Disallow unnecessary `await` for sync queries | ![badge-angular][] ![badge-dom][] ![badge-marko][] ![badge-react][] ![badge-svelte][] ![badge-vue][] | | 🔧 | diff --git a/docs/rules/await-async-utils.md b/docs/rules/await-async-utils.md index ac22b228..482ba3b8 100644 --- a/docs/rules/await-async-utils.md +++ b/docs/rules/await-async-utils.md @@ -2,6 +2,8 @@ 💼 This rule is enabled in the following configs: `angular`, `dom`, `marko`, `react`, `svelte`, `vue`. +🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). + Ensure that promises returned by async utils are handled properly. From 6de56f609cbf4162a36d69a807dff8adcdead7df Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 22 Sep 2025 19:01:07 +0900 Subject: [PATCH 4/8] feat: implement wrapWithFunctionExpressionFix function --- lib/rules/await-async-queries.ts | 20 ++++++-------------- lib/rules/await-async-utils.ts | 21 +-------------------- lib/utils/wrap-function-expression-fix.ts | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 34 deletions(-) create mode 100644 lib/utils/wrap-function-expression-fix.ts diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 5963c451..59bd3beb 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -11,6 +11,7 @@ import { isMemberExpression, isPromiseHandled, } from '../node-utils'; +import { wrapWithFunctionExpressionFix } from '../utils/wrap-function-expression-fix'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -164,20 +165,11 @@ export default createTestingLibraryRule({ ); } - const ruleFixes = [IdentifierNodeFixer]; - if (!functionExpression.async) { - /** - * Mutate the actual node so if other nodes exist in this - * function expression body they don't also try to fix it. - */ - functionExpression.async = true; - - ruleFixes.push( - fixer.insertTextBefore(functionExpression, 'async ') - ); - } - - return ruleFixes; + return wrapWithFunctionExpressionFix( + fixer, + IdentifierNodeFixer, + functionExpression + ); }, }); } diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 62e38191..44648ba5 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -14,6 +14,7 @@ import { isPromiseHandled, isProperty, } from '../node-utils'; +import { wrapWithFunctionExpressionFix } from '../utils/wrap-function-expression-fix'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; @@ -93,26 +94,6 @@ export default createTestingLibraryRule({ } } } - function wrapWithFunctionExpressionFix( - fixer: TSESLint.RuleFixer, - ruleFix: TSESLint.RuleFix, - functionExpression: - | TSESTree.ArrowFunctionExpression - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | null - ) { - if (functionExpression && !functionExpression.async) { - /** - * Mutate the actual node so if other nodes exist in this - * function expression body they don't also try to fix it. - */ - functionExpression.async = true; - - return [ruleFix, fixer.insertTextBefore(functionExpression, 'async ')]; - } - return ruleFix; - } function insertAwaitBeforeNode( fixer: TSESLint.RuleFixer, diff --git a/lib/utils/wrap-function-expression-fix.ts b/lib/utils/wrap-function-expression-fix.ts new file mode 100644 index 00000000..e3e64b3a --- /dev/null +++ b/lib/utils/wrap-function-expression-fix.ts @@ -0,0 +1,22 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; + +export const wrapWithFunctionExpressionFix = ( + fixer: TSESLint.RuleFixer, + ruleFix: TSESLint.RuleFix, + functionExpression: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | null +) => { + if (functionExpression && !functionExpression.async) { + /** + * Mutate the actual node so if other nodes exist in this + * function expression body they don't also try to fix it. + */ + functionExpression.async = true; + + return [ruleFix, fixer.insertTextBefore(functionExpression, 'async ')]; + } + return ruleFix; +}; From 77ed1ebf90a63ebbae9c688fbe083e663001ce1e Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 22 Sep 2025 23:43:24 +0900 Subject: [PATCH 5/8] refactor: rename to addAsyncToFunctionFix --- lib/rules/await-async-queries.ts | 4 ++-- lib/rules/await-async-utils.ts | 6 +++--- ...ction-expression-fix.ts => add-async-fo-function-fix.ts} | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/utils/{wrap-function-expression-fix.ts => add-async-fo-function-fix.ts} (92%) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index 59bd3beb..f55cdec6 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -11,7 +11,7 @@ import { isMemberExpression, isPromiseHandled, } from '../node-utils'; -import { wrapWithFunctionExpressionFix } from '../utils/wrap-function-expression-fix'; +import { addAsyncToFunctionFix } from '../utils/add-async-fo-function-fix'; import type { TSESTree } from '@typescript-eslint/utils'; @@ -165,7 +165,7 @@ export default createTestingLibraryRule({ ); } - return wrapWithFunctionExpressionFix( + return addAsyncToFunctionFix( fixer, IdentifierNodeFixer, functionExpression diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 44648ba5..21239fa8 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -14,7 +14,7 @@ import { isPromiseHandled, isProperty, } from '../node-utils'; -import { wrapWithFunctionExpressionFix } from '../utils/wrap-function-expression-fix'; +import { addAsyncToFunctionFix } from '../utils/add-async-fo-function-fix'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; @@ -181,7 +181,7 @@ export default createTestingLibraryRule({ callExpressionIdentifier ); const awaitFix = insertAwaitBeforeNode(fixer, referenceNode); - return wrapWithFunctionExpressionFix( + return addAsyncToFunctionFix( fixer, awaitFix, functionExpression @@ -201,7 +201,7 @@ export default createTestingLibraryRule({ }, fix: (fixer) => { const awaitFix = insertAwaitBeforeNode(fixer, referenceNode); - return wrapWithFunctionExpressionFix( + return addAsyncToFunctionFix( fixer, awaitFix, functionExpression diff --git a/lib/utils/wrap-function-expression-fix.ts b/lib/utils/add-async-fo-function-fix.ts similarity index 92% rename from lib/utils/wrap-function-expression-fix.ts rename to lib/utils/add-async-fo-function-fix.ts index e3e64b3a..abfb4dcf 100644 --- a/lib/utils/wrap-function-expression-fix.ts +++ b/lib/utils/add-async-fo-function-fix.ts @@ -1,6 +1,6 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; -export const wrapWithFunctionExpressionFix = ( +export const addAsyncToFunctionFix = ( fixer: TSESLint.RuleFixer, ruleFix: TSESLint.RuleFix, functionExpression: From 1c5ab1fc00386acca947ed2cacd1b8ca51c37c46 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 22 Sep 2025 23:54:07 +0900 Subject: [PATCH 6/8] test: add tests for addAsyncToFunctionFix rule --- .../utils/add-async-fo-function-fix.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/lib/utils/add-async-fo-function-fix.test.ts diff --git a/tests/lib/utils/add-async-fo-function-fix.test.ts b/tests/lib/utils/add-async-fo-function-fix.test.ts new file mode 100644 index 00000000..da2c59da --- /dev/null +++ b/tests/lib/utils/add-async-fo-function-fix.test.ts @@ -0,0 +1,119 @@ +import { createTestingLibraryRule } from '../../../lib/create-testing-library-rule'; +import { addAsyncToFunctionFix } from '../../../lib/utils/add-async-fo-function-fix'; +import { createRuleTester } from '../test-utils'; + +import type { TSESTree } from '@typescript-eslint/utils'; + +type MessageIds = 'alwaysAsync'; + +const rule = createTestingLibraryRule<[], MessageIds>({ + name: __filename, + meta: { + docs: { + recommendedConfig: { + dom: 'error', + angular: 'error', + react: 'error', + vue: 'error', + svelte: 'error', + marko: 'error', + }, + description: 'Fake rule for testing addAsyncToFunctionFix', + }, + messages: { alwaysAsync: 'Function should be async' }, + schema: [], + fixable: 'code', + type: 'problem', + }, + defaultOptions: [], + create(context) { + const reportIfNotAsync = ( + node: + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration + | TSESTree.ArrowFunctionExpression + ) => { + if (!node.async) { + context.report({ + node, + messageId: 'alwaysAsync', + fix(fixer) { + return addAsyncToFunctionFix( + fixer, + fixer.insertTextBefore(node, ''), + node + ); + }, + }); + } + }; + return { + FunctionExpression: reportIfNotAsync, + ArrowFunctionExpression: reportIfNotAsync, + FunctionDeclaration: reportIfNotAsync, + }; + }, +}); + +const ruleTester = createRuleTester(); + +ruleTester.run(addAsyncToFunctionFix.name, rule, { + valid: [ + { code: 'async function foo() {}' }, + { code: 'const bar = async function() {}' }, + { + code: ` + async function foo(a, b) { + return a + b; + }`, + }, + ], + invalid: [ + { + code: 'const bar = function() {}', + output: 'const bar = async function() {}', + errors: [{ messageId: 'alwaysAsync' }], + }, + { + code: 'const bar = () => {}', + output: 'const bar = async () => {}', + errors: [{ messageId: 'alwaysAsync' }], + }, + { + code: ` + function foo(a, b) { + return a + b; + }`, + output: ` + async function foo(a, b) { + return a + b; + }`, + errors: [{ messageId: 'alwaysAsync', line: 1 }], + }, + { + code: ` + const bar = async function() {} + const foo = function() {} + `, + output: ` + const bar = async function() {} + const foo = async function() {} + `, + errors: [{ messageId: 'alwaysAsync', line: 3 }], + }, + { + code: ` + const bar = function() {} + const foo = function() {} + `, + output: ` + const bar = async function() {} + const foo = async function() {} + `, + errors: [ + { messageId: 'alwaysAsync', line: 2 }, + { messageId: 'alwaysAsync', line: 3 }, + ], + }, + ], +}); From 76a36217272bd740341726c415a6c379e167d3bb Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Mon, 22 Sep 2025 23:56:10 +0900 Subject: [PATCH 7/8] test: fix error line number --- tests/lib/utils/add-async-fo-function-fix.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/utils/add-async-fo-function-fix.test.ts b/tests/lib/utils/add-async-fo-function-fix.test.ts index da2c59da..a46605e8 100644 --- a/tests/lib/utils/add-async-fo-function-fix.test.ts +++ b/tests/lib/utils/add-async-fo-function-fix.test.ts @@ -88,7 +88,7 @@ ruleTester.run(addAsyncToFunctionFix.name, rule, { async function foo(a, b) { return a + b; }`, - errors: [{ messageId: 'alwaysAsync', line: 1 }], + errors: [{ messageId: 'alwaysAsync', line: 2 }], }, { code: ` From 42f308de801b73f40ad8099ad4bfde777656b7e2 Mon Sep 17 00:00:00 2001 From: Hasegawa-Yukihiro Date: Tue, 23 Sep 2025 15:39:00 +0900 Subject: [PATCH 8/8] refactor: fix file name --- lib/rules/await-async-queries.ts | 2 +- lib/rules/await-async-utils.ts | 2 +- ...dd-async-fo-function-fix.ts => add-async-to-function-fix.ts} | 0 ...o-function-fix.test.ts => add-async-to-function-fix.test.ts} | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename lib/utils/{add-async-fo-function-fix.ts => add-async-to-function-fix.ts} (100%) rename tests/lib/utils/{add-async-fo-function-fix.test.ts => add-async-to-function-fix.test.ts} (99%) diff --git a/lib/rules/await-async-queries.ts b/lib/rules/await-async-queries.ts index f55cdec6..f71b0447 100644 --- a/lib/rules/await-async-queries.ts +++ b/lib/rules/await-async-queries.ts @@ -11,7 +11,7 @@ import { isMemberExpression, isPromiseHandled, } from '../node-utils'; -import { addAsyncToFunctionFix } from '../utils/add-async-fo-function-fix'; +import { addAsyncToFunctionFix } from '../utils/add-async-to-function-fix'; import type { TSESTree } from '@typescript-eslint/utils'; diff --git a/lib/rules/await-async-utils.ts b/lib/rules/await-async-utils.ts index 21239fa8..d8324dd3 100644 --- a/lib/rules/await-async-utils.ts +++ b/lib/rules/await-async-utils.ts @@ -14,7 +14,7 @@ import { isPromiseHandled, isProperty, } from '../node-utils'; -import { addAsyncToFunctionFix } from '../utils/add-async-fo-function-fix'; +import { addAsyncToFunctionFix } from '../utils/add-async-to-function-fix'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; diff --git a/lib/utils/add-async-fo-function-fix.ts b/lib/utils/add-async-to-function-fix.ts similarity index 100% rename from lib/utils/add-async-fo-function-fix.ts rename to lib/utils/add-async-to-function-fix.ts diff --git a/tests/lib/utils/add-async-fo-function-fix.test.ts b/tests/lib/utils/add-async-to-function-fix.test.ts similarity index 99% rename from tests/lib/utils/add-async-fo-function-fix.test.ts rename to tests/lib/utils/add-async-to-function-fix.test.ts index a46605e8..c13dc155 100644 --- a/tests/lib/utils/add-async-fo-function-fix.test.ts +++ b/tests/lib/utils/add-async-to-function-fix.test.ts @@ -1,5 +1,5 @@ import { createTestingLibraryRule } from '../../../lib/create-testing-library-rule'; -import { addAsyncToFunctionFix } from '../../../lib/utils/add-async-fo-function-fix'; +import { addAsyncToFunctionFix } from '../../../lib/utils/add-async-to-function-fix'; import { createRuleTester } from '../test-utils'; import type { TSESTree } from '@typescript-eslint/utils';