Skip to content

Commit

Permalink
feat: create prefer-to-be-object rule
Browse files Browse the repository at this point in the history
  • Loading branch information
G-Rath committed Feb 29, 2020
1 parent 9bd067c commit 676de1d
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ for installations requiring long-term consistency.
| ----------------------- | ---------------------------- | ------------------ |
| [prefer-to-be-array][] | Suggest using `toBeArray()` | ![fixable-green][] |
| [prefer-to-be-false][] | Suggest using `toBeFalse()` | ![fixable-green][] |
| [prefer-to-be-object][] | Suggest using `toBeObject()` | ![fixable-green][] |
| [prefer-to-be-true][] | Suggest using `toBeTrue()` | ![fixable-green][] |

## Credit
Expand All @@ -94,6 +95,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting

[prefer-to-be-array]: docs/rules/prefer-to-be-array.md
[prefer-to-be-false]: docs/rules/prefer-to-be-false.md
[prefer-to-be-object]: docs/rules/prefer-to-be-object.md
[prefer-to-be-true]: docs/rules/prefer-to-be-true.md
[fixable-green]: https://img.shields.io/badge/-fixable-green.svg
[fixable-yellow]: https://img.shields.io/badge/-fixable-yellow.svg
44 changes: 44 additions & 0 deletions docs/rules/prefer-to-be-object.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Suggest using `toBeObject()` (prefer-to-be-object)

For expecting a value to be an object, `jest-extended` provides the `toBeObject`
matcher.

## Rule details

This rule triggers a warning if an `expect` assertion is found asserting that a
value is an object using one of the following methods:

- Comparing the result of `<value> instanceof Object` to a boolean value,
- Calling the `toBeInstanceOf` matcher with the `Object` class.

The following patterns are considered warnings:

```js
expect([] instanceof Object).toBe(true);

expect(myValue instanceof Object).toStrictEqual(false);

expect(theResults() instanceof Object).not.toBeFalse();

expect([]).toBeInstanceOf(true);

expect(myValue).resolves.toBeInstanceOf(Object);

expect(theResults()).not.toBeInstanceOf(Object);
```

The following patterns are _not_ considered warnings:

```js
expect({}).toBeObject();

expect(myValue).not.toBeObject();

expect(queryApi()).resolves.toBeObject();

expect(theResults()).toBeObject();
```

## Further Reading

- [`jest-extended#toBeObject` matcher](https://github.com/jest-community/jest-extended#tobeobject)
1 change: 1 addition & 0 deletions src/__tests__/__snapshots__/rules.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Object {
"rules": Object {
"jest/prefer-to-be-array": "error",
"jest/prefer-to-be-false": "error",
"jest/prefer-to-be-object": "error",
"jest/prefer-to-be-true": "error",
},
},
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolve } from 'path';
import plugin from '../';

const ruleNames = Object.keys(plugin.rules);
const numberOfRules = 3;
const numberOfRules = 4;

