Skip to content

Commit

Permalink
[valid-expect] include fixer for adding missing await (#574)
Browse files Browse the repository at this point in the history
* feat: add async and await

* test: tests for adding async and await

* docs: update document
  • Loading branch information
y-hsgw authored Nov 26, 2024
1 parent 6388046 commit 1da7ed4
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export default [
| [require-to-throw-message](docs/rules/require-to-throw-message.md) | require toThrow() to be called with an error message | | 🌐 | | | |
| [require-top-level-describe](docs/rules/require-top-level-describe.md) | enforce that all tests are in a top-level describe | | 🌐 | | | |
| [valid-describe-callback](docs/rules/valid-describe-callback.md) | enforce valid describe callback || | | | |
| [valid-expect](docs/rules/valid-expect.md) | enforce valid `expect()` usage || | | | |
| [valid-expect](docs/rules/valid-expect.md) | enforce valid `expect()` usage || | 🔧 | | |
| [valid-title](docs/rules/valid-title.md) | enforce valid titles || | 🔧 | | |

<!-- end auto-generated rules list -->
Expand Down
8 changes: 5 additions & 3 deletions docs/rules/valid-expect.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

💼 This rule is enabled in the ✅ `recommended` config.

🔧 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 -->

This rule triggers a warning if `expect` is called with no argument or with more than one argument. You change that behavior by setting the `minArgs` and `maxArgs` options.
Expand Down Expand Up @@ -32,7 +34,7 @@ This rule triggers a warning if `expect` is called with no argument or with more
- Default: `[]`


```js
```js
{
"vitest/valid-expect": ["error", {
"asyncMatchers": ["toBeResolved", "toBeRejected"]
Expand All @@ -42,8 +44,8 @@ This rule triggers a warning if `expect` is called with no argument or with more

avoid using asyncMatchers with `expect`:



3. `minArgs`

- Type: `number`
Expand Down
91 changes: 84 additions & 7 deletions src/rules/valid-expect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
import { createEslintRule, getAccessorValue, isSupportedAccessor } from '../utils'
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'
import { createEslintRule, FunctionExpression, getAccessorValue, isFunction, isSupportedAccessor } from '../utils'
import { parseVitestFnCallWithReason } from '../utils/parse-vitest-fn-call'
import { ModifierName } from '../utils/types'

Expand Down Expand Up @@ -44,6 +44,21 @@ const getPromiseCallExpressionNode = (node: TSESTree.Node) => {
const promiseArrayExceptionKey = ({ start, end }: TSESTree.SourceLocation) =>
`${start.line}:${start.column}-${end.line}:${end.column}`

const getNormalizeFunctionExpression = (
functionExpression: FunctionExpression,
):
| TSESTree.PropertyComputedName
| TSESTree.PropertyNonComputedName
| FunctionExpression => {
if (
functionExpression.parent.type === AST_NODE_TYPES.Property &&
functionExpression.type === AST_NODE_TYPES.FunctionExpression
)
return functionExpression.parent;

return functionExpression;
};

function getParentIfThenified(node: TSESTree.Node): TSESTree.Node {
const grandParentNode = node.parent?.parent

Expand All @@ -65,6 +80,15 @@ const findPromiseCallExpressionNode = (node: TSESTree.Node) =>
? getPromiseCallExpressionNode(node.parent)
: null

const findFirstFunctionExpression = ({
parent,
}: TSESTree.Node): FunctionExpression | null => {
if (!parent)
return null;

return isFunction(parent) ? parent : findFirstFunctionExpression(parent);
};

const isAcceptableReturnNode = (
node: TSESTree.Node,
allowReturn: boolean
Expand Down Expand Up @@ -110,6 +134,7 @@ export default createEslintRule<[
'Promises which return async assertions must be awaited{{orReturned}}'
},
type: 'suggestion',
fixable: "code",
schema: [
{
type: 'object',
Expand Down Expand Up @@ -143,6 +168,13 @@ export default createEslintRule<[
}],
create: (context, [{ alwaysAwait, asyncMatchers = defaultAsyncMatchers, minArgs = 1, maxArgs = 1 }]) => {
const arrayExceptions = new Set<string>()
const descriptors: Array<{
node: TSESTree.Node;
messageId: Extract<
MESSAGE_IDS,
'asyncMustBeAwaited' | 'promisesWithAsyncAssertionsMustBeAwaited'
>;
}> = [];

const pushPromiseArrayException = (loc: TSESTree.SourceLocation) => arrayExceptions.add(promiseArrayExceptionKey(loc))

Expand Down Expand Up @@ -293,19 +325,64 @@ export default createEslintRule<[
if (finalNode.parent
&& !isAcceptableReturnNode(finalNode.parent, !alwaysAwait)
&& !promiseArrayExceptionExists(finalNode.loc)) {
context.report({
loc: finalNode.loc,
data: { orReturned },
descriptors.push({
messageId: finalNode === targetNode
? 'asyncMustBeAwaited'
: 'promisesWithAsyncAssertionsMustBeAwaited',
node
node: finalNode
})

if (isParentArrayExpression)
pushPromiseArrayException(finalNode.loc)
}
}
},
'Program:exit'() {
const fixes: TSESLint.RuleFix[] = [];

descriptors.forEach(({ node, messageId }, index) => {
const orReturned = alwaysAwait ? '' : ' or returned';

context.report({
loc: node.loc,
data: { orReturned },
messageId,
node,
fix(fixer) {
const functionExpression = findFirstFunctionExpression(node);

if (!functionExpression)
return null;

const foundAsyncFixer = fixes.some(fix => fix.text === 'async ');

if (!functionExpression.async && !foundAsyncFixer) {
const targetFunction =
getNormalizeFunctionExpression(functionExpression);

fixes.push(fixer.insertTextBefore(targetFunction, 'async '));
}

const returnStatement =
node.parent?.type === AST_NODE_TYPES.ReturnStatement
? node.parent
: null;

if (alwaysAwait && returnStatement) {
const sourceCodeText =
context.sourceCode.getText(returnStatement);
const replacedText = sourceCodeText.replace('return', 'await');

fixes.push(fixer.replaceText(returnStatement, replacedText));
}
else {
fixes.push(fixer.insertTextBefore(node, 'await '));
}

return index === descriptors.length - 1 ? fixes : null;
},
});
});
},
}
}
})
Loading

0 comments on commit 1da7ed4

Please sign in to comment.