@@ -36,7 +36,13 @@ export const rule = createRule({
3636
3737 create : detectTanstackQueryImports ( ( context , _options , helpers ) => {
3838 const trackedVariables : Record < string , string > = { }
39+ const trackedCustomHooks : Record < string , string > = { }
3940 const hookAliasMap : Record < string , string > = { }
41+ const pendingVariableDeclarators : Array < TSESTree . VariableDeclarator > = [ ]
42+ const pendingDependencyChecks : Array < {
43+ reactHook : string
44+ depsArray : TSESTree . ArrayExpression
45+ } > = [ ]
4046
4147 function getReactHook ( node : TSESTree . CallExpression ) : string | undefined {
4248 if ( node . callee . type === 'Identifier' ) {
@@ -81,6 +87,10 @@ export const rule = createRule({
8187 }
8288 }
8389
90+ function isCustomHookName ( hookName : string ) : boolean {
91+ return / ^ u s e [ A - Z 0 - 9 ] / . test ( hookName )
92+ }
93+
8494 function hasCombineProperty (
8595 callExpression : TSESTree . CallExpression ,
8696 ) : boolean {
@@ -98,6 +108,95 @@ export const rule = createRule({
98108 )
99109 }
100110
111+ function getDirectQueryHook (
112+ callExpression : TSESTree . CallExpression ,
113+ ) : string | undefined {
114+ if (
115+ callExpression . callee . type !== AST_NODE_TYPES . Identifier ||
116+ ! allHookNames . includes ( callExpression . callee . name ) ||
117+ ! helpers . isTanstackQueryImport ( callExpression . callee )
118+ ) {
119+ return undefined
120+ }
121+
122+ if (
123+ ( callExpression . callee . name === 'useQueries' ||
124+ callExpression . callee . name === 'useSuspenseQueries' ) &&
125+ hasCombineProperty ( callExpression )
126+ ) {
127+ return undefined
128+ }
129+
130+ return callExpression . callee . name
131+ }
132+
133+ function getTrackedQueryHook (
134+ callExpression : TSESTree . CallExpression ,
135+ ) : string | undefined {
136+ const directQueryHook = getDirectQueryHook ( callExpression )
137+ if ( directQueryHook !== undefined ) {
138+ return directQueryHook
139+ }
140+
141+ if ( callExpression . callee . type === AST_NODE_TYPES . Identifier ) {
142+ return trackedCustomHooks [ callExpression . callee . name ]
143+ }
144+
145+ return undefined
146+ }
147+
148+ function getReturnedQueryHook (
149+ body :
150+ | TSESTree . FunctionExpression [ 'body' ]
151+ | TSESTree . ArrowFunctionExpression [ 'body' ] ,
152+ ) : string | undefined {
153+ if ( body . type === AST_NODE_TYPES . CallExpression ) {
154+ return getDirectQueryHook ( body )
155+ }
156+
157+ if ( body . type !== AST_NODE_TYPES . BlockStatement ) {
158+ return undefined
159+ }
160+
161+ const returnStatements = body . body . filter (
162+ ( statement ) : statement is TSESTree . ReturnStatement =>
163+ statement . type === AST_NODE_TYPES . ReturnStatement ,
164+ )
165+ if ( returnStatements . length !== 1 ) {
166+ return undefined
167+ }
168+
169+ const returnArgument = returnStatements [ 0 ] ?. argument
170+ if ( returnArgument ?. type === AST_NODE_TYPES . CallExpression ) {
171+ return getDirectQueryHook ( returnArgument )
172+ }
173+
174+ return undefined
175+ }
176+
177+ function checkDependencyArray (
178+ reactHook : string ,
179+ depsArray : TSESTree . ArrayExpression ,
180+ ) {
181+ depsArray . elements . forEach ( ( dep ) => {
182+ if (
183+ dep !== null &&
184+ dep . type === AST_NODE_TYPES . Identifier &&
185+ trackedVariables [ dep . name ] !== undefined
186+ ) {
187+ const queryHook = trackedVariables [ dep . name ]
188+ context . report ( {
189+ node : dep ,
190+ messageId : 'noUnstableDeps' ,
191+ data : {
192+ queryHook,
193+ reactHook,
194+ } ,
195+ } )
196+ }
197+ } )
198+ }
199+
101200 return {
102201 ImportDeclaration ( node : TSESTree . ImportDeclaration ) {
103202 if (
@@ -118,24 +217,36 @@ export const rule = createRule({
118217 }
119218 } ,
120219
220+ FunctionDeclaration ( node ) {
221+ if ( node . id === null || ! isCustomHookName ( node . id . name ) ) {
222+ return
223+ }
224+
225+ const queryHook = getReturnedQueryHook ( node . body )
226+ if ( queryHook !== undefined ) {
227+ trackedCustomHooks [ node . id . name ] = queryHook
228+ }
229+ } ,
230+
121231 VariableDeclarator ( node ) {
122232 if (
233+ node . id . type === AST_NODE_TYPES . Identifier &&
234+ isCustomHookName ( node . id . name ) &&
123235 node . init !== null &&
124- node . init . type === AST_NODE_TYPES . CallExpression &&
125- node . init . callee . type === AST_NODE_TYPES . Identifier &&
126- allHookNames . includes ( node . init . callee . name ) &&
127- helpers . isTanstackQueryImport ( node . init . callee )
236+ ( node . init . type === AST_NODE_TYPES . ArrowFunctionExpression ||
237+ node . init . type === AST_NODE_TYPES . FunctionExpression )
128238 ) {
129- // Special case for useQueries/useSuspenseQueries with combine property - it's stable
130- if (
131- ( node . init . callee . name === 'useQueries' ||
132- node . init . callee . name === 'useSuspenseQueries' ) &&
133- hasCombineProperty ( node . init )
134- ) {
135- // Don't track useQueries/useSuspenseQueries with combine as unstable
136- return
239+ const queryHook = getReturnedQueryHook ( node . init . body )
240+ if ( queryHook !== undefined ) {
241+ trackedCustomHooks [ node . id . name ] = queryHook
137242 }
138- collectVariableNames ( node . id , node . init . callee . name )
243+ }
244+
245+ if (
246+ node . init !== null &&
247+ node . init . type === AST_NODE_TYPES . CallExpression
248+ ) {
249+ pendingVariableDeclarators . push ( node )
139250 }
140251 } ,
141252 CallExpression : ( node ) => {
@@ -145,26 +256,28 @@ export const rule = createRule({
145256 node . arguments . length > 1 &&
146257 node . arguments [ 1 ] ?. type === AST_NODE_TYPES . ArrayExpression
147258 ) {
148- const depsArray = node . arguments [ 1 ] . elements
149- depsArray . forEach ( ( dep ) => {
150- if (
151- dep !== null &&
152- dep . type === AST_NODE_TYPES . Identifier &&
153- trackedVariables [ dep . name ] !== undefined
154- ) {
155- const queryHook = trackedVariables [ dep . name ]
156- context . report ( {
157- node : dep ,
158- messageId : 'noUnstableDeps' ,
159- data : {
160- queryHook,
161- reactHook,
162- } ,
163- } )
164- }
259+ pendingDependencyChecks . push ( {
260+ reactHook,
261+ depsArray : node . arguments [ 1 ] ,
165262 } )
166263 }
167264 } ,
265+ 'Program:exit' ( ) {
266+ pendingVariableDeclarators . forEach ( ( node ) => {
267+ if ( node . init ?. type !== AST_NODE_TYPES . CallExpression ) {
268+ return
269+ }
270+
271+ const queryHook = getTrackedQueryHook ( node . init )
272+ if ( queryHook !== undefined ) {
273+ collectVariableNames ( node . id , queryHook )
274+ }
275+ } )
276+
277+ pendingDependencyChecks . forEach ( ( { reactHook, depsArray } ) => {
278+ checkDependencyArray ( reactHook , depsArray )
279+ } )
280+ } ,
168281 }
169282 } ) ,
170283} )
0 commit comments