describe('rules', () => {
it('should have a corresponding doc for each rule', () => {
Expand Down
99 changes: 99 additions & 0 deletions src/rules/__tests__/prefer-to-be-object.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { TSESLint } from '@typescript-eslint/experimental-utils';
import rule, { MessageIds, Options } from '../prefer-to-be-object';

const ruleTester = new TSESLint.RuleTester();

// makes ts happy about the dynamic test generation
const messageId = 'preferToBeObject' as const;

const createTestsForEqualityMatchers = (): Array<TSESLint.InvalidTestCase<
MessageIds,
Options
>> =>
['toBe', 'toEqual', 'toStrictEqual']
.map(matcher => [
{
code: `expect({} instanceof Object).${matcher}(true);`,
errors: [{ messageId, column: 30, line: 1 }],
output: `expect({}).toBeObject();`,
},
{
code: `expect({} instanceof Object).not.${matcher}(true);`,
errors: [{ messageId, column: 34, line: 1 }],
output: `expect({}).not.toBeObject();`,
},
{
code: `expect({} instanceof Object).${matcher}(false);`,
errors: [{ messageId, column: 30, line: 1 }],
output: `expect({}).not.toBeObject();`,
},
{
code: `expect({} instanceof Object).not.${matcher}(false);`,
errors: [{ messageId, column: 34, line: 1 }],
output: `expect({}).toBeObject();`,
},
])
.reduce((arr, cur) => arr.concat(cur), []);

ruleTester.run('prefer-to-be-object', rule, {
valid: [
'expect.hasAssertions',
'expect',
'expect().not',
'expect().toBe',
'expect().toBe(true)',
'expect({}).toBe(true)',
'expect({}).toBeObject()',
'expect({}).not.toBeObject()',
'expect([] instanceof Array).not.toBeObject()',
'expect({}).not.toBeInstanceOf(Array)',
],
invalid: [
...createTestsForEqualityMatchers(),
{
code: 'expect(({} instanceof Object)).toBeTrue();',
errors: [{ messageId, column: 32, line: 1 }],
output: 'expect(({})).toBeObject();',
},
{
code: 'expect({} instanceof Object).toBeTrue();',
errors: [{ messageId, column: 30, line: 1 }],
output: 'expect({}).toBeObject();',
},
{
code: 'expect({} instanceof Object).not.toBeTrue();',
errors: [{ messageId, column: 34, line: 1 }],
output: 'expect({}).not.toBeObject();',
},
{
code: 'expect({} instanceof Object).toBeFalse();',
errors: [{ messageId, column: 30, line: 1 }],
output: 'expect({}).not.toBeObject();',
},
{
code: 'expect({} instanceof Object).not.toBeFalse();',
errors: [{ messageId, column: 34, line: 1 }],
output: 'expect({}).toBeObject();',
},
{
code: 'expect({}).toBeInstanceOf(Object);',
errors: [{ messageId, column: 12, line: 1 }],
output: 'expect({}).toBeObject();',
},
{
code: 'expect({}).not.toBeInstanceOf(Object);',
errors: [{ messageId, column: 16, line: 1 }],
output: 'expect({}).not.toBeObject();',
},
{
code: 'expect(requestValues()).resolves.toBeInstanceOf(Object);',
errors: [{ messageId, column: 34, line: 1 }],
output: 'expect(requestValues()).resolves.toBeObject();',
},
{
code: 'expect(queryApi()).resolves.not.toBeInstanceOf(Object);',
errors: [{ messageId, column: 33, line: 1 }],
output: 'expect(queryApi()).resolves.not.toBeObject();',
},
],
});
113 changes: 113 additions & 0 deletions src/rules/prefer-to-be-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
import {
ModifierName,
createRule,
followTypeAssertionChain,
isBooleanEqualityMatcher,
isExpectCall,
isInstanceOfBinaryExpression,
isParsedInstanceOfMatcherCall,
parseExpectCall,
} from './utils';

export type MessageIds = 'preferToBeObject';
export type Options = [];

export default createRule<Options, MessageIds>({
name: __filename,
meta: {
docs: {
category: 'Stylistic Issues',
description: 'Suggest using `toBeObject()`',
recommended: false,
},
messages: {
preferToBeObject:
'Prefer using `toBeObject()` to test if a value is an Object.',
},
fixable: 'code',
type: 'suggestion',
schema: [],
},
defaultOptions: [],
create(context) {
return {
CallExpression(node) {
if (!isExpectCall(node)) {
return;
}

const { expect, modifier, matcher } = parseExpectCall(node);

if (!matcher) {
return;
}

if (isParsedInstanceOfMatcherCall(matcher, 'Object')) {
context.report({
node: matcher.node.property,
messageId: 'preferToBeObject',
fix: fixer => [
fixer.replaceTextRange(
[
matcher.node.property.range[0],
matcher.node.property.range[1] + '(Object)'.length,
],
'toBeObject()',
),
],
});

return;
}

const [expectArg] = expect.arguments;

if (
!expectArg ||
!isBooleanEqualityMatcher(matcher) ||
!isInstanceOfBinaryExpression(expectArg, 'Object')
) {
return;
}

context.report({
node: matcher.node.property,
messageId: 'preferToBeObject',
fix(fixer) {
const fixes = [
fixer.replaceText(matcher.node.property, 'toBeObject'),
fixer.removeRange([expectArg.left.range[1], expectArg.range[1]]),
];

let invertCondition = matcher.name === 'toBeFalse';

if (matcher.arguments?.length) {
const [matcherArg] = matcher.arguments;

fixes.push(fixer.remove(matcherArg));

// toBeFalse can't have arguments, so this won't be true beforehand
invertCondition =
matcherArg.type === AST_NODE_TYPES.Literal &&
followTypeAssertionChain(matcherArg).value === false;
}

if (invertCondition) {
fixes.push(
modifier && modifier.name === ModifierName.not
? fixer.removeRange([
modifier.node.property.range[0] - 1,
modifier.node.property.range[1],
])
: fixer.insertTextBefore(matcher.node.property, 'not.'),
);
}

return fixes;
},
});
},
};
},
});

0 comments on commit 676de1d

Please sign in to comment.