-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
264 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# Prefer toBeObject() (`vitest/prefer-to-be-object`) | ||
|
||
⚠️ 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 --> | ||
```js | ||
expectTypeOf({}).not.toBeInstanceOf(Object); | ||
|
||
// should be | ||
expectTypeOf({}).not.toBeObject(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { test, describe } from 'vitest' | ||
import ruleTester from '../utils/tester' | ||
import rule, { RULE_NAME } from './prefer-to-be-object' | ||
|
||
const messageId = 'preferToBeObject' | ||
|
||
describe(RULE_NAME, () => { | ||
test(RULE_NAME, () => { | ||
ruleTester.run(RULE_NAME, rule, { | ||
valid: [ | ||
'expectTypeOf.hasAssertions', | ||
'expectTypeOf.hasAssertions()', | ||
'expectTypeOf', | ||
'expectTypeOf().not', | ||
'expectTypeOf().toBe', | ||
'expectTypeOf().toBe(true)', | ||
'expectTypeOf({}).toBe(true)', | ||
'expectTypeOf({}).toBeObject()', | ||
'expectTypeOf({}).not.toBeObject()', | ||
'expectTypeOf([] instanceof Array).not.toBeObject()', | ||
'expectTypeOf({}).not.toBeInstanceOf(Array)' | ||
], | ||
invalid: [ | ||
{ | ||
code: 'expectTypeOf(({} instanceof Object)).toBeTruthy();', | ||
output: 'expectTypeOf(({})).toBeObject();', | ||
errors: [{ messageId: 'preferToBeObject', column: 38, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({} instanceof Object).toBeTruthy();', | ||
output: 'expectTypeOf({}).toBeObject();', | ||
errors: [{ messageId, column: 36, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({} instanceof Object).not.toBeTruthy();', | ||
output: 'expectTypeOf({}).not.toBeObject();', | ||
errors: [{ messageId, column: 40, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({} instanceof Object).toBeFalsy();', | ||
output: 'expectTypeOf({}).not.toBeObject();', | ||
errors: [{ messageId, column: 36, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({} instanceof Object).not.toBeFalsy();', | ||
output: 'expectTypeOf({}).toBeObject();', | ||
errors: [{ messageId, column: 40, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({}).toBeInstanceOf(Object);', | ||
output: 'expectTypeOf({}).toBeObject();', | ||
errors: [{ messageId, column: 18, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf({}).not.toBeInstanceOf(Object);', | ||
output: 'expectTypeOf({}).not.toBeObject();', | ||
errors: [{ messageId, column: 22, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf(requestValues()).resolves.toBeInstanceOf(Object);', | ||
output: 'expectTypeOf(requestValues()).resolves.toBeObject();', | ||
errors: [{ messageId, column: 40, line: 1 }] | ||
}, | ||
{ | ||
code: 'expectTypeOf(queryApi()).resolves.not.toBeInstanceOf(Object);', | ||
output: 'expectTypeOf(queryApi()).resolves.not.toBeObject();', | ||
errors: [{ messageId, column: 39, line: 1 }] | ||
} | ||
] | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { AST_NODE_TYPES } from '@typescript-eslint/utils' | ||
import { createEslintRule, getAccessorValue, isParsedInstanceOfMatcherCall } from '../utils' | ||
import { isBooleanEqualityMatcher, isInstanceOfBinaryExpression } from '../utils/msc' | ||
import { followTypeAssertionChain, parseVitestFnCall } from '../utils/parseVitestFnCall' | ||
|
||
export const RULE_NAME = 'prefer-to-be-object' | ||
export type MESSAGE_IDS = 'preferToBeObject'; | ||
export type Options = [] | ||
|
||
export default createEslintRule<Options, MESSAGE_IDS>({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Prefer toBeObject()', | ||
recommended: 'error' | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
preferToBeObject: 'Prefer toBeObject() to test if a value is an object.' | ||
}, | ||
schema: [] | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
return { | ||
CallExpression(node) { | ||
const vitestFnCall = parseVitestFnCall(node, context) | ||
|
||
if (vitestFnCall?.type !== 'expectTypeOf') | ||
return | ||
|
||
if (isParsedInstanceOfMatcherCall(vitestFnCall, 'Object')) { | ||
context.report({ | ||
node: vitestFnCall.matcher, | ||
messageId: 'preferToBeObject', | ||
fix: fixer => [ | ||
fixer.replaceTextRange( | ||
[ | ||
vitestFnCall.matcher.range[0], | ||
vitestFnCall.matcher.range[1] + '(Object)'.length | ||
], | ||
'toBeObject()' | ||
) | ||
] | ||
}) | ||
return | ||
} | ||
|
||
const { parent: expectTypeOf } = vitestFnCall.head.node | ||
|
||
if (expectTypeOf?.type !== AST_NODE_TYPES.CallExpression) | ||
return | ||
|
||
const [expectTypeOfArgs] = expectTypeOf.arguments | ||
|
||
if (!expectTypeOfArgs || | ||
!isBooleanEqualityMatcher(vitestFnCall) || | ||
!isInstanceOfBinaryExpression(expectTypeOfArgs, 'Object')) | ||
return | ||
|
||
context.report({ | ||
node: vitestFnCall.matcher, | ||
messageId: 'preferToBeObject', | ||
fix(fixer) { | ||
const fixes = [ | ||
fixer.replaceText(vitestFnCall.matcher, 'toBeObject'), | ||
fixer.removeRange([expectTypeOfArgs.left.range[1], expectTypeOfArgs.range[1]]) | ||
] | ||
|
||
let invertCondition = getAccessorValue(vitestFnCall.matcher) === 'toBeFalsy' | ||
|
||
if (vitestFnCall.args.length) { | ||
const [matcherArg] = vitestFnCall.args | ||
|
||
fixes.push(fixer.remove(matcherArg)) | ||
|
||
invertCondition = matcherArg.type === AST_NODE_TYPES.Literal && | ||
followTypeAssertionChain(matcherArg).value === false | ||
} | ||
|
||
if (invertCondition) { | ||
const notModifier = vitestFnCall.modifiers.find(node => getAccessorValue(node) === 'not') | ||
|
||
fixes.push(notModifier | ||
? fixer.removeRange([ | ||
notModifier.range[0] - 1, | ||
notModifier.range[1] | ||
]) | ||
: fixer.insertTextBefore(vitestFnCall.matcher, 'not.') | ||
) | ||
} | ||
return fixes | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils' | ||
import { getFirstMatcherArg, ParsedExpectVitestFnCall } from './parseVitestFnCall' | ||
import { EqualityMatcher } from './types' | ||
import { getAccessorValue, isSupportedAccessor } from '.' | ||
|
||
export const isBooleanLiteral = (node: TSESTree.Node): node is TSESTree.BooleanLiteral => | ||
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'boolean' | ||
|
||
/** | ||
* Checks if the given `ParsedExpectMatcher` is either a call to one of the equality matchers, | ||
* with a boolean` literal as the sole argument, *or* is a call to `toBeTruthy` or `toBeFalsy`. | ||
*/ | ||
export const isBooleanEqualityMatcher = ( | ||
expectFnCall: ParsedExpectVitestFnCall | ||
): boolean => { | ||
const matcherName = getAccessorValue(expectFnCall.matcher) | ||
|
||
if (['toBeTruthy', 'toBeFalsy'].includes(matcherName)) | ||
return true | ||
|
||
if (expectFnCall.args.length !== 1) | ||
return false | ||
|
||
const arg = getFirstMatcherArg(expectFnCall) | ||
|
||
// eslint-disable-next-line no-prototype-builtins | ||
return EqualityMatcher.hasOwnProperty(matcherName) && isBooleanLiteral(arg) | ||
} | ||
|
||
export const isInstanceOfBinaryExpression = ( | ||
node: TSESTree.Node, | ||
className: string | ||
): node is TSESTree.BinaryExpression => | ||
node.type === AST_NODE_TYPES.BinaryExpression && | ||
node.operator === 'instanceof' && | ||
isSupportedAccessor(node.right, className) |
Oops, something went wrong.