Skip to content

Commit 83a4c48

Browse files
katakonstSimenB
authored andcommittedOct 14, 2018
feat: add prefer-to-contain rule (#174)
Fixes #100
1 parent 9466959 commit 83a4c48

File tree

5 files changed

+385
-0
lines changed

5 files changed

+385
-0
lines changed
 

Diff for: ‎README.md

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ for more information about extending configuration files.
9797
| [prefer-strict-equal][] | Suggest using `toStrictEqual()` | | ![fixable-green][] |
9898
| [prefer-to-be-null][] | Suggest using `toBeNull()` | | ![fixable-green][] |
9999
| [prefer-to-be-undefined][] | Suggest using `toBeUndefined()` | | ![fixable-green][] |
100+
| [prefer-to-contain][] | Suggest using `toContain()` | | ![fixable-green][] |
100101
| [prefer-to-have-length][] | Suggest using `toHaveLength()` | ![recommended][] | ![fixable-green][] |
101102
| [prefer-inline-snapshots][] | Suggest using `toMatchInlineSnapshot()` | | ![fixable-green][] |
102103
| [require-tothrow-message][] | Require that `toThrow()` and `toThrowError` includes a message | | |
@@ -126,6 +127,7 @@ for more information about extending configuration files.
126127
[prefer-strict-equal]: docs/rules/prefer-strict-equal.md
127128
[prefer-to-be-null]: docs/rules/prefer-to-be-null.md
128129
[prefer-to-be-undefined]: docs/rules/prefer-to-be-undefined.md
130+
[prefer-to-contain]: docs/rules/prefer-to-contain.md
129131
[prefer-to-have-length]: docs/rules/prefer-to-have-length.md
130132
[prefer-inline-snapshots]: docs/rules/prefer-inline-snapshots.md
131133
[require-tothrow-message]: docs/rules/require-tothrow-message.md

Diff for: ‎docs/rules/prefer-to-contain.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Suggest using `toContain()` (prefer-to-contain)
2+
3+
In order to have a better failure message, `toContain()` should be used upon
4+
asserting expectations on an array containing an object.
5+
6+
## Rule details
7+
8+
This rule triggers a warning if `toBe()` or `isEqual()` is used to assert object
9+
inclusion in an array
10+
11+
```js
12+
expect(a.includes(b)).toBe(true);
13+
```
14+
15+
```js
16+
expect(a.includes(b)).not.toBe(true);
17+
```
18+
19+
```js
20+
expect(a.includes(b)).toBe(false);
21+
```
22+
23+
### Default configuration
24+
25+
The following patterns are considered a warning:
26+
27+
```js
28+
expect(a.includes(b)).toBe(true);
29+
```
30+
31+
```js
32+
expect(a.includes(b)).not.toBe(true);
33+
```
34+
35+
```js
36+
expect(a.includes(b)).toBe(false);
37+
```
38+
39+
The following patterns are not a warning:
40+
41+
```js
42+
expect(a).toContain(b);
43+
```
44+
45+
```js
46+
expect(a).not.toContain(b);
47+
```

Diff for: ‎index.js

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const noTestPrefixes = require('./rules/no-test-prefixes');
1414
const noTestReturnStatement = require('./rules/no-test-return-statement');
1515
const preferToBeNull = require('./rules/prefer-to-be-null');
1616
const preferToBeUndefined = require('./rules/prefer-to-be-undefined');
17+
const preferToContain = require('./rules/prefer-to-contain');
1718
const preferToHaveLength = require('./rules/prefer-to-have-length');
1819
const validDescribe = require('./rules/valid-describe');
1920
const validExpect = require('./rules/valid-expect');
@@ -84,6 +85,7 @@ module.exports = {
8485
'no-test-return-statement': noTestReturnStatement,
8586
'prefer-to-be-null': preferToBeNull,
8687
'prefer-to-be-undefined': preferToBeUndefined,
88+
'prefer-to-contain': preferToContain,
8789
'prefer-to-have-length': preferToHaveLength,
8890
'valid-describe': validDescribe,
8991
'valid-expect': validExpect,

Diff for: ‎rules/__tests__/prefer-to-contain.test.js

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
'use strict';
2+
3+
const RuleTester = require('eslint').RuleTester;
4+
const rule = require('../prefer-to-contain');
5+
6+
const ruleTester = new RuleTester();
7+
8+
ruleTester.run('prefer-to-contain', rule, {
9+
valid: [
10+
'expect(a).toContain(b);',
11+
"expect(a.name).toBe('b');",
12+
'expect(a).toBe(true);',
13+
`expect(a).toEqual(b)`,
14+
`expect(a.test(c)).toEqual(b)`,
15+
`expect(a.includes(b)).toEqual()`,
16+
`expect(a.includes(b)).toEqual("test")`,
17+
`expect(a.includes(b)).toBe("test")`,
18+
`expect(a.includes()).toEqual()`,
19+
`expect(a.includes()).toEqual(true)`,
20+
`expect(a.includes(b,c)).toBe(true)`,
21+
`expect([{a:1}]).toContain({a:1})`,
22+
`expect([1].includes(1)).toEqual`,
23+
`expect([1].includes).toEqual`,
24+
`expect([1].includes).not`,
25+
`expect(a.test(b)).resolves.toEqual(true)`,
26+
`expect(a.test(b)).resolves.not.toEqual(true)`,
27+
`expect(a).not.toContain(b)`,
28+
],
29+
invalid: [
30+
{
31+
code: 'expect(a.includes(b)).toEqual(true);',
32+
errors: [
33+
{
34+
message: 'Use toContain() instead',
35+
column: 23,
36+
line: 1,
37+
},
38+
],
39+
output: 'expect(a).toContain(b);',
40+
},
41+
{
42+
code: 'expect(a.includes(b)).toEqual(false);',
43+
errors: [
44+
{
45+
message: 'Use toContain() instead',
46+
column: 23,
47+
line: 1,
48+
},
49+
],
50+
output: 'expect(a).not.toContain(b);',
51+
},
52+
{
53+
code: 'expect(a.includes(b)).not.toEqual(false);',
54+
errors: [
55+
{
56+
message: 'Use toContain() instead',
57+
column: 23,
58+
line: 1,
59+
},
60+
],
61+
output: 'expect(a).toContain(b);',
62+
},
63+
{
64+
code: 'expect(a.includes(b)).not.toEqual(true);',
65+
errors: [
66+
{
67+
message: 'Use toContain() instead',
68+
column: 23,
69+
line: 1,
70+
},
71+
],
72+
output: 'expect(a).not.toContain(b);',
73+
},
74+
{
75+
code: 'expect(a.includes(b)).toBe(true);',
76+
errors: [
77+
{
78+
message: 'Use toContain() instead',
79+
column: 23,
80+
line: 1,
81+
},
82+
],
83+
output: 'expect(a).toContain(b);',
84+
},
85+
{
86+
code: 'expect(a.includes(b)).toBe(false);',
87+
errors: [
88+
{
89+
message: 'Use toContain() instead',
90+
column: 23,
91+
line: 1,
92+
},
93+
],
94+
output: 'expect(a).not.toContain(b);',
95+
},
96+
{
97+
code: 'expect(a.includes(b)).not.toBe(false);',
98+
errors: [
99+
{
100+
message: 'Use toContain() instead',
101+
column: 23,
102+
line: 1,
103+
},
104+
],
105+
output: 'expect(a).toContain(b);',
106+
},
107+
{
108+
code: 'expect(a.includes(b)).not.toBe(true);',
109+
errors: [
110+
{
111+
message: 'Use toContain() instead',
112+
column: 23,
113+
line: 1,
114+
},
115+
],
116+
output: 'expect(a).not.toContain(b);',
117+
},
118+
{
119+
code: 'expect(a.test(t).includes(b.test(p))).toEqual(true);',
120+
errors: [
121+
{
122+
message: 'Use toContain() instead',
123+
column: 39,
124+
line: 1,
125+
},
126+
],
127+
output: 'expect(a.test(t)).toContain(b.test(p));',
128+
},
129+
{
130+
code: 'expect(a.test(t).includes(b.test(p))).toEqual(false);',
131+
errors: [
132+
{
133+
message: 'Use toContain() instead',
134+
column: 39,
135+
line: 1,
136+
},
137+
],
138+
output: 'expect(a.test(t)).not.toContain(b.test(p));',
139+
},
140+
{
141+
code: 'expect(a.test(t).includes(b.test(p))).not.toEqual(true);',
142+
errors: [
143+
{
144+
message: 'Use toContain() instead',
145+
column: 39,
146+
line: 1,
147+
},
148+
],
149+
output: 'expect(a.test(t)).not.toContain(b.test(p));',
150+
},
151+
{
152+
code: 'expect(a.test(t).includes(b.test(p))).not.toEqual(false);',
153+
errors: [
154+
{
155+
message: 'Use toContain() instead',
156+
column: 39,
157+
line: 1,
158+
},
159+
],
160+
output: 'expect(a.test(t)).toContain(b.test(p));',
161+
},
162+
{
163+
code: 'expect([{a:1}].includes({a:1})).toBe(true);',
164+
errors: [
165+
{
166+
message: 'Use toContain() instead',
167+
column: 33,
168+
line: 1,
169+
},
170+
],
171+
output: 'expect([{a:1}]).toContain({a:1});',
172+
},
173+
{
174+
code: 'expect([{a:1}].includes({a:1})).toBe(false);',
175+
errors: [
176+
{
177+
message: 'Use toContain() instead',
178+
column: 33,
179+
line: 1,
180+
},
181+
],
182+
output: 'expect([{a:1}]).not.toContain({a:1});',
183+
},
184+
{
185+
code: 'expect([{a:1}].includes({a:1})).not.toBe(true);',
186+
errors: [
187+
{
188+
message: 'Use toContain() instead',
189+
column: 33,
190+
line: 1,
191+
},
192+
],
193+
output: 'expect([{a:1}]).not.toContain({a:1});',
194+
},
195+
{
196+
code: 'expect([{a:1}].includes({a:1})).not.toBe(false);',
197+
errors: [
198+
{
199+
message: 'Use toContain() instead',
200+
column: 33,
201+
line: 1,
202+
},
203+
],
204+
output: 'expect([{a:1}]).toContain({a:1});',
205+
},
206+
],
207+
});

Diff for: ‎rules/prefer-to-contain.js

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use strict';
2+
3+
const getDocsUrl = require('./util').getDocsUrl;
4+
const expectCase = require('./util').expectCase;
5+
const expectResolveCase = require('./util').expectResolveCase;
6+
const expectRejectCase = require('./util').expectRejectCase;
7+
const method = require('./util').method;
8+
const argument = require('./util').argument;
9+
10+
const isEqualityCheck = node =>
11+
method(node) &&
12+
(method(node).name === 'toBe' || method(node).name === 'toEqual');
13+
14+
const isArgumentValid = node =>
15+
argument(node).value === true || argument(node).value === false;
16+
17+
const hasOneArgument = node => node.arguments && node.arguments.length === 1;
18+
19+
const isValidEqualityCheck = node =>
20+
isEqualityCheck(node) &&
21+
hasOneArgument(node.parent.parent) &&
22+
isArgumentValid(node);
23+
24+
const isEqualityNegation = node =>
25+
method(node).name === 'not' && isValidEqualityCheck(node.parent);
26+
27+
const hasIncludesMethod = node =>
28+
node.arguments[0] &&
29+
node.arguments[0].callee &&
30+
node.arguments[0].callee.property &&
31+
node.arguments[0].callee.property.name === 'includes';
32+
33+
const isValidIncludesMethod = node =>
34+
hasIncludesMethod(node) && hasOneArgument(node.arguments[0]);
35+
36+
const getNegationFixes = (node, sourceCode, fixer) => {
37+
const negationPropertyDot = sourceCode.getFirstTokenBetween(
38+
node.parent.object,
39+
node.parent.property,
40+
token => token.value === '.'
41+
);
42+
const toContainFunc =
43+
isEqualityNegation(node) && argument(node.parent).value
44+
? 'not.toContain'
45+
: 'toContain';
46+
47+
//.includes function argument
48+
const containArg = node.arguments[0].arguments[0];
49+
return [
50+
fixer.remove(negationPropertyDot),
51+
fixer.remove(method(node)),
52+
fixer.replaceText(method(node.parent), toContainFunc),
53+
fixer.replaceText(argument(node.parent), sourceCode.getText(containArg)),
54+
];
55+
};
56+
57+
const getCommonFixes = (node, sourceCode, fixer) => {
58+
const containArg = node.arguments[0].arguments[0];
59+
const includesCaller = node.arguments[0].callee;
60+
61+
const propertyDot = sourceCode.getFirstTokenBetween(
62+
includesCaller.object,
63+
includesCaller.property,
64+
token => token.value === '.'
65+
);
66+
67+
const closingParenthesis = sourceCode.getTokenAfter(containArg);
68+
const openParenthesis = sourceCode.getTokenBefore(containArg);
69+
70+
return [
71+
fixer.remove(containArg),
72+
fixer.remove(includesCaller.property),
73+
fixer.remove(propertyDot),
74+
fixer.remove(closingParenthesis),
75+
fixer.remove(openParenthesis),
76+
];
77+
};
78+
79+
module.exports = {
80+
meta: {
81+
docs: {
82+
url: getDocsUrl(__filename),
83+
},
84+
fixable: 'code',
85+
},
86+
create(context) {
87+
return {
88+
CallExpression(node) {
89+
if (
90+
!(expectResolveCase(node) || expectRejectCase(node)) &&
91+
expectCase(node) &&
92+
(isEqualityNegation(node) || isValidEqualityCheck(node)) &&
93+
isValidIncludesMethod(node)
94+
) {
95+
context.report({
96+
fix(fixer) {
97+
const sourceCode = context.getSourceCode();
98+
99+
let fixArr = getCommonFixes(node, sourceCode, fixer);
100+
if (isEqualityNegation(node)) {
101+
return getNegationFixes(node, sourceCode, fixer).concat(fixArr);
102+
}
103+
104+
const toContainFunc = argument(node).value
105+
? 'toContain'
106+
: 'not.toContain';
107+
108+
//.includes function argument
109+
const containArg = node.arguments[0].arguments[0];
110+
111+
fixArr.push(fixer.replaceText(method(node), toContainFunc));
112+
fixArr.push(
113+
fixer.replaceText(
114+
argument(node),
115+
sourceCode.getText(containArg)
116+
)
117+
);
118+
return fixArr;
119+
},
120+
message: 'Use toContain() instead',
121+
node: method(node),
122+
});
123+
}
124+
},
125+
};
126+
},
127+
};

0 commit comments

Comments
 (0)
Please sign in to comment.