Skip to content

Commit 35ddfed

Browse files
authored
feat: switch to new scope-based jest fn call parser to support @jest/globals (#20)
* feat: switch to new scope-based jest fn call parser to support `@jest/globals` * chore: remove unneeded utils * test: add some extra cases for coverage * ci: only collect coverage on runs using ESLint v8+ * chore: add back test project
1 parent 1208b32 commit 35ddfed

17 files changed

+1996
-653
lines changed

.github/workflows/nodejs.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@ name: Unit tests & Release
33
on:
44
push:
55
branches:
6-
- master
76
- main
87
- next
98
pull_request:
109
branches:
11-
- master
1210
- main
1311
- next
1412

@@ -87,10 +85,12 @@ jobs:
8785
yarn
8886
yarn add --dev eslint@${{ matrix.eslint-version }}
8987
- name: run tests
90-
run: yarn test --coverage
88+
# only collect coverage on eslint versions that support dynamic import
89+
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
9190
env:
9291
CI: true
9392
- uses: codecov/codecov-action@v3
93+
if: ${{ matrix.eslint-version >= 8 }}
9494
test-os:
9595
name: Test on ${{ matrix.os }} using Node.js LTS
9696
needs: prepare-yarn-cache
@@ -109,10 +109,12 @@ jobs:
109109
- name: install
110110
run: yarn
111111
- name: run tests
112-
run: yarn test --coverage
112+
# only collect coverage on eslint versions that support dynamic import
113+
run: yarn test --coverage ${{ matrix.eslint-version >= 8 }}
113114
env:
114115
CI: true
115116
- uses: codecov/codecov-action@v3
117+
if: ${{ matrix.eslint-version >= 8 }}
116118

117119
docs:
118120
if: ${{ github.event_name == 'pull_request' }}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,16 +112,19 @@
112112
"@babel/preset-typescript": "^7.3.3",
113113
"@commitlint/cli": "^16.0.0",
114114
"@commitlint/config-conventional": "^16.0.0",
115+
"@schemastore/package": "^0.0.6",
115116
"@semantic-release/changelog": "^6.0.0",
116117
"@semantic-release/git": "^10.0.0",
117118
"@tsconfig/node12": "^1.0.11",
119+
"@types/dedent": "^0.7.0",
118120
"@types/jest": "^28.0.0",
119121
"@types/node": "^16.0.0",
120122
"@types/prettier": "^2.7.0",
121123
"@typescript-eslint/eslint-plugin": "^5.0.0",
122124
"@typescript-eslint/parser": "^5.0.0",
123125
"babel-jest": "^28.0.0",
124126
"babel-plugin-replace-ts-export-assignment": "^0.0.2",
127+
"dedent": "^0.7.0",
125128
"eslint": "^6.0.0 || ^7.0.0 || ^8.0.0",
126129
"eslint-config-prettier": "^8.3.0",
127130
"eslint-plugin-eslint-comments": "^3.1.2",

src/rules/__tests__/prefer-to-be-array.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const createTestsForEqualityMatchers = (): Array<
5050

5151
ruleTester.run('prefer-to-be-array', rule, {
5252
valid: [
53+
'expect.hasAssertions',
54+
'expect.hasAssertions()',
5355
'expect',
5456
'expect()',
5557
'expect().toBe(true)',

src/rules/__tests__/prefer-to-be-object.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const createTestsForEqualityMatchers = (): Array<
3737
ruleTester.run('prefer-to-be-object', rule, {
3838
valid: [
3939
'expect.hasAssertions',
40+
'expect.hasAssertions()',
4041
'expect',
4142
'expect().not',
4243
'expect().toBe',

src/rules/prefer-to-be-array.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
22
import {
3-
ModifierName,
43
createRule,
54
followTypeAssertionChain,
5+
getAccessorValue,
66
isBooleanEqualityMatcher,
7-
isExpectCall,
87
isInstanceOfBinaryExpression,
98
isParsedInstanceOfMatcherCall,
109
isSupportedAccessor,
11-
parseExpectCall,
10+
parseJestFnCall,
1211
} from './utils';
1312

1413
const isArrayIsArrayCall = (
@@ -42,25 +41,21 @@ export default createRule<Options, MessageIds>({
4241
create(context) {
4342
return {
4443
CallExpression(node) {
45-
if (!isExpectCall(node)) {
46-
return;
47-
}
48-
49-
const { expect, modifier, matcher } = parseExpectCall(node);
44+
const jestFnCall = parseJestFnCall(node, context);
5045

51-
if (!matcher) {
46+
if (jestFnCall?.type !== 'expect') {
5247
return;
5348
}
5449

55-
if (isParsedInstanceOfMatcherCall(matcher, 'Array')) {
50+
if (isParsedInstanceOfMatcherCall(jestFnCall, 'Array')) {
5651
context.report({
57-
node: matcher.node.property,
52+
node: jestFnCall.matcher,
5853
messageId: 'preferToBeArray',
5954
fix: fixer => [
6055
fixer.replaceTextRange(
6156
[
62-
matcher.node.property.range[0],
63-
matcher.node.property.range[1] + '(Array)'.length,
57+
jestFnCall.matcher.range[0],
58+
jestFnCall.matcher.range[1] + '(Array)'.length,
6459
],
6560
'toBeArray()',
6661
),
@@ -70,11 +65,17 @@ export default createRule<Options, MessageIds>({
7065
return;
7166
}
7267

68+
const { parent: expect } = jestFnCall.head.node;
69+
70+
if (expect?.type !== AST_NODE_TYPES.CallExpression) {
71+
return;
72+
}
73+
7374
const [expectArg] = expect.arguments;
7475

7576
if (
7677
!expectArg ||
77-
!isBooleanEqualityMatcher(matcher) ||
78+
!isBooleanEqualityMatcher(jestFnCall) ||
7879
!(
7980
isArrayIsArrayCall(expectArg) ||
8081
isInstanceOfBinaryExpression(expectArg, 'Array')
@@ -84,11 +85,11 @@ export default createRule<Options, MessageIds>({
8485
}
8586

8687
context.report({
87-
node: matcher.node.property,
88+
node: jestFnCall.matcher,
8889
messageId: 'preferToBeArray',
8990
fix(fixer) {
9091
const fixes = [
91-
fixer.replaceText(matcher.node.property, 'toBeArray'),
92+
fixer.replaceText(jestFnCall.matcher, 'toBeArray'),
9293
expectArg.type === AST_NODE_TYPES.CallExpression
9394
? fixer.remove(expectArg.callee)
9495
: fixer.removeRange([
@@ -97,10 +98,11 @@ export default createRule<Options, MessageIds>({
9798
]),
9899
];
99100

100-
let invertCondition = matcher.name === 'toBeFalse';
101+
let invertCondition =
102+
getAccessorValue(jestFnCall.matcher) === 'toBeFalse';
101103

102-
if (matcher.arguments?.length) {
103-
const [matcherArg] = matcher.arguments;
104+
if (jestFnCall.args.length) {
105+
const [matcherArg] = jestFnCall.args;
104106

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

@@ -111,13 +113,17 @@ export default createRule<Options, MessageIds>({
111113
}
112114

113115
if (invertCondition) {
116+
const notModifier = jestFnCall.modifiers.find(
117+
nod => getAccessorValue(nod) === 'not',
118+
);
119+
114120
fixes.push(
115-
modifier && modifier.name === ModifierName.not
121+
notModifier
116122
? fixer.removeRange([
117-
modifier.node.property.range[0] - 1,
118-
modifier.node.property.range[1],
123+
notModifier.range[0] - 1,
124+
notModifier.range[1],
119125
])
120-
: fixer.insertTextBefore(matcher.node.property, 'not.'),
126+
: fixer.insertTextBefore(jestFnCall.matcher, 'not.'),
121127
);
122128
}
123129

src/rules/prefer-to-be-false.ts

Lines changed: 15 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
22
import {
3-
MaybeTypeCast,
4-
ParsedEqualityMatcherCall,
5-
ParsedExpectMatcher,
3+
EqualityMatcher,
64
createRule,
7-
followTypeAssertionChain,
8-
isExpectCall,
9-
isParsedEqualityMatcherCall,
10-
parseExpectCall,
5+
getAccessorValue,
6+
getFirstMatcherArg,
7+
parseJestFnCall,
118
} from './utils';
129

1310
interface FalseLiteral extends TSESTree.BooleanLiteral {
@@ -17,20 +14,6 @@ interface FalseLiteral extends TSESTree.BooleanLiteral {
1714
const isFalseLiteral = (node: TSESTree.Node): node is FalseLiteral =>
1815
node.type === AST_NODE_TYPES.Literal && node.value === false;
1916

20-
/**
21-
* Checks if the given `ParsedExpectMatcher` is a call to one of the equality matchers,
22-
* with a `false` literal as the sole argument.
23-
*
24-
* @param {ParsedExpectMatcher} matcher
25-
*
26-
* @return {matcher is ParsedEqualityMatcherCall<MaybeTypeCast<FalseLiteral>>}
27-
*/
28-
const isFalseEqualityMatcher = (
29-
matcher: ParsedExpectMatcher,
30-
): matcher is ParsedEqualityMatcherCall<MaybeTypeCast<FalseLiteral>> =>
31-
isParsedEqualityMatcherCall(matcher) &&
32-
isFalseLiteral(followTypeAssertionChain(matcher.arguments[0]));
33-
3417
export default createRule({
3518
name: __filename,
3619
meta: {
@@ -50,19 +33,23 @@ export default createRule({
5033
create(context) {
5134
return {
5235
CallExpression(node) {
53-
if (!isExpectCall(node)) {
36+
const jestFnCall = parseJestFnCall(node, context);
37+
38+
if (jestFnCall?.type !== 'expect') {
5439
return;
5540
}
5641

57-
const { matcher } = parseExpectCall(node);
58-
59-
if (matcher && isFalseEqualityMatcher(matcher)) {
42+
if (
43+
jestFnCall.args.length === 1 &&
44+
isFalseLiteral(getFirstMatcherArg(jestFnCall)) &&
45+
EqualityMatcher.hasOwnProperty(getAccessorValue(jestFnCall.matcher))
46+
) {
6047
context.report({
61-
node: matcher.node.property,
48+
node: jestFnCall.matcher,
6249
messageId: 'preferToBeFalse',
6350
fix: fixer => [
64-
fixer.replaceText(matcher.node.property, 'toBeFalse'),
65-
fixer.remove(matcher.arguments[0]),
51+
fixer.replaceText(jestFnCall.matcher, 'toBeFalse'),
52+
fixer.remove(jestFnCall.args[0]),
6653
],
6754
});
6855
}

src/rules/prefer-to-be-object.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
22
import {
3-
ModifierName,
43
createRule,
54
followTypeAssertionChain,
5+
getAccessorValue,
66
isBooleanEqualityMatcher,
7-
isExpectCall,
87
isInstanceOfBinaryExpression,
98
isParsedInstanceOfMatcherCall,
10-
parseExpectCall,
9+
parseJestFnCall,
1110
} from './utils';
1211

1312
export type MessageIds = 'preferToBeObject';
@@ -33,25 +32,21 @@ export default createRule<Options, MessageIds>({
3332
create(context) {
3433
return {
3534
CallExpression(node) {
36-
if (!isExpectCall(node)) {
37-
return;
38-
}
39-
40-
const { expect, modifier, matcher } = parseExpectCall(node);
35+
const jestFnCall = parseJestFnCall(node, context);
4136

42-
if (!matcher) {
37+
if (jestFnCall?.type !== 'expect') {
4338
return;
4439
}
4540

46-
if (isParsedInstanceOfMatcherCall(matcher, 'Object')) {
41+
if (isParsedInstanceOfMatcherCall(jestFnCall, 'Object')) {
4742
context.report({
48-
node: matcher.node.property,
43+
node: jestFnCall.matcher,
4944
messageId: 'preferToBeObject',
5045
fix: fixer => [
5146
fixer.replaceTextRange(
5247
[
53-
matcher.node.property.range[0],
54-
matcher.node.property.range[1] + '(Object)'.length,
48+
jestFnCall.matcher.range[0],
49+
jestFnCall.matcher.range[1] + '(Object)'.length,
5550
],
5651
'toBeObject()',
5752
),
@@ -61,29 +56,36 @@ export default createRule<Options, MessageIds>({
6156
return;
6257
}
6358

59+
const { parent: expect } = jestFnCall.head.node;
60+
61+
if (expect?.type !== AST_NODE_TYPES.CallExpression) {
62+
return;
63+
}
64+
6465
const [expectArg] = expect.arguments;
6566

6667
if (
6768
!expectArg ||
68-
!isBooleanEqualityMatcher(matcher) ||
69+
!isBooleanEqualityMatcher(jestFnCall) ||
6970
!isInstanceOfBinaryExpression(expectArg, 'Object')
7071
) {
7172
return;
7273
}
7374

7475
context.report({
75-
node: matcher.node.property,
76+
node: jestFnCall.matcher,
7677
messageId: 'preferToBeObject',
7778
fix(fixer) {
7879
const fixes = [
79-
fixer.replaceText(matcher.node.property, 'toBeObject'),
80+
fixer.replaceText(jestFnCall.matcher, 'toBeObject'),
8081
fixer.removeRange([expectArg.left.range[1], expectArg.range[1]]),
8182
];
8283

83-
let invertCondition = matcher.name === 'toBeFalse';
84+
let invertCondition =
85+
getAccessorValue(jestFnCall.matcher) === 'toBeFalse';
8486

85-
if (matcher.arguments?.length) {
86-
const [matcherArg] = matcher.arguments;
87+
if (jestFnCall.args?.length) {
88+
const [matcherArg] = jestFnCall.args;
8789

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

@@ -94,13 +96,17 @@ export default createRule<Options, MessageIds>({
9496
}
9597

9698
if (invertCondition) {
99+
const notModifier = jestFnCall.modifiers.find(
100+
nod => getAccessorValue(nod) === 'not',
101+
);
102+
97103
fixes.push(
98-
modifier && modifier.name === ModifierName.not
104+
notModifier
99105
? fixer.removeRange([
100-
modifier.node.property.range[0] - 1,
101-
modifier.node.property.range[1],
106+
notModifier.range[0] - 1,
107+
notModifier.range[1],
102108
])
103-
: fixer.insertTextBefore(matcher.node.property, 'not.'),
109+
: fixer.insertTextBefore(jestFnCall.matcher, 'not.'),
104110
);
105111
}
106112

0 commit comments

Comments
 (0)