1
1
import { AST_NODE_TYPES , TSESLint , TSESTree } from '@typescript-eslint/utils' ;
2
2
import {
3
- KnownCallExpression ,
3
+ ParsedExpectFnCall ,
4
4
createRule ,
5
5
getAccessorValue ,
6
- hasOnlyOneArgument ,
7
6
isFunction ,
8
- isSupportedAccessor ,
9
7
isTypeOfJestFnCall ,
10
8
parseJestFnCall ,
11
9
} from './utils' ;
12
10
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
+ }
23
18
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
+ } ;
29
27
30
28
const suggestRemovingExtraArguments = (
31
29
args : TSESTree . CallExpression [ 'arguments' ] ,
@@ -107,6 +105,7 @@ export default createRule<[RuleOptions], MessageIds>({
107
105
let expressionDepth = 0 ;
108
106
let hasExpectInCallback = false ;
109
107
let hasExpectInLoop = false ;
108
+ let hasExpectAssertionsAsFirstStatement = false ;
110
109
let inTestCaseCall = false ;
111
110
let inForLoop = false ;
112
111
@@ -140,6 +139,53 @@ export default createRule<[RuleOptions], MessageIds>({
140
139
return false ;
141
140
} ;
142
141
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
+
143
189
const enterExpression = ( ) => inTestCaseCall && expressionDepth ++ ;
144
190
const exitExpression = ( ) => inTestCaseCall && expressionDepth -- ;
145
191
const enterForLoop = ( ) => ( inForLoop = true ) ;
@@ -166,6 +212,20 @@ export default createRule<[RuleOptions], MessageIds>({
166
212
}
167
213
168
214
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
+
169
229
if ( inForLoop ) {
170
230
hasExpectInLoop = true ;
171
231
}
@@ -202,92 +262,23 @@ export default createRule<[RuleOptions], MessageIds>({
202
262
hasExpectInLoop = false ;
203
263
hasExpectInCallback = false ;
204
264
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 ;
220
267
221
268
return ;
222
269
}
223
270
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
-
288
271
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
+ } ) ) ,
291
282
} ) ;
292
283
} ,
293
284
} ;
0 commit comments