Skip to content

Commit f4dd97a

Browse files
authored
feat(no-restricted-matchers): match based on start of chain, requiring each permutation to be set (#1218)
BREAKING CHANGE: `no-restricted-matchers` now checks against the start of the expect chain, meaning you have to explicitly list each possible matcher & modifier permutations that you want to restrict
1 parent d3dff78 commit f4dd97a

File tree

3 files changed

+57
-70
lines changed

3 files changed

+57
-70
lines changed

docs/rules/no-restricted-matchers.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ alternatives.
88
Bans are expressed in the form of a map, with the value being either a string
99
message to be shown, or `null` if the default rule message should be used.
1010

11-
Both matchers, modifiers, and chains of the two are checked, allowing for
12-
specific variations of a matcher to be banned if desired.
11+
Bans are checked against the start of the `expect` chain - this means that to
12+
ban a specific matcher entirely you must specify all six permutations, but
13+
allows you to ban modifiers as well.
1314

1415
By default, this map is empty, meaning no matchers or modifiers are banned.
1516

@@ -22,7 +23,12 @@ For example:
2223
{
2324
"toBeFalsy": null,
2425
"resolves": "Use `expect(await promise)` instead.",
25-
"not.toHaveBeenCalledWith": null
26+
"toHaveBeenCalledWith": null,
27+
"not.toHaveBeenCalledWith": null,
28+
"resolves.toHaveBeenCalledWith": null,
29+
"rejects.toHaveBeenCalledWith": null,
30+
"resolves.not.toHaveBeenCalledWith": null,
31+
"rejects.not.toHaveBeenCalledWith": null
2632
}
2733
]
2834
}
@@ -32,15 +38,18 @@ Examples of **incorrect** code for this rule with the above configuration
3238

3339
```js
3440
it('is false', () => {
41+
// if this has a modifer (i.e. `not.toBeFalsy`), it would be considered fine
3542
expect(a).toBeFalsy();
3643
});
3744

3845
it('resolves', async () => {
46+
// all uses of this modifier are disallowed, regardless of matcher
3947
await expect(myPromise()).resolves.toBe(true);
4048
});
4149

4250
describe('when an error happens', () => {
4351
it('does not upload the file', async () => {
52+
// all uses of this matcher are disallowed
4453
expect(uploadFileMock).not.toHaveBeenCalledWith('file.name');
4554
});
4655
});

src/rules/__tests__/no-restricted-matchers.test.ts

+36-47
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ ruleTester.run('no-restricted-matchers', rule, {
3737
code: 'expect(a)["toBe"](b)',
3838
options: [{ 'not.toBe': null }],
3939
},
40+
{
41+
code: 'expect(a).resolves.not.toBe(b)',
42+
options: [{ not: null }],
43+
},
44+
{
45+
code: 'expect(a).resolves.not.toBe(b)',
46+
options: [{ 'not.toBe': null }],
47+
},
48+
{
49+
code: "expect(uploadFileMock).resolves.toHaveBeenCalledWith('file.name')",
50+
options: [
51+
{ 'not.toHaveBeenCalledWith': 'Use not.toHaveBeenCalled instead' },
52+
],
53+
},
54+
{
55+
code: "expect(uploadFileMock).resolves.not.toHaveBeenCalledWith('file.name')",
56+
options: [
57+
{ 'not.toHaveBeenCalledWith': 'Use not.toHaveBeenCalled instead' },
58+
],
59+
},
4060
],
4161
invalid: [
4262
{
@@ -47,7 +67,7 @@ ruleTester.run('no-restricted-matchers', rule, {
4767
messageId: 'restrictedChain',
4868
data: {
4969
message: null,
50-
chain: 'toBe',
70+
restriction: 'toBe',
5171
},
5272
column: 11,
5373
line: 1,
@@ -62,7 +82,7 @@ ruleTester.run('no-restricted-matchers', rule, {
6282
messageId: 'restrictedChain',
6383
data: {
6484
message: null,
65-
chain: 'toBe',
85+
restriction: 'toBe',
6686
},
6787
column: 11,
6888
line: 1,
@@ -77,7 +97,7 @@ ruleTester.run('no-restricted-matchers', rule, {
7797
messageId: 'restrictedChain',
7898
data: {
7999
message: null,
80-
chain: 'not',
100+
restriction: 'not',
81101
},
82102
column: 11,
83103
line: 1,
@@ -92,7 +112,7 @@ ruleTester.run('no-restricted-matchers', rule, {
92112
messageId: 'restrictedChain',
93113
data: {
94114
message: null,
95-
chain: 'not',
115+
restriction: 'not',
96116
},
97117
column: 11,
98118
line: 1,
@@ -107,28 +127,13 @@ ruleTester.run('no-restricted-matchers', rule, {
107127
messageId: 'restrictedChain',
108128
data: {
109129
message: null,
110-
chain: 'resolves',
130+
restriction: 'resolves',
111131
},
112132
column: 11,
113133
line: 1,
114134
},
115135
],
116136
},
117-
{
118-
code: 'expect(a).resolves.not.toBe(b)',
119-
options: [{ not: null }],
120-
errors: [
121-
{
122-
messageId: 'restrictedChain',
123-
data: {
124-
message: null,
125-
chain: 'not',
126-
},
127-
column: 20,
128-
line: 1,
129-
},
130-
],
131-
},
132137
{
133138
code: 'expect(a).resolves.not.toBe(b)',
134139
options: [{ resolves: null }],
@@ -137,7 +142,7 @@ ruleTester.run('no-restricted-matchers', rule, {
137142
messageId: 'restrictedChain',
138143
data: {
139144
message: null,
140-
chain: 'resolves',
145+
restriction: 'resolves',
141146
},
142147
column: 11,
143148
line: 1,
@@ -152,29 +157,13 @@ ruleTester.run('no-restricted-matchers', rule, {
152157
messageId: 'restrictedChain',
153158
data: {
154159
message: null,
155-
chain: 'resolves.not',
160+
restriction: 'resolves.not',
156161
},
157162
column: 11,
158163
line: 1,
159164
},
160165
],
161166
},
162-
{
163-
code: 'expect(a).resolves.not.toBe(b)',
164-
options: [{ 'not.toBe': null }],
165-
errors: [
166-
{
167-
messageId: 'restrictedChain',
168-
data: {
169-
message: null,
170-
chain: 'not.toBe',
171-
},
172-
endColumn: 28,
173-
column: 20,
174-
line: 1,
175-
},
176-
],
177-
},
178167
{
179168
code: 'expect(a).not.toBe(b)',
180169
options: [{ 'not.toBe': null }],
@@ -183,7 +172,7 @@ ruleTester.run('no-restricted-matchers', rule, {
183172
messageId: 'restrictedChain',
184173
data: {
185174
message: null,
186-
chain: 'not.toBe',
175+
restriction: 'not.toBe',
187176
},
188177
endColumn: 19,
189178
column: 11,
@@ -199,7 +188,7 @@ ruleTester.run('no-restricted-matchers', rule, {
199188
messageId: 'restrictedChain',
200189
data: {
201190
message: null,
202-
chain: 'resolves.not.toBe',
191+
restriction: 'resolves.not.toBe',
203192
},
204193
endColumn: 28,
205194
column: 11,
@@ -215,7 +204,7 @@ ruleTester.run('no-restricted-matchers', rule, {
215204
messageId: 'restrictedChainWithMessage',
216205
data: {
217206
message: 'Prefer `toStrictEqual` instead',
218-
chain: 'toBe',
207+
restriction: 'toBe',
219208
},
220209
column: 11,
221210
line: 1,
@@ -234,25 +223,25 @@ ruleTester.run('no-restricted-matchers', rule, {
234223
messageId: 'restrictedChainWithMessage',
235224
data: {
236225
message: 'Use `expect(await promise)` instead.',
237-
chain: 'resolves',
226+
restriction: 'resolves',
238227
},
239-
endColumn: 52,
228+
endColumn: 57,
240229
column: 44,
241230
},
242231
],
243232
},
244233
{
245234
code: 'expect(Promise.resolve({})).rejects.toBeFalsy()',
246-
options: [{ toBeFalsy: null }],
235+
options: [{ 'rejects.toBeFalsy': null }],
247236
errors: [
248237
{
249238
messageId: 'restrictedChain',
250239
data: {
251240
message: null,
252-
chain: 'toBeFalsy',
241+
restriction: 'rejects.toBeFalsy',
253242
},
254243
endColumn: 46,
255-
column: 37,
244+
column: 29,
256245
},
257246
],
258247
},
@@ -266,7 +255,7 @@ ruleTester.run('no-restricted-matchers', rule, {
266255
messageId: 'restrictedChainWithMessage',
267256
data: {
268257
message: 'Use not.toHaveBeenCalled instead',
269-
chain: 'not.toHaveBeenCalledWith',
258+
restriction: 'not.toHaveBeenCalledWith',
270259
},
271260
endColumn: 48,
272261
column: 24,

src/rules/no-restricted-matchers.ts

+9-20
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default createRule<
2121
},
2222
],
2323
messages: {
24-
restrictedChain: 'Use of `{{ chain }}` is disallowed',
24+
restrictedChain: 'Use of `{{ restriction }}` is disallowed',
2525
restrictedChainWithMessage: '{{ message }}',
2626
},
2727
},
@@ -35,31 +35,20 @@ export default createRule<
3535
return;
3636
}
3737

38-
const permutations = [jestFnCall.members];
39-
40-
if (jestFnCall.members.length > 2) {
41-
permutations.push([jestFnCall.members[0], jestFnCall.members[1]]);
42-
permutations.push([jestFnCall.members[1], jestFnCall.members[2]]);
43-
}
44-
45-
if (jestFnCall.members.length > 1) {
46-
permutations.push(...jestFnCall.members.map(nod => [nod]));
47-
}
48-
49-
for (const permutation of permutations) {
50-
const chain = permutation.map(nod => getAccessorValue(nod)).join('.');
51-
52-
if (chain in restrictedChains) {
53-
const message = restrictedChains[chain];
38+
const chain = jestFnCall.members
39+
.map(nod => getAccessorValue(nod))
40+
.join('.');
5441

42+
for (const [restriction, message] of Object.entries(restrictedChains)) {
43+
if (chain.startsWith(restriction)) {
5544
context.report({
5645
messageId: message
5746
? 'restrictedChainWithMessage'
5847
: 'restrictedChain',
59-
data: { message, chain },
48+
data: { message, restriction },
6049
loc: {
61-
start: permutation[0].loc.start,
62-
end: permutation[permutation.length - 1].loc.end,
50+
start: jestFnCall.members[0].loc.start,
51+
end: jestFnCall.members[jestFnCall.members.length - 1].loc.end,
6352
},
6453
});
6554

0 commit comments

Comments
 (0)