Skip to content

Commit 401bdd6

Browse files
committed
feat(eslint-plugin-query): add typescript-aware detection for custom query hooks
Extend the no-rest-destructuring rule to also report when rest destructuring is used on custom hooks (functions starting with "use") that return a TanStack Query result type. This requires the TypeScript type checker, so the logic is only executed when @typescript-eslint/parser provides type information. Affected files: noRestDestructuring.ts Signed-off-by: ChinhLee <76194645+chinhkrb113@users.noreply.github.com>
1 parent 67b12ae commit 401bdd6

File tree

1 file changed

+97
-0
lines changed

1 file changed

+97
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { createRule } from '../utils'
2+
import type { TSESTree } from '@typescript-eslint/utils'
3+
4+
export const noRestDestructuring = createRule<[], 'noRestDestructuring'>({
5+
name: 'no-rest-destructuring',
6+
meta: {
7+
type: 'problem',
8+
docs: {
9+
description: 'Disallow rest destructuring of query results',
10+
url: 'https://tanstack.com/query/latest/docs/eslint/no-rest-destructuring',
11+
},
12+
messages: {
13+
noRestDestructuring:
14+
'Destructuring the result of a query hook with a rest parameter can cause unexpected behavior. Instead, destructure the result into a variable first, then destructure the variable.',
15+
},
16+
schema: [],
17+
},
18+
defaultOptions: [],
19+
create(context) {
20+
const parserServices =
21+
context.sourceCode?.parserServices ?? context.parserServices
22+
23+
function isTanstackQueryResult(node: TSESTree.Node): boolean {
24+
if (!parserServices?.hasTypeInformation) {
25+
return false
26+
}
27+
28+
const checker = parserServices.program.getTypeChecker()
29+
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node)
30+
if (!tsNode) return false
31+
32+
const type = checker.getTypeAtLocation(tsNode)
33+
const symbol = type.symbol || type.aliasSymbol
34+
if (!symbol) return false
35+
36+
const typeName = symbol.escapedName.toString()
37+
38+
const queryResultTypes = [
39+
'UseQueryResult',
40+
'UseInfiniteQueryResult',
41+
'QueryObserverResult',
42+
'InfiniteQueryObserverResult',
43+
'UseBaseQueryResult',
44+
]
45+
46+
if (queryResultTypes.some((t) => typeName.includes(t))) {
47+
return true
48+
}
49+
50+
const declarations = symbol.declarations || []
51+
for (const decl of declarations) {
52+
const fileName = decl.getSourceFile().fileName
53+
if (fileName.includes('@tanstack') && fileName.includes('query')) {
54+
return true
55+
}
56+
}
57+
58+
return false
59+
}
60+
61+
return {
62+
VariableDeclarator(node: TSESTree.VariableDeclarator) {
63+
if (node.id.type !== 'ObjectPattern') return
64+
65+
const hasRest = node.id.properties.some(
66+
(prop) => prop.type === 'RestElement'
67+
)
68+
69+
if (!hasRest) return
70+
71+
const init = node.init
72+
73+
if (!init || init.type !== 'CallExpression') return
74+
75+
const callee = init.callee
76+
77+
let isQueryHook = false
78+
79+
if (callee.type === 'Identifier' && callee.name.startsWith('use')) {
80+
const name = callee.name
81+
if (['useQuery', 'useInfiniteQuery'].includes(name)) {
82+
isQueryHook = true
83+
} else if (parserServices?.hasTypeInformation) {
84+
isQueryHook = isTanstackQueryResult(init)
85+
}
86+
}
87+
88+
if (isQueryHook) {
89+
context.report({
90+
node,
91+
messageId: 'noRestDestructuring',
92+
})
93+
}
94+
},
95+
}
96+
},
97+
})

0 commit comments

Comments
 (0)