Skip to content

Commit

Permalink
Feat/prefer todo (#145)
Browse files Browse the repository at this point in the history
* feat: added prefer todo support

* feat: prefer-to-do

* feat: prefer-to-do

* feat: update to contain

* feat: update to contain
  • Loading branch information
veritem committed Apr 7, 2023
1 parent 00e9713 commit 3148c5f
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ To use the all configuration, extend it in your `.eslintrc` file:
| [prefer-to-be-falsy](docs/rules/prefer-to-be-falsy.md) | Suggest using toBeFalsy() | 🌐 | 🔧 | |
| [prefer-to-be-object](docs/rules/prefer-to-be-object.md) | Prefer toBeObject() | 🌐 | 🔧 | |
| [prefer-to-be-truthy](docs/rules/prefer-to-be-truthy.md) | Suggest using `toBeTruthy` | 🌐 | 🔧 | |
| [prefer-to-contain](docs/rules/prefer-to-contain.md) | Prefer using toContain() | 🌐 | 🔧 | |
| [prefer-to-have-length](docs/rules/prefer-to-have-length.md) | Suggest using toHaveLength() | 🌐 | 🔧 | |
| [prefer-todo](docs/rules/prefer-todo.md) | Suggest using `test.todo` | 🌐 | 🔧 | |
| [preferMockPromiseShorthand](docs/rules/preferMockPromiseShorthand.md) | Prefer mock resolved/rejected shorthands for promises | 🌐 | 🔧 | |
Expand Down
27 changes: 27 additions & 0 deletions docs/rules/prefer-to-contain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Prefer using toContain() (`vitest/prefer-to-contain`)

⚠️ This rule _warns_ in the 🌐 `all` 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 `toBe()`, `toEqual()` or `toStrickEqual()` is used to assert object inclusion in an array.


The following patterns are considered warnings:


```ts
expect(a.includes(b)).toBe(true);
expect(a.includes(b)).toEqual(true);
expect(a.includes(b)).toStrictEqual(true);
```


The following patterns are not considered warnings:

```ts
expect(a).toContain(b);
```
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,10 @@ import validDescribeCallback, { RULE_NAME as validDescribeCallbackName } from '.
import requireTopLevelDescribe, { RULE_NAME as requireTopLevelDescribeName } from './rules/require-top-level-describe'
import requireToThrowMessage, { RULE_NAME as requireToThrowMessageName } from './rules/require-to-throw-message'
import requireHook, { RULE_NAME as requireHookName } from './rules/require-hook'
import preferSpyOn, { RULE_NAME as preferSpyOnName } from './rules/prefer-spy-on'
import preferTodo, { RULE_NAME as preferTodoName } from './rules/prefer-todo'
import preferSpyOn, { RULE_NAME as preferSpyOnName } from './rules/prefer-spy-on'
import preferComparisonMatcher, { RULE_NAME as preferComparisonMatcherName } from './rules/prefer-comparison-matcher'
import preferToContain, { RULE_NAME as preferToContainName } from './rules/prefer-to-contain'

const createConfig = (rules: Record<string, string>) => ({
plugins: ['vitest'],
Expand Down Expand Up @@ -96,9 +97,11 @@ const allRules = {
[requireTopLevelDescribeName]: 'warn',
[requireToThrowMessageName]: 'warn',
[requireHookName]: 'warn',
[preferTodoName]: 'warn',
[preferSpyOnName]: 'warn',
[preferTodoName]: 'warn',
[preferComparisonMatcherName]: 'warn'
[preferComparisonMatcherName]: 'warn',
[preferToContainName]: 'warn'
}

const recommended = {
Expand Down Expand Up @@ -158,9 +161,11 @@ export default {
[requireTopLevelDescribeName]: requireTopLevelDescribe,
[requireToThrowMessageName]: requireToThrowMessage,
[requireHookName]: requireHook,
[preferTodoName]: preferTodo,
[preferSpyOnName]: preferSpyOn,
[preferTodoName]: preferTodo,
[preferComparisonMatcherName]: preferComparisonMatcher
[preferComparisonMatcherName]: preferComparisonMatcher,
[preferToContainName]: preferToContain
},
configs: {
all: createConfig(allRules),
Expand Down
195 changes: 195 additions & 0 deletions src/rules/prefer-to-contain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { describe, test } from 'vitest'
import ruleTester from '../utils/tester'
import rule, { RULE_NAME } from './prefer-to-contain'

describe(RULE_NAME, () => {
test(RULE_NAME, () => {
ruleTester.run(RULE_NAME, rule, {
valid: [
// 'expect.hasAssertions',
// 'expect.hasAssertions()',
// 'expect.assertions(1)',
// 'expect().toBe(false);',
// 'expect(a).toContain(b);',
// 'expect(a.name).toBe(\'b\');',
// 'expect(a).toBe(true);',
// 'expect(a).toEqual(b)',
// 'expect(a.test(c)).toEqual(b)',
// 'expect(a.includes(b)).toEqual()',
// 'expect(a.includes(b)).toEqual("test")',
// 'expect(a.includes(b)).toBe("test")',
// 'expect(a.includes()).toEqual()',
// 'expect(a.includes()).toEqual(true)',
// 'expect(a.includes(b,c)).toBe(true)',
// 'expect([{a:1}]).toContain({a:1})',
// 'expect([1].includes(1)).toEqual',
// 'expect([1].includes).toEqual',
// 'expect([1].includes).not',
// 'expect(a.test(b)).resolves.toEqual(true)',
// 'expect(a.test(b)).resolves.not.toEqual(true)',
// 'expect(a).not.toContain(b)',
// 'expect(a.includes(...[])).toBe(true)',
// 'expect(a.includes(b)).toBe(...true)',
// 'expect(a);'
],
invalid: [
{
code: 'expect(a.includes(b)).toEqual(true);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b,),).toEqual(true,);',
output: 'expect(a,).toContain(b,);',
parserOptions: { ecmaVersion: 2017 },
errors: [{ messageId: 'useToContain', column: 25, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b)).toEqual(true);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 26, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b))[\'toEqual\'](true);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 26, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b)).toEqual(false);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 26, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b)).not.toEqual(false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 30, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b))[\'not\'].toEqual(false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect(a[\'includes\'](b))[\'not\'][\'toEqual\'](false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect(a.includes(b)).toEqual(false);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toEqual(false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toEqual(true);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.includes(b)).toBe(true);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b)).toBe(false);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toBe(false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toBe(true);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.includes(b)).toStrictEqual(true);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b)).toStrictEqual(false);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 23, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toStrictEqual(false);',
output: 'expect(a).toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.includes(b)).not.toStrictEqual(true);',
output: 'expect(a).not.toContain(b);',
errors: [{ messageId: 'useToContain', column: 27, line: 1 }]
},
{
code: 'expect(a.test(t).includes(b.test(p))).toEqual(true);',
output: 'expect(a.test(t)).toContain(b.test(p));',
errors: [{ messageId: 'useToContain', column: 39, line: 1 }]
},
{
code: 'expect(a.test(t).includes(b.test(p))).toEqual(false);',
output: 'expect(a.test(t)).not.toContain(b.test(p));',
errors: [{ messageId: 'useToContain', column: 39, line: 1 }]
},
{
code: 'expect(a.test(t).includes(b.test(p))).not.toEqual(true);',
output: 'expect(a.test(t)).not.toContain(b.test(p));',
errors: [{ messageId: 'useToContain', column: 43, line: 1 }]
},
{
code: 'expect(a.test(t).includes(b.test(p))).not.toEqual(false);',
output: 'expect(a.test(t)).toContain(b.test(p));',
errors: [{ messageId: 'useToContain', column: 43, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).toBe(true);',
output: 'expect([{a:1}]).toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).toBe(false);',
output: 'expect([{a:1}]).not.toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).not.toBe(true);',
output: 'expect([{a:1}]).not.toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 37, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).not.toBe(false);',
output: 'expect([{a:1}]).toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 37, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).toStrictEqual(true);',
output: 'expect([{a:1}]).toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).toStrictEqual(false);',
output: 'expect([{a:1}]).not.toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 33, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).not.toStrictEqual(true);',
output: 'expect([{a:1}]).not.toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 37, line: 1 }]
},
{
code: 'expect([{a:1}].includes({a:1})).not.toStrictEqual(false);',
output: 'expect([{a:1}]).toContain({a:1});',
errors: [{ messageId: 'useToContain', column: 37, line: 1 }]
}
]
})
})
})
94 changes: 94 additions & 0 deletions src/rules/prefer-to-contain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'
import { KnownCallExpression, createEslintRule, getAccessorValue, isSupportedAccessor } from '../utils'
import { hasOnlyOneArgument, isBooleanLiteral } from '../utils/msc'
import { getFirstMatcherArg, parseVitestFnCall } from '../utils/parseVitestFnCall'
import { CallExpressionWithSingleArgument, EqualityMatcher, ModifierName } from '../utils/types'

export const RULE_NAME = 'prefer-to-contain'
type MESSAGE_IDS = 'useToContain';
type Options = []

type FixableIncludesCallExpression = KnownCallExpression<'includes'> &
CallExpressionWithSingleArgument;

const isFixableIncludesCallExpression = (node: TSESTree.Node): node is FixableIncludesCallExpression =>
node.type === AST_NODE_TYPES.CallExpression &&
node.callee.type === AST_NODE_TYPES.MemberExpression &&
isSupportedAccessor(node.callee.property, 'includes') &&
hasOnlyOneArgument(node) &&
node.arguments[0].type !== AST_NODE_TYPES.SpreadElement

export default createEslintRule<Options, MESSAGE_IDS>({
name: RULE_NAME,
meta: {
docs: {
description: 'Prefer using toContain()',
recommended: 'warn'
},
messages: {
useToContain: 'Use toContain() instead'
},
fixable: 'code',
type: 'suggestion',
schema: []
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
const vitestFnCall = parseVitestFnCall(node, context)

if (vitestFnCall?.type !== 'expect' || vitestFnCall.args.length === 0)
return

const { parent: expect } = vitestFnCall.head.node

if (expect?.type !== AST_NODE_TYPES.CallExpression) return

const {
arguments: [includesCall],
range: [, expectCallEnd]
} = expect

const { matcher } = vitestFnCall
const matcherArg = getFirstMatcherArg(vitestFnCall)

if (
!includesCall ||
matcherArg.type === AST_NODE_TYPES.SpreadElement ||
// eslint-disable-next-line no-prototype-builtins
!EqualityMatcher.hasOwnProperty(getAccessorValue(matcher)) ||
!isBooleanLiteral(matcherArg) ||
!isFixableIncludesCallExpression(includesCall))
return

const hasNot = vitestFnCall.modifiers.some(nod => getAccessorValue(nod) === 'not')

context.report({
fix(fixer) {
const sourceCode = context.getSourceCode()

const addNotModifier = matcherArg.value === hasNot

return [
fixer.removeRange([
includesCall.callee.property.range[0] - 1,
includesCall.range[1]
]),
fixer.replaceTextRange([expectCallEnd, matcher.parent.range[1]],
addNotModifier
? `.${ModifierName.not}.toContain`
: '.toContain'),
fixer.replaceText(
vitestFnCall.args[0],
sourceCode.getText(includesCall.arguments[0])
)
]
},
messageId: 'useToContain',
node: matcher
})
}
}
}
})
4 changes: 3 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// MIT License
// Copyright (c) 2018 Jonathan Kim
// Imported from https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/utils/accessors.ts#L6
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable no-use-before-define */
// Imported from https://github.com/jest-community/eslint-plugin-jest/blob/main/src/rules/utils/accessors.ts#L6
import { TSESLint, AST_NODE_TYPES, ESLintUtils, TSESTree } from '@typescript-eslint/utils'
import { KnownMemberExpression, ParsedExpectVitestFnCall } from './parseVitestFnCall'

Expand Down

0 comments on commit 3148c5f

Please sign in to comment.