diff --git a/README.md b/README.md index be7f0df..3a651af 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ To use the all configuration, extend it in your `.eslintrc` file: | [prefer-to-be](docs/rules/prefer-to-be.md) | Suggest using toBe() | ✅ | 🔧 | | | [prefer-to-be-false](docs/rules/prefer-to-be-false.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` | 🌐 | 🔧 | | | [valid-expect](docs/rules/valid-expect.md) | Enforce valid `expect()` usage | ✅ | | | | [valid-title](docs/rules/valid-title.md) | Enforce valid titles | ✅ | 🔧 | | diff --git a/docs/rules/prefer-to-be-truthy.md b/docs/rules/prefer-to-be-truthy.md new file mode 100644 index 0000000..c321ce0 --- /dev/null +++ b/docs/rules/prefer-to-be-truthy.md @@ -0,0 +1,17 @@ +# Suggest using `toBeTruthy` (`vitest/prefer-to-be-truthy`) + +⚠️ 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). + + + +```js +// bad +expect(foo).toBe(true) +expectTypeOf(foo).toBe(true) + +// good +expect(foo).toBeTruthy() +expectTypeOf(foo).toBeTruthy() +``` diff --git a/src/index.ts b/src/index.ts index 4ffffd6..981680c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ import validTitle, { RULE_NAME as validTitleName } from './rules/valid-title' import validExpect, { RULE_NAME as validExpectName } from './rules/valid-expect' import preferToBeFalse, { RULE_NAME as preferToBeFalseName } from './rules/prefer-to-be-false' import preferToBeObject, { RULE_NAME as preferToBeObjectName } from './rules/prefer-to-be-object' +import preferToBeTruthy, { RULE_NAME as preferToBeTruthyName } from './rules/prefer-to-be-truthy' const createConfig = (rules: Record) => ({ plugins: ['vitest'], @@ -65,7 +66,8 @@ const allRules = { [noTestReturnStatementName]: 'warn', [preferCalledWithName]: 'warn', [preferToBeFalseName]: 'warn', - [preferToBeObjectName]: 'warn' + [preferToBeObjectName]: 'warn', + [preferToBeTruthyName]: 'warn' } const recommended = { @@ -109,7 +111,8 @@ export default { [validTitleName]: validTitle, [validExpectName]: validExpect, [preferToBeFalseName]: preferToBeFalse, - [preferToBeObjectName]: preferToBeObject + [preferToBeObjectName]: preferToBeObject, + [preferToBeTruthyName]: preferToBeTruthy }, configs: { all: createConfig(allRules), diff --git a/src/rules/prefer-to-be-truthy.test.ts b/src/rules/prefer-to-be-truthy.test.ts new file mode 100644 index 0000000..fa9edaa --- /dev/null +++ b/src/rules/prefer-to-be-truthy.test.ts @@ -0,0 +1,70 @@ +import { describe, it } from 'vitest' +import ruleTester from '../utils/tester' +import rule, { RULE_NAME } from './prefer-to-be-truthy' + +const messageId = 'preferToBeTruthy' + +describe(RULE_NAME, () => { + it(RULE_NAME, () => { + ruleTester.run(RULE_NAME, rule, { + valid: [ + '[].push(true)', + 'expect("something");', + 'expect(true).toBeTrue();', + 'expect(false).toBeTrue();', + 'expect(fal,se).toBeFalse();', + 'expect(true).toBeFalse();', + 'expect(value).toEqual();', + 'expect(value).not.toBeTrue();', + 'expect(value).not.toEqual();', + 'expect(value).toBe(undefined);', + 'expect(value).not.toBe(undefined);', + 'expect(true).toBe(false)', + 'expect(value).toBe();', + 'expect(true).toMatchSnapshot();', + 'expect("a string").toMatchSnapshot(true);', + 'expect("a string").not.toMatchSnapshot();', + 'expect(something).toEqual(\'a string\');', + 'expect(true).toBe', + 'expectTypeOf(true).toBe()' + ], + invalid: [ + { + code: 'expect(false).toBe(true);', + output: 'expect(false).toBeTruthy();', + errors: [{ messageId, column: 15, line: 1 }] + }, + { + code: 'expectTypeOf(false).toBe(true);', + output: 'expectTypeOf(false).toBeTruthy();', + errors: [{ messageId, column: 21, line: 1 }] + }, + { + code: 'expect(wasSuccessful).toEqual(true);', + output: 'expect(wasSuccessful).toBeTruthy();', + errors: [{ messageId, column: 23, line: 1 }] + }, + { + code: 'expect(fs.existsSync(\'/path/to/file\')).toStrictEqual(true);', + output: 'expect(fs.existsSync(\'/path/to/file\')).toBeTruthy();', + errors: [{ messageId, column: 40, line: 1 }] + }, + { + code: 'expect("a string").not.toBe(true);', + output: 'expect("a string").not.toBeTruthy();', + errors: [{ messageId, column: 24, line: 1 }] + }, + { + code: 'expect("a string").not.toEqual(true);', + output: 'expect("a string").not.toBeTruthy();', + errors: [{ messageId, column: 24, line: 1 }] + }, + { + code: 'expectTypeOf("a string").not.toStrictEqual(true);', + output: 'expectTypeOf("a string").not.toBeTruthy();', + errors: [{ messageId, column: 30, line: 1 }] + } + ] + }) + }) +}) diff --git a/src/rules/prefer-to-be-truthy.ts b/src/rules/prefer-to-be-truthy.ts new file mode 100644 index 0000000..48e9e7d --- /dev/null +++ b/src/rules/prefer-to-be-truthy.ts @@ -0,0 +1,55 @@ +import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils' +import { createEslintRule, getAccessorValue } from '../utils' +import { getFirstMatcherArg, parseVitestFnCall } from '../utils/parseVitestFnCall' +import { EqualityMatcher } from '../utils/types' + +type MESSAGE_IDS = 'preferToBeTruthy' +export const RULE_NAME = 'prefer-to-be-truthy' +type Options = [] + +interface TrueLiteral extends TSESTree.BooleanLiteral { + value: true; +} + +const isTrueLiteral = (node: TSESTree.Node): node is TrueLiteral => + node.type === AST_NODE_TYPES.Literal && node.value === true + +export default createEslintRule({ + name: RULE_NAME, + meta: { + type: 'suggestion', + docs: { + description: 'Suggest using `toBeTruthy`', + recommended: 'warn' + }, + messages: { + preferToBeTruthy: 'Prefer using `toBeTruthy` to test value is `true`' + }, + fixable: 'code', + schema: [] + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + const vitestFnCall = parseVitestFnCall(node, context) + + if (!(vitestFnCall?.type === 'expect' || vitestFnCall?.type === 'expectTypeOf')) return + + if (vitestFnCall.args.length === 1 && + isTrueLiteral(getFirstMatcherArg(vitestFnCall)) && + // eslint-disable-next-line no-prototype-builtins + EqualityMatcher.hasOwnProperty(getAccessorValue(vitestFnCall.matcher))) { + context.report({ + node: vitestFnCall.matcher, + messageId: 'preferToBeTruthy', + fix: fixer => [ + fixer.replaceText(vitestFnCall.matcher, 'toBeTruthy'), + fixer.remove(vitestFnCall.args[0]) + ] + }) + } + } + } + } +})