-
Notifications
You must be signed in to change notification settings - Fork 248
feat: create new no-unneeded-async-expect-function rule
#1863
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
Changes from all commits
c497904
c7e1d1e
b23a00b
36e4235
0bf81a6
3e890b2
3f1d42d
6c023de
785c04c
65ba578
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Disallow unnecessary async function wrapper for expected promises (`no-unneeded-async-expect-function`) | ||
|
|
||
| 🔧 This rule is automatically fixable by the | ||
| [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix). | ||
|
|
||
| <!-- end auto-generated rule header --> | ||
|
|
||
| `Jest` can handle fulfilled/rejected promisified function call normally but | ||
| occassionally, engineers wrap said function in another `async` function that is | ||
| excessively verbose and make the tests harder to read. | ||
|
|
||
| ## Rule details | ||
|
|
||
| This rule triggers a warning if `expect` is passed with an an `async` function | ||
| that has a single `await` call. | ||
|
|
||
| Examples of **incorrect** code for this rule | ||
|
|
||
| ```js | ||
| it('wrong1', async () => { | ||
| await expect(async () => { | ||
| await doSomethingAsync(); | ||
| }).rejects.toThrow(); | ||
| }); | ||
|
|
||
| it('wrong2', async () => { | ||
| await expect(async function () { | ||
| await doSomethingAsync(); | ||
| }).rejects.toThrow(); | ||
| }); | ||
| ``` | ||
|
|
||
| Examples of **correct** code for this rule | ||
|
|
||
| ```js | ||
| it('right1', async () => { | ||
| await expect(doSomethingAsync()).rejects.toThrow(); | ||
| }); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| import dedent from 'dedent'; | ||
| import rule from '../no-unneeded-async-expect-function'; | ||
| import { FlatCompatRuleTester as RuleTester, espreeParser } from './test-utils'; | ||
|
|
||
| const ruleTester = new RuleTester({ | ||
| parser: espreeParser, | ||
| parserOptions: { | ||
| ecmaVersion: 2017, | ||
| }, | ||
| }); | ||
|
|
||
| ruleTester.run('no-unneeded-async-expect-function', rule, { | ||
| valid: [ | ||
| 'expect.hasAssertions()', | ||
| dedent` | ||
| it('pass', async () => { | ||
| expect(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass', async () => { | ||
| await expect(doSomethingAsync()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass', async () => { | ||
| await expect(doSomethingAsync(1, 2)).resolves.toBe(1); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass', async () => { | ||
| await expect(async () => { | ||
| await doSomethingAsync(); | ||
| await doSomethingTwiceAsync(1, 2); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| { | ||
| code: dedent` | ||
| import { expect as pleaseExpect } from '@jest/globals'; | ||
| it('pass', async () => { | ||
| await pleaseExpect(doSomethingAsync()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| parserOptions: { sourceType: 'module' }, | ||
| }, | ||
| dedent` | ||
| it('pass', async () => { | ||
| await expect(async () => { | ||
| doSomethingAsync(); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass', async () => { | ||
| await expect(async () => { | ||
| const a = 1; | ||
| await doSomethingAsync(a); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass for non-async expect', async () => { | ||
| await expect(() => { | ||
| doSomethingSync(a); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass for await in expect', async () => { | ||
| await expect(await doSomethingAsync()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass for different matchers', async () => { | ||
| await expect(await doSomething()).not.toThrow(); | ||
| await expect(await doSomething()).toHaveLength(2); | ||
| await expect(await doSomething()).toHaveReturned(); | ||
| await expect(await doSomething()).not.toHaveBeenCalled(); | ||
| await expect(await doSomething()).not.toBeDefined(); | ||
| await expect(await doSomething()).toEqual(2); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass for using await within for-loop', async () => { | ||
| const b = [async () => Promise.resolve(1), async () => Promise.reject(2)]; | ||
| await expect(async() => { | ||
| for (const a of b) { | ||
| await b(); | ||
| } | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| dedent` | ||
| it('pass for using await within array', async () => { | ||
| await expect(async() => [await Promise.reject(2)]).rejects.toThrow(2); | ||
| }) | ||
| `, | ||
| ], | ||
| invalid: [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it'd be good if we could have a test with say a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is done in HEAD commit |
||
| { | ||
| code: dedent` | ||
| it('should be fixed', async () => { | ||
| await expect(async () => { | ||
| await doSomethingAsync(); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed', async () => { | ||
| await expect(doSomethingAsync()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: dedent` | ||
| it('should be fixed', async () => { | ||
| await expect(async function () { | ||
| await doSomethingAsync(); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed', async () => { | ||
| await expect(doSomethingAsync()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: dedent` | ||
| it('should be fixed for async arrow function', async () => { | ||
| await expect(async () => { | ||
| await doSomethingAsync(1, 2); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed for async arrow function', async () => { | ||
| await expect(doSomethingAsync(1, 2)).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: dedent` | ||
| it('should be fixed for async normal function', async () => { | ||
| await expect(async function () { | ||
| await doSomethingAsync(1, 2); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed for async normal function', async () => { | ||
| await expect(doSomethingAsync(1, 2)).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: dedent` | ||
| it('should be fixed for Promise.all', async () => { | ||
| await expect(async function () { | ||
| await Promise.all([doSomethingAsync(1, 2), doSomethingAsync()]); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed for Promise.all', async () => { | ||
| await expect(Promise.all([doSomethingAsync(1, 2), doSomethingAsync()])).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| { | ||
| code: dedent` | ||
| it('should be fixed for async ref to expect', async () => { | ||
| const a = async () => { await doSomethingAsync() }; | ||
| await expect(async () => { | ||
| await a(); | ||
| }).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| output: dedent` | ||
| it('should be fixed for async ref to expect', async () => { | ||
| const a = async () => { await doSomethingAsync() }; | ||
| await expect(a()).rejects.toThrow(); | ||
| }) | ||
| `, | ||
| errors: [ | ||
| { | ||
| endColumn: 4, | ||
| column: 16, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils'; | ||
| import { createRule, isFunction, parseJestFnCall } from './utils'; | ||
|
|
||
| export default createRule({ | ||
| name: __filename, | ||
| meta: { | ||
| docs: { | ||
| description: | ||
| 'Disallow unnecessary async function wrapper for expected promises', | ||
| }, | ||
| fixable: 'code', | ||
| messages: { | ||
| noAsyncWrapperForExpectedPromise: 'Unnecessary async function wrapper', | ||
| }, | ||
| schema: [], | ||
| type: 'suggestion', | ||
| }, | ||
| defaultOptions: [], | ||
| create(context) { | ||
| return { | ||
| CallExpression(node: TSESTree.CallExpression) { | ||
| const jestFnCall = parseJestFnCall(node, context); | ||
|
|
||
| if (jestFnCall?.type !== 'expect') { | ||
| return; | ||
| } | ||
|
|
||
| const { parent } = jestFnCall.head.node; | ||
|
|
||
| if (parent?.type !== AST_NODE_TYPES.CallExpression) { | ||
| return; | ||
| } | ||
|
|
||
| const [awaitNode] = parent.arguments; | ||
|
|
||
| if ( | ||
| !awaitNode || | ||
| !isFunction(awaitNode) || | ||
| !awaitNode?.async || | ||
| awaitNode.body.type !== AST_NODE_TYPES.BlockStatement || | ||
| awaitNode.body.body.length !== 1 | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| const [callback] = awaitNode.body.body; | ||
|
|
||
| if ( | ||
| callback.type === AST_NODE_TYPES.ExpressionStatement && | ||
| callback.expression.type === AST_NODE_TYPES.AwaitExpression && | ||
| callback.expression.argument.type === AST_NODE_TYPES.CallExpression | ||
| ) { | ||
| const innerAsyncFuncCall = callback.expression.argument; | ||
|
|
||
| context.report({ | ||
| node: awaitNode, | ||
| messageId: 'noAsyncWrapperForExpectedPromise', | ||
| fix(fixer) { | ||
| const { sourceCode } = context; | ||
|
|
||
| return [ | ||
| fixer.replaceTextRange( | ||
| [awaitNode.range[0], awaitNode.range[1]], | ||
| sourceCode.getText(innerAsyncFuncCall), | ||
| ), | ||
| ]; | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be good to have some tests that:
expectawaitwithin theexpectcall (e.g.expect(await x()))rejectsorresolveis being usedawaitwithin an arrayThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done in HEAD commit