1
- import { ESLintUtils , TSESTree } from '@typescript-eslint/experimental-utils' ;
2
- import { getDocsUrl } from '../utils ' ;
1
+ import { TSESTree } from '@typescript-eslint/experimental-utils' ;
2
+ import { createTestingLibraryRule } from '../create-testing-library-rule ' ;
3
3
import {
4
4
isImportSpecifier ,
5
5
isMemberExpression ,
6
6
isIdentifier ,
7
7
findClosestCallExpressionNode ,
8
+ isCallExpression ,
9
+ isImportDeclaration ,
10
+ isImportNamespaceSpecifier ,
11
+ isVariableDeclarator ,
12
+ isObjectPattern ,
13
+ isProperty ,
8
14
} from '../node-utils' ;
9
15
10
16
export const RULE_NAME = 'prefer-wait-for' ;
11
- export type MessageIds = 'preferWaitForMethod' | 'preferWaitForImport' ;
17
+ export type MessageIds =
18
+ | 'preferWaitForMethod'
19
+ | 'preferWaitForImport'
20
+ | 'preferWaitForRequire' ;
12
21
type Options = [ ] ;
13
22
14
23
const DEPRECATED_METHODS = [ 'wait' , 'waitForElement' , 'waitForDomChange' ] ;
15
24
16
- export default ESLintUtils . RuleCreator ( getDocsUrl ) < Options , MessageIds > ( {
25
+ export default createTestingLibraryRule < Options , MessageIds > ( {
17
26
name : RULE_NAME ,
18
27
meta : {
19
28
type : 'suggestion' ,
@@ -26,14 +35,43 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
26
35
preferWaitForMethod :
27
36
'`{{ methodName }}` is deprecated in favour of `waitFor`' ,
28
37
preferWaitForImport : 'import `waitFor` instead of deprecated async utils' ,
38
+ preferWaitForRequire :
39
+ 'require `waitFor` instead of deprecated async utils' ,
29
40
} ,
30
41
31
42
fixable : 'code' ,
32
43
schema : [ ] ,
33
44
} ,
34
45
defaultOptions : [ ] ,
35
46
36
- create ( context ) {
47
+ create ( context , _ , helpers ) {
48
+ let addWaitFor = false ;
49
+
50
+ const reportRequire = ( node : TSESTree . ObjectPattern ) => {
51
+ context . report ( {
52
+ node : node ,
53
+ messageId : 'preferWaitForRequire' ,
54
+ fix ( fixer ) {
55
+ const excludedImports = [ ...DEPRECATED_METHODS , 'waitFor' ] ;
56
+
57
+ const newAllRequired = node . properties
58
+ . filter (
59
+ ( s ) =>
60
+ isProperty ( s ) &&
61
+ isIdentifier ( s . key ) &&
62
+ ! excludedImports . includes ( s . key . name )
63
+ )
64
+ . map (
65
+ ( s ) => ( ( s as TSESTree . Property ) . key as TSESTree . Identifier ) . name
66
+ ) ;
67
+
68
+ newAllRequired . push ( 'waitFor' ) ;
69
+
70
+ return fixer . replaceText ( node , `{ ${ newAllRequired . join ( ',' ) } }` ) ;
71
+ } ,
72
+ } ) ;
73
+ } ;
74
+
37
75
const reportImport = ( node : TSESTree . ImportDeclaration ) => {
38
76
context . report ( {
39
77
node : node ,
@@ -112,46 +150,100 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
112
150
} ;
113
151
114
152
return {
115
- 'ImportDeclaration[source.value=/testing-library/]' (
116
- node : TSESTree . ImportDeclaration
117
- ) {
118
- const deprecatedImportSpecifiers = node . specifiers . filter (
119
- ( specifier ) =>
120
- isImportSpecifier ( specifier ) &&
121
- specifier . imported &&
122
- DEPRECATED_METHODS . includes ( specifier . imported . name )
123
- ) ;
124
-
125
- deprecatedImportSpecifiers . forEach ( ( importSpecifier , i ) => {
126
- if ( i === 0 ) {
127
- reportImport ( node ) ;
128
- }
129
-
130
- context
131
- . getDeclaredVariables ( importSpecifier )
132
- . forEach ( ( variable ) =>
133
- variable . references . forEach ( ( reference ) =>
134
- reportWait ( reference . identifier )
135
- )
136
- ) ;
137
- } ) ;
153
+ 'CallExpression > MemberExpression' ( node : TSESTree . MemberExpression ) {
154
+ const isDeprecatedMethod =
155
+ isIdentifier ( node . property ) &&
156
+ DEPRECATED_METHODS . includes ( node . property . name ) ;
157
+ if ( ! isDeprecatedMethod ) {
158
+ // the method does not match a deprecated method
159
+ return ;
160
+ }
161
+ const testingLibraryNode =
162
+ helpers . getCustomModuleImportNode ( ) ??
163
+ helpers . getTestingLibraryImportNode ( ) ;
164
+ // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import * as TL from 'foo'"
165
+ const callerIsTestingLibraryFromImport =
166
+ isIdentifier ( node . object ) &&
167
+ isImportDeclaration ( testingLibraryNode ) &&
168
+ isImportNamespaceSpecifier ( testingLibraryNode . specifiers [ 0 ] ) &&
169
+ node . object . name === testingLibraryNode . specifiers [ 0 ] . local . name ;
170
+ // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const tl = require('foo')"
171
+ const callerIsTestingLibraryFromRequire =
172
+ isIdentifier ( node . object ) &&
173
+ isCallExpression ( testingLibraryNode ) &&
174
+ isVariableDeclarator ( testingLibraryNode . parent ) &&
175
+ isIdentifier ( testingLibraryNode . parent . id ) &&
176
+ node . object . name === testingLibraryNode . parent . id . name ;
177
+ if (
178
+ ! callerIsTestingLibraryFromImport &&
179
+ ! callerIsTestingLibraryFromRequire
180
+ ) {
181
+ // the method does not match from the imported elements from TL (even from custom)
182
+ return ;
183
+ }
184
+ addWaitFor = true ;
185
+ reportWait ( node . property as TSESTree . Identifier ) ; // compiler is not picking up correctly, it should have inferred it is an identifier
138
186
} ,
139
- 'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier' (
140
- node : TSESTree . ImportNamespaceSpecifier
141
- ) {
142
- context . getDeclaredVariables ( node ) . forEach ( ( variable ) =>
143
- variable . references . forEach ( ( reference ) => {
144
- if (
145
- isMemberExpression ( reference . identifier . parent ) &&
146
- isIdentifier ( reference . identifier . parent . property ) &&
147
- DEPRECATED_METHODS . includes (
148
- reference . identifier . parent . property . name
149
- )
150
- ) {
151
- reportWait ( reference . identifier . parent . property ) ;
152
- }
153
- } )
154
- ) ;
187
+ 'CallExpression > Identifier' ( node : TSESTree . Identifier ) {
188
+ const testingLibraryNode =
189
+ helpers . getCustomModuleImportNode ( ) ??
190
+ helpers . getTestingLibraryImportNode ( ) ;
191
+ // this verifies the owner of the MemberExpression is the same as the node if it was imported with "import { deprecated as aliased } from 'foo'"
192
+ const callerIsTestingLibraryFromImport =
193
+ isImportDeclaration ( testingLibraryNode ) &&
194
+ testingLibraryNode . specifiers . some (
195
+ ( s ) =>
196
+ isImportSpecifier ( s ) &&
197
+ DEPRECATED_METHODS . includes ( s . imported . name ) &&
198
+ s . local . name === node . name
199
+ ) ;
200
+ // this verifies the owner of the MemberExpression is the same as the node if it was imported with "const { deprecatedMethod } = require('foo')"
201
+ const callerIsTestingLibraryFromRequire =
202
+ isCallExpression ( testingLibraryNode ) &&
203
+ isVariableDeclarator ( testingLibraryNode . parent ) &&
204
+ isObjectPattern ( testingLibraryNode . parent . id ) &&
205
+ testingLibraryNode . parent . id . properties . some (
206
+ ( p ) =>
207
+ isProperty ( p ) &&
208
+ isIdentifier ( p . key ) &&
209
+ isIdentifier ( p . value ) &&
210
+ p . value . name === node . name &&
211
+ DEPRECATED_METHODS . includes ( p . key . name )
212
+ ) ;
213
+ if (
214
+ ! callerIsTestingLibraryFromRequire &&
215
+ ! callerIsTestingLibraryFromImport
216
+ ) {
217
+ return ;
218
+ }
219
+ addWaitFor = true ;
220
+ reportWait ( node ) ;
221
+ } ,
222
+ 'Program:exit' ( ) {
223
+ if ( ! addWaitFor ) {
224
+ return ;
225
+ }
226
+ // now that all usages of deprecated methods were replaced, remove the extra imports
227
+ const testingLibraryNode =
228
+ helpers . getCustomModuleImportNode ( ) ??
229
+ helpers . getTestingLibraryImportNode ( ) ;
230
+ if ( isCallExpression ( testingLibraryNode ) ) {
231
+ const parent = testingLibraryNode . parent as TSESTree . VariableDeclarator ;
232
+ if ( ! isObjectPattern ( parent . id ) ) {
233
+ // if there is no destructuring, there is nothing to replace
234
+ return ;
235
+ }
236
+ reportRequire ( parent . id ) ;
237
+ } else {
238
+ if (
239
+ testingLibraryNode . specifiers . length === 1 &&
240
+ isImportNamespaceSpecifier ( testingLibraryNode . specifiers [ 0 ] )
241
+ ) {
242
+ // if we import everything, there is nothing to replace
243
+ return ;
244
+ }
245
+ reportImport ( testingLibraryNode ) ;
246
+ }
155
247
} ,
156
248
} ;
157
249
} ,
0 commit comments