Skip to content

Commit fd54ca1

Browse files
authored
fix(prefer-expect-assertions): use scoped based jest fn call parser for expect checks (#1201)
1 parent 3843016 commit fd54ca1

File tree

2 files changed

+123
-105
lines changed

2 files changed

+123
-105
lines changed

src/rules/__tests__/prefer-expect-assertions.test.ts

+33-6
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ ruleTester.run('prefer-expect-assertions', rule, {
8888
`,
8989
options: [{ onlyFunctionsWithAsyncKeyword: true }],
9090
},
91+
{
92+
code: dedent`
93+
import { expect as pleaseExpect } from '@jest/globals';
94+
95+
it("returns numbers that are greater than four", function() {
96+
pleaseExpect.assertions(2);
97+
98+
for(let thing in things) {
99+
pleaseExpect(number).toBeGreaterThan(4);
100+
}
101+
});
102+
`,
103+
parserOptions: { sourceType: 'module' },
104+
},
91105
],
92106
invalid: [
93107
{
@@ -120,11 +134,11 @@ ruleTester.run('prefer-expect-assertions', rule, {
120134
suggestions: [
121135
{
122136
messageId: 'suggestAddingHasAssertions',
123-
output: 'it("it1", () => { expect.hasAssertions();foo()})',
137+
output: 'it("it1", () => {expect.hasAssertions(); foo()})',
124138
},
125139
{
126140
messageId: 'suggestAddingAssertions',
127-
output: 'it("it1", () => { expect.assertions();foo()})',
141+
output: 'it("it1", () => {expect.assertions(); foo()})',
128142
},
129143
],
130144
},
@@ -146,17 +160,17 @@ ruleTester.run('prefer-expect-assertions', rule, {
146160
{
147161
messageId: 'suggestAddingHasAssertions',
148162
output: dedent`
149-
it("it1", function() {
150-
expect.hasAssertions();someFunctionToDo();
163+
it("it1", function() {expect.hasAssertions();
164+
someFunctionToDo();
151165
someFunctionToDo2();
152166
});
153167
`,
154168
},
155169
{
156170
messageId: 'suggestAddingAssertions',
157171
output: dedent`
158-
it("it1", function() {
159-
expect.assertions();someFunctionToDo();
172+
it("it1", function() {expect.assertions();
173+
someFunctionToDo();
160174
someFunctionToDo2();
161175
});
162176
`,
@@ -1180,6 +1194,19 @@ ruleTester.run('prefer-expect-assertions (callbacks)', rule, {
11801194
},
11811195
],
11821196
},
1197+
{
1198+
code: dedent`
1199+
it("returns numbers that are greater than four", function(expect) {
1200+
expect.assertions(2);
1201+
1202+
for(let thing in things) {
1203+
expect(number).toBeGreaterThan(4);
1204+
}
1205+
});
1206+
`,
1207+
parserOptions: { sourceType: 'module' },
1208+
errors: [{ endColumn: 3, column: 1, messageId: 'haveExpectAssertions' }],
1209+
},
11831210
],
11841211
});
11851212

src/rules/prefer-expect-assertions.ts

+90-99
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,29 @@
11
import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils';
22
import {
3-
KnownCallExpression,
3+
ParsedExpectFnCall,
44
createRule,
55
getAccessorValue,
6-
hasOnlyOneArgument,
76
isFunction,
8-
isSupportedAccessor,
97
isTypeOfJestFnCall,
108
parseJestFnCall,
119
} from './utils';
1210

13-
const isExpectAssertionsOrHasAssertionsCall = (
14-
expression: TSESTree.Node,
15-
): expression is KnownCallExpression<'assertions' | 'hasAssertions'> =>
16-
expression.type === AST_NODE_TYPES.CallExpression &&
17-
expression.callee.type === AST_NODE_TYPES.MemberExpression &&
18-
isSupportedAccessor(expression.callee.object, 'expect') &&
19-
isSupportedAccessor(expression.callee.property) &&
20-
['assertions', 'hasAssertions'].includes(
21-
getAccessorValue(expression.callee.property),
22-
);
11+
const isFirstStatement = (node: TSESTree.CallExpression): boolean => {
12+
let parent: TSESTree.Node['parent'] = node;
13+
14+
while (parent) {
15+
if (parent.parent?.type === AST_NODE_TYPES.BlockStatement) {
16+
return parent.parent.body[0] === parent;
17+
}
2318

24-
const isFirstLineExprStmt = (
25-
functionBody: TSESTree.Statement[],
26-
): functionBody is [TSESTree.ExpressionStatement] =>
27-
functionBody[0] &&
28-
functionBody[0].type === AST_NODE_TYPES.ExpressionStatement;
19+
parent = parent.parent;
20+
}
21+
22+
/* istanbul ignore next */
23+
throw new Error(
24+
`Could not find BlockStatement - please file a github issue at https://github.com/jest-community/eslint-plugin-jest`,
25+
);
26+
};
2927

3028
const suggestRemovingExtraArguments = (
3129
args: TSESTree.CallExpression['arguments'],
@@ -107,6 +105,7 @@ export default createRule<[RuleOptions], MessageIds>({
107105
let expressionDepth = 0;
108106
let hasExpectInCallback = false;
109107
let hasExpectInLoop = false;
108+
let hasExpectAssertionsAsFirstStatement = false;
110109
let inTestCaseCall = false;
111110
let inForLoop = false;
112111

@@ -140,6 +139,53 @@ export default createRule<[RuleOptions], MessageIds>({
140139
return false;
141140
};
142141

142+
const checkExpectHasAssertions = (expectFnCall: ParsedExpectFnCall) => {
143+
if (getAccessorValue(expectFnCall.members[0]) === 'hasAssertions') {
144+
if (expectFnCall.args.length) {
145+
context.report({
146+
messageId: 'hasAssertionsTakesNoArguments',
147+
node: expectFnCall.matcher,
148+
suggest: [suggestRemovingExtraArguments(expectFnCall.args, 0)],
149+
});
150+
}
151+
152+
return;
153+
}
154+
155+
if (expectFnCall.args.length !== 1) {
156+
let { loc } = expectFnCall.matcher;
157+
const suggest: TSESLint.ReportSuggestionArray<MessageIds> = [];
158+
159+
if (expectFnCall.args.length) {
160+
loc = expectFnCall.args[1].loc;
161+
suggest.push(suggestRemovingExtraArguments(expectFnCall.args, 1));
162+
}
163+
164+
context.report({
165+
messageId: 'assertionsRequiresOneArgument',
166+
suggest,
167+
loc,
168+
});
169+
170+
return;
171+
}
172+
173+
const [arg] = expectFnCall.args;
174+
175+
if (
176+
arg.type === AST_NODE_TYPES.Literal &&
177+
typeof arg.value === 'number' &&
178+
Number.isInteger(arg.value)
179+
) {
180+
return;
181+
}
182+
183+
context.report({
184+
messageId: 'assertionsRequiresNumberArgument',
185+
node: arg,
186+
});
187+
};
188+
143189
const enterExpression = () => inTestCaseCall && expressionDepth++;
144190
const exitExpression = () => inTestCaseCall && expressionDepth--;
145191
const enterForLoop = () => (inForLoop = true);
@@ -166,6 +212,20 @@ export default createRule<[RuleOptions], MessageIds>({
166212
}
167213

168214
if (jestFnCall?.type === 'expect' && inTestCaseCall) {
215+
if (
216+
expressionDepth === 1 &&
217+
isFirstStatement(node) &&
218+
jestFnCall.head.node.parent?.type ===
219+
AST_NODE_TYPES.MemberExpression &&
220+
jestFnCall.members.length === 1 &&
221+
['assertions', 'hasAssertions'].includes(
222+
getAccessorValue(jestFnCall.members[0]),
223+
)
224+
) {
225+
checkExpectHasAssertions(jestFnCall);
226+
hasExpectAssertionsAsFirstStatement = true;
227+
}
228+
169229
if (inForLoop) {
170230
hasExpectInLoop = true;
171231
}
@@ -202,92 +262,23 @@ export default createRule<[RuleOptions], MessageIds>({
202262
hasExpectInLoop = false;
203263
hasExpectInCallback = false;
204264

205-
const testFuncBody = testFn.body.body;
206-
207-
if (!isFirstLineExprStmt(testFuncBody)) {
208-
context.report({
209-
messageId: 'haveExpectAssertions',
210-
node,
211-
suggest: suggestions.map(([messageId, text]) => ({
212-
messageId,
213-
fix: fixer =>
214-
fixer.insertTextBeforeRange(
215-
[testFn.body.range[0] + 1, testFn.body.range[1]],
216-
text,
217-
),
218-
})),
219-
});
265+
if (hasExpectAssertionsAsFirstStatement) {
266+
hasExpectAssertionsAsFirstStatement = false;
220267

221268
return;
222269
}
223270

224-
const testFuncFirstLine = testFuncBody[0].expression;
225-
226-
if (!isExpectAssertionsOrHasAssertionsCall(testFuncFirstLine)) {
227-
context.report({
228-
messageId: 'haveExpectAssertions',
229-
node,
230-
suggest: suggestions.map(([messageId, text]) => ({
231-
messageId,
232-
fix: fixer => fixer.insertTextBefore(testFuncBody[0], text),
233-
})),
234-
});
235-
236-
return;
237-
}
238-
239-
if (
240-
isSupportedAccessor(
241-
testFuncFirstLine.callee.property,
242-
'hasAssertions',
243-
)
244-
) {
245-
if (testFuncFirstLine.arguments.length) {
246-
context.report({
247-
messageId: 'hasAssertionsTakesNoArguments',
248-
node: testFuncFirstLine.callee.property,
249-
suggest: [
250-
suggestRemovingExtraArguments(testFuncFirstLine.arguments, 0),
251-
],
252-
});
253-
}
254-
255-
return;
256-
}
257-
258-
if (!hasOnlyOneArgument(testFuncFirstLine)) {
259-
let { loc } = testFuncFirstLine.callee.property;
260-
const suggest: TSESLint.ReportSuggestionArray<MessageIds> = [];
261-
262-
if (testFuncFirstLine.arguments.length) {
263-
loc = testFuncFirstLine.arguments[1].loc;
264-
suggest.push(
265-
suggestRemovingExtraArguments(testFuncFirstLine.arguments, 1),
266-
);
267-
}
268-
269-
context.report({
270-
messageId: 'assertionsRequiresOneArgument',
271-
suggest,
272-
loc,
273-
});
274-
275-
return;
276-
}
277-
278-
const [arg] = testFuncFirstLine.arguments;
279-
280-
if (
281-
arg.type === AST_NODE_TYPES.Literal &&
282-
typeof arg.value === 'number' &&
283-
Number.isInteger(arg.value)
284-
) {
285-
return;
286-
}
287-
288271
context.report({
289-
messageId: 'assertionsRequiresNumberArgument',
290-
node: arg,
272+
messageId: 'haveExpectAssertions',
273+
node,
274+
suggest: suggestions.map(([messageId, text]) => ({
275+
messageId,
276+
fix: fixer =>
277+
fixer.insertTextBeforeRange(
278+
[testFn.body.range[0] + 1, testFn.body.range[1]],
279+
text,
280+
),
281+
})),
291282
});
292283
},
293284
};

0 commit comments

Comments
 (0)