11import { AST_NODE_TYPES , TSESLint , TSESTree } from '@typescript-eslint/utils' ;
22import {
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
3028const 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