8
8
getInnermostReturningFunction ,
9
9
getVariableReferences ,
10
10
isPromiseHandled ,
11
+ isMemberExpression ,
12
+ isCallExpression ,
11
13
} from '../node-utils' ;
12
14
13
15
export const RULE_NAME = 'await-async-query' ;
@@ -39,13 +41,128 @@ export default createTestingLibraryRule<Options, MessageIds>({
39
41
defaultOptions : [ ] ,
40
42
41
43
create ( context , _ , helpers ) {
42
- const functionWrappersNames : string [ ] = [ ] ;
44
+ const functionWrappersNamesSync : string [ ] = [ ] ;
45
+ const functionWrappersNamesAsync : string [ ] = [ ] ;
46
+
47
+ function detectSyncQueryWrapper ( node : TSESTree . Identifier ) {
48
+ const innerFunction = getInnermostReturningFunction ( context , node ) ;
49
+ if ( innerFunction ) {
50
+ functionWrappersNamesSync . push ( getFunctionName ( innerFunction ) ) ;
51
+ }
52
+ }
43
53
44
54
function detectAsyncQueryWrapper ( node : TSESTree . Identifier ) {
45
55
const innerFunction = getInnermostReturningFunction ( context , node ) ;
46
56
if ( innerFunction ) {
47
- functionWrappersNames . push ( getFunctionName ( innerFunction ) ) ;
57
+ functionWrappersNamesAsync . push ( getFunctionName ( innerFunction ) ) ;
58
+ }
59
+ }
60
+
61
+ function resolveVariable (
62
+ node : TSESTree . Node ,
63
+ scope : ReturnType < typeof context [ 'getScope' ] > | null
64
+ ) : TSESTree . Node | null {
65
+ if ( scope == null ) {
66
+ return null ;
67
+ }
68
+
69
+ if ( node . type === 'Identifier' ) {
70
+ const variable = scope . variables . find ( ( { name } ) => name === node . name ) ;
71
+
72
+ // variable not found in this scope, so recursively check parent scope(s) for definition
73
+ if ( variable == null ) {
74
+ return resolveVariable ( node , scope . upper ) ;
75
+ }
76
+
77
+ if ( variable . defs . length === 0 ) {
78
+ return null ;
79
+ }
80
+
81
+ const result = variable . defs [ variable . defs . length - 1 ] . node ;
82
+
83
+ if ( ! ASTUtils . isVariableDeclarator ( result ) ) {
84
+ return null ;
85
+ }
86
+
87
+ return result . init ;
88
+ }
89
+
90
+ return node ;
91
+ }
92
+
93
+ // true in cases like:
94
+ // - getByText('foo').findByType(SomeType)
95
+ // - (await findByText('foo')).findByType(SomeType)
96
+ // - const variable = await findByText('foo'); variable.findByType(SomeType)
97
+ // - function helper() { return screen.getByText('foo'); }; helper().findByType(SomeType)
98
+ function hasQueryResultInChain ( node : TSESTree . Node ) : boolean {
99
+ if ( ASTUtils . isIdentifier ( node ) ) {
100
+ return false ;
101
+ }
102
+
103
+ if ( ASTUtils . isAwaitExpression ( node ) ) {
104
+ // great, we have an inline await, so let's check if it's a query
105
+ const identifierNode = getDeepestIdentifierNode ( node ) ;
106
+
107
+ if ( ! identifierNode ) {
108
+ return false ;
109
+ }
110
+
111
+ if (
112
+ helpers . isAsyncQuery ( identifierNode ) &&
113
+ isPromiseHandled ( identifierNode )
114
+ ) {
115
+ return true ;
116
+ }
117
+
118
+ if (
119
+ functionWrappersNamesAsync . includes ( identifierNode . name ) &&
120
+ isPromiseHandled ( identifierNode )
121
+ ) {
122
+ return true ;
123
+ }
124
+
125
+ return false ;
126
+ }
127
+
128
+ if ( isMemberExpression ( node ) ) {
129
+ // check inline sync query (e.g. foo.getByText(...) checks `getByText`)
130
+ if (
131
+ ASTUtils . isIdentifier ( node . property ) &&
132
+ helpers . isSyncQuery ( node . property )
133
+ ) {
134
+ return true ;
135
+ }
136
+
137
+ // check sync query reference (e.g. foo.getByText(...) checks `foo` is defined elsewhere)
138
+ if ( ASTUtils . isIdentifier ( node . object ) ) {
139
+ const definition = resolveVariable ( node . object , context . getScope ( ) ) ;
140
+
141
+ if ( definition == null ) {
142
+ return false ;
143
+ }
144
+
145
+ return hasQueryResultInChain ( definition ) ;
146
+ }
147
+
148
+ // check sync query reference (e.g. foo().getByText(...) checks `foo` is defined elsewhere)
149
+ if ( isCallExpression ( node . object ) ) {
150
+ if (
151
+ ASTUtils . isIdentifier ( node . object . callee ) &&
152
+ functionWrappersNamesSync . includes ( node . object . callee . name )
153
+ ) {
154
+ return true ;
155
+ }
156
+ }
157
+
158
+ return hasQueryResultInChain ( node . object ) ;
159
+ }
160
+
161
+ if ( isCallExpression ( node ) ) {
162
+ return hasQueryResultInChain ( node . callee ) ;
48
163
}
164
+
165
+ return false ;
49
166
}
50
167
51
168
return {
@@ -56,6 +173,10 @@ export default createTestingLibraryRule<Options, MessageIds>({
56
173
return ;
57
174
}
58
175
176
+ if ( helpers . isSyncQuery ( identifierNode ) ) {
177
+ detectSyncQueryWrapper ( identifierNode ) ;
178
+ }
179
+
59
180
if ( helpers . isAsyncQuery ( identifierNode ) ) {
60
181
// detect async query used within wrapper function for later analysis
61
182
detectAsyncQueryWrapper ( identifierNode ) ;
@@ -69,6 +190,11 @@ export default createTestingLibraryRule<Options, MessageIds>({
69
190
return ;
70
191
}
71
192
193
+ // check chained usage for an instance of sync query, which means this might be a false positive from react-test-renderer
194
+ if ( hasQueryResultInChain ( closestCallExpressionNode ) ) {
195
+ return ;
196
+ }
197
+
72
198
const references = getVariableReferences (
73
199
context ,
74
200
closestCallExpressionNode . parent
@@ -104,7 +230,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
104
230
}
105
231
}
106
232
} else if (
107
- functionWrappersNames . includes ( identifierNode . name ) &&
233
+ functionWrappersNamesAsync . includes ( identifierNode . name ) &&
108
234
! isPromiseHandled ( identifierNode )
109
235
) {
110
236
// check async queries used within a wrapper previously detected
0 commit comments