1- import {
2- ESLintUtils ,
3- TSESTree ,
4- ASTUtils ,
5- } from '@typescript-eslint/experimental-utils' ;
6- import { getDocsUrl } from '../utils' ;
1+ import { TSESTree , ASTUtils } from '@typescript-eslint/experimental-utils' ;
2+ import { createTestingLibraryRule } from '../create-testing-library-rule' ;
73import {
84 isImportSpecifier ,
95 isMemberExpression ,
106 findClosestCallExpressionNode ,
7+ isCallExpression ,
8+ isImportNamespaceSpecifier ,
9+ isObjectPattern ,
10+ isProperty ,
1111} from '../node-utils' ;
1212
1313export const RULE_NAME = 'prefer-wait-for' ;
14- export type MessageIds = 'preferWaitForMethod' | 'preferWaitForImport' ;
14+ export type MessageIds =
15+ | 'preferWaitForMethod'
16+ | 'preferWaitForImport'
17+ | 'preferWaitForRequire' ;
1518type Options = [ ] ;
1619
1720const DEPRECATED_METHODS = [ 'wait' , 'waitForElement' , 'waitForDomChange' ] ;
1821
19- export default ESLintUtils . RuleCreator ( getDocsUrl ) < Options , MessageIds > ( {
22+ export default createTestingLibraryRule < Options , MessageIds > ( {
2023 name : RULE_NAME ,
2124 meta : {
2225 type : 'suggestion' ,
@@ -29,14 +32,43 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
2932 preferWaitForMethod :
3033 '`{{ methodName }}` is deprecated in favour of `waitFor`' ,
3134 preferWaitForImport : 'import `waitFor` instead of deprecated async utils' ,
35+ preferWaitForRequire :
36+ 'require `waitFor` instead of deprecated async utils' ,
3237 } ,
3338
3439 fixable : 'code' ,
3540 schema : [ ] ,
3641 } ,
3742 defaultOptions : [ ] ,
3843
39- create ( context ) {
44+ create ( context , _ , helpers ) {
45+ let addWaitFor = false ;
46+
47+ const reportRequire = ( node : TSESTree . ObjectPattern ) => {
48+ context . report ( {
49+ node : node ,
50+ messageId : 'preferWaitForRequire' ,
51+ fix ( fixer ) {
52+ const excludedImports = [ ...DEPRECATED_METHODS , 'waitFor' ] ;
53+
54+ const newAllRequired = node . properties
55+ . filter (
56+ ( s ) =>
57+ isProperty ( s ) &&
58+ ASTUtils . isIdentifier ( s . key ) &&
59+ ! excludedImports . includes ( s . key . name )
60+ )
61+ . map (
62+ ( s ) => ( ( s as TSESTree . Property ) . key as TSESTree . Identifier ) . name
63+ ) ;
64+
65+ newAllRequired . push ( 'waitFor' ) ;
66+
67+ return fixer . replaceText ( node , `{ ${ newAllRequired . join ( ',' ) } }` ) ;
68+ } ,
69+ } ) ;
70+ } ;
71+
4072 const reportImport = ( node : TSESTree . ImportDeclaration ) => {
4173 context . report ( {
4274 node : node ,
@@ -115,46 +147,57 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
115147 } ;
116148
117149 return {
118- 'ImportDeclaration[source.value=/testing-library/]' (
119- node : TSESTree . ImportDeclaration
120- ) {
121- const deprecatedImportSpecifiers = node . specifiers . filter (
122- ( specifier ) =>
123- isImportSpecifier ( specifier ) &&
124- specifier . imported &&
125- DEPRECATED_METHODS . includes ( specifier . imported . name )
126- ) ;
127-
128- deprecatedImportSpecifiers . forEach ( ( importSpecifier , i ) => {
129- if ( i === 0 ) {
130- reportImport ( node ) ;
131- }
132-
133- context
134- . getDeclaredVariables ( importSpecifier )
135- . forEach ( ( variable ) =>
136- variable . references . forEach ( ( reference ) =>
137- reportWait ( reference . identifier )
138- )
139- ) ;
140- } ) ;
150+ 'CallExpression > MemberExpression' ( node : TSESTree . MemberExpression ) {
151+ const isDeprecatedMethod =
152+ ASTUtils . isIdentifier ( node . property ) &&
153+ DEPRECATED_METHODS . includes ( node . property . name ) ;
154+ if ( ! isDeprecatedMethod ) {
155+ // the method does not match a deprecated method
156+ return ;
157+ }
158+ if ( ! helpers . isNodeComingFromTestingLibrary ( node ) ) {
159+ // the method does not match from the imported elements from TL (even from custom)
160+ return ;
161+ }
162+ addWaitFor = true ;
163+ reportWait ( node . property as TSESTree . Identifier ) ; // compiler is not picking up correctly, it should have inferred it is an identifier
141164 } ,
142- 'ImportDeclaration[source.value=/testing-library/] > ImportNamespaceSpecifier' (
143- node : TSESTree . ImportNamespaceSpecifier
144- ) {
145- context . getDeclaredVariables ( node ) . forEach ( ( variable ) =>
146- variable . references . forEach ( ( reference ) => {
147- if (
148- isMemberExpression ( reference . identifier . parent ) &&
149- ASTUtils . isIdentifier ( reference . identifier . parent . property ) &&
150- DEPRECATED_METHODS . includes (
151- reference . identifier . parent . property . name
152- )
153- ) {
154- reportWait ( reference . identifier . parent . property ) ;
155- }
156- } )
157- ) ;
165+ 'CallExpression > Identifier' ( node : TSESTree . Identifier ) {
166+ if ( ! DEPRECATED_METHODS . includes ( node . name ) ) {
167+ return ;
168+ }
169+
170+ if ( ! helpers . isNodeComingFromTestingLibrary ( node ) ) {
171+ return ;
172+ }
173+ addWaitFor = true ;
174+ reportWait ( node ) ;
175+ } ,
176+ 'Program:exit' ( ) {
177+ if ( ! addWaitFor ) {
178+ return ;
179+ }
180+ // now that all usages of deprecated methods were replaced, remove the extra imports
181+ const testingLibraryNode =
182+ helpers . getCustomModuleImportNode ( ) ??
183+ helpers . getTestingLibraryImportNode ( ) ;
184+ if ( isCallExpression ( testingLibraryNode ) ) {
185+ const parent = testingLibraryNode . parent as TSESTree . VariableDeclarator ;
186+ if ( ! isObjectPattern ( parent . id ) ) {
187+ // if there is no destructuring, there is nothing to replace
188+ return ;
189+ }
190+ reportRequire ( parent . id ) ;
191+ } else {
192+ if (
193+ testingLibraryNode . specifiers . length === 1 &&
194+ isImportNamespaceSpecifier ( testingLibraryNode . specifiers [ 0 ] )
195+ ) {
196+ // if we import everything, there is nothing to replace
197+ return ;
198+ }
199+ reportImport ( testingLibraryNode ) ;
200+ }
158201 } ,
159202 } ;
160203 } ,
0 commit comments