-
Notifications
You must be signed in to change notification settings - Fork 238
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: create
no-restricted-matchers
rule (#575)
- Loading branch information
Showing
6 changed files
with
332 additions
and
1 deletion.
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,47 @@ | ||
# Disallow specific matchers & modifiers (`no-restricted-matchers`) | ||
|
||
This rule bans specific matchers & modifiers from being used, and can suggest | ||
alternatives. | ||
|
||
## Rule Details | ||
|
||
Bans are expressed in the form of a map, with the value being either a string | ||
message to be shown, or `null` if the default rule message should be used. | ||
|
||
Both matchers, modifiers, and chains of the two are checked, allowing for | ||
specific variations of a matcher to be banned if desired. | ||
|
||
By default, this map is empty, meaning no matchers or modifiers are banned. | ||
|
||
For example: | ||
|
||
```json | ||
{ | ||
"jest/no-restricted-matchers": [ | ||
"error", | ||
{ | ||
"toBeFalsy": null, | ||
"resolves": "Use `expect(await promise)` instead.", | ||
"not.toHaveBeenCalledWith": null | ||
} | ||
] | ||
} | ||
``` | ||
|
||
Examples of **incorrect** code for this rule with the above configuration | ||
|
||
```js | ||
it('is false', () => { | ||
expect(a).toBeFalsy(); | ||
}); | ||
|
||
it('resolves', async () => { | ||
await expect(myPromise()).resolves.toBe(true); | ||
}); | ||
|
||
describe('when an error happens', () => { | ||
it('does not upload the file', async () => { | ||
expect(uploadFileMock).not.toHaveBeenCalledWith('file.name'); | ||
}); | ||
}); | ||
``` |
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,185 @@ | ||
import { TSESLint } from '@typescript-eslint/experimental-utils'; | ||
import resolveFrom from 'resolve-from'; | ||
import rule from '../no-restricted-matchers'; | ||
|
||
const ruleTester = new TSESLint.RuleTester({ | ||
parser: resolveFrom(require.resolve('eslint'), 'espree'), | ||
parserOptions: { | ||
ecmaVersion: 2017, | ||
}, | ||
}); | ||
|
||
ruleTester.run('no-restricted-matchers', rule, { | ||
valid: [ | ||
'expect(a).toHaveBeenCalled()', | ||
'expect(a).not.toHaveBeenCalled()', | ||
'expect(a).toHaveBeenCalledTimes()', | ||
'expect(a).toHaveBeenCalledWith()', | ||
'expect(a).toHaveBeenLastCalledWith()', | ||
'expect(a).toHaveBeenNthCalledWith()', | ||
'expect(a).toHaveReturned()', | ||
'expect(a).toHaveReturnedTimes()', | ||
'expect(a).toHaveReturnedWith()', | ||
'expect(a).toHaveLastReturnedWith()', | ||
'expect(a).toHaveNthReturnedWith()', | ||
'expect(a).toThrow()', | ||
'expect(a).rejects;', | ||
'expect(a);', | ||
{ | ||
code: 'expect(a).resolves', | ||
options: [{ not: null }], | ||
}, | ||
{ | ||
code: 'expect(a).toBe(b)', | ||
options: [{ 'not.toBe': null }], | ||
}, | ||
{ | ||
code: 'expect(a)["toBe"](b)', | ||
options: [{ 'not.toBe': null }], | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
code: 'expect(a).toBe(b)', | ||
options: [{ toBe: null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'toBe', | ||
}, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(a)["toBe"](b)', | ||
options: [{ toBe: null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'toBe', | ||
}, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(a).not', | ||
options: [{ not: null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'not', | ||
}, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(a).not.toBe(b)', | ||
options: [{ not: null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'not', | ||
}, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(a).not.toBe(b)', | ||
options: [{ 'not.toBe': null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'not.toBe', | ||
}, | ||
endColumn: 19, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(a).toBe(b)', | ||
options: [{ toBe: 'Prefer `toStrictEqual` instead' }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChainWithMessage', | ||
data: { | ||
message: 'Prefer `toStrictEqual` instead', | ||
chain: 'toBe', | ||
}, | ||
column: 11, | ||
line: 1, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
test('some test', async () => { | ||
await expect(Promise.resolve(1)).resolves.toBe(1); | ||
}); | ||
`, | ||
options: [{ resolves: 'Use `expect(await promise)` instead.' }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChainWithMessage', | ||
data: { | ||
message: 'Use `expect(await promise)` instead.', | ||
chain: 'resolves', | ||
}, | ||
endColumn: 52, | ||
column: 44, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: 'expect(Promise.resolve({})).rejects.toBeFalsy()', | ||
options: [{ toBeFalsy: null }], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChain', | ||
data: { | ||
message: null, | ||
chain: 'toBeFalsy', | ||
}, | ||
endColumn: 46, | ||
column: 37, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: "expect(uploadFileMock).not.toHaveBeenCalledWith('file.name')", | ||
options: [ | ||
{ 'not.toHaveBeenCalledWith': 'Use not.toHaveBeenCalled instead' }, | ||
], | ||
errors: [ | ||
{ | ||
messageId: 'restrictedChainWithMessage', | ||
data: { | ||
message: 'Use not.toHaveBeenCalled instead', | ||
chain: 'not.toHaveBeenCalledWith', | ||
}, | ||
endColumn: 48, | ||
column: 24, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |
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,97 @@ | ||
import { createRule, isExpectCall, parseExpectCall } from './utils'; | ||
|
||
export default createRule< | ||
[Record<string, string | null>], | ||
'restrictedChain' | 'restrictedChainWithMessage' | ||
>({ | ||
name: __filename, | ||
meta: { | ||
docs: { | ||
category: 'Best Practices', | ||
description: 'Disallow specific matchers & modifiers', | ||
recommended: false, | ||
}, | ||
type: 'suggestion', | ||
schema: [ | ||
{ | ||
type: 'object', | ||
additionalProperties: { | ||
type: ['string', 'null'], | ||
}, | ||
}, | ||
], | ||
messages: { | ||
restrictedChain: 'Use of `{{ chain }}` is disallowed', | ||
restrictedChainWithMessage: '{{ message }}', | ||
}, | ||
}, | ||
defaultOptions: [{}], | ||
create(context, [restrictedChains]) { | ||
return { | ||
CallExpression(node) { | ||
if (!isExpectCall(node)) { | ||
return; | ||
} | ||
|
||
const { matcher, modifier } = parseExpectCall(node); | ||
|
||
if (matcher) { | ||
const chain = matcher.name; | ||
|
||
if (chain in restrictedChains) { | ||
const message = restrictedChains[chain]; | ||
|
||
context.report({ | ||
messageId: message | ||
? 'restrictedChainWithMessage' | ||
: 'restrictedChain', | ||
data: { message, chain }, | ||
node: matcher.node.property, | ||
}); | ||
|
||
return; | ||
} | ||
} | ||
|
||
if (modifier) { | ||
const chain = modifier.name; | ||
|
||
if (chain in restrictedChains) { | ||
const message = restrictedChains[chain]; | ||
|
||
context.report({ | ||
messageId: message | ||
? 'restrictedChainWithMessage' | ||
: 'restrictedChain', | ||
data: { message, chain }, | ||
node: modifier.node.property, | ||
}); | ||
|
||
return; | ||
} | ||
} | ||
|
||
if (matcher && modifier) { | ||
const chain = `${modifier.name}.${matcher.name}`; | ||
|
||
if (chain in restrictedChains) { | ||
const message = restrictedChains[chain]; | ||
|
||
context.report({ | ||
messageId: message | ||
? 'restrictedChainWithMessage' | ||
: 'restrictedChain', | ||
data: { message, chain }, | ||
loc: { | ||
start: modifier.node.property.loc.start, | ||
end: matcher.node.property.loc.end, | ||
}, | ||
}); | ||
|
||
return; | ||
} | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |