-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
build(eslint-plugin-query): add typescript-aware detection for custom query hooks #10376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||
| import { createRule } from '../utils' | ||||||||||||
| import type { TSESTree } from '@typescript-eslint/utils' | ||||||||||||
|
|
||||||||||||
| export const noRestDestructuring = createRule<[], 'noRestDestructuring'>({ | ||||||||||||
| name: 'no-rest-destructuring', | ||||||||||||
| meta: { | ||||||||||||
| type: 'problem', | ||||||||||||
| docs: { | ||||||||||||
| description: 'Disallow rest destructuring of query results', | ||||||||||||
| url: 'https://tanstack.com/query/latest/docs/eslint/no-rest-destructuring', | ||||||||||||
| }, | ||||||||||||
| messages: { | ||||||||||||
| noRestDestructuring: | ||||||||||||
| '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.', | ||||||||||||
| }, | ||||||||||||
| schema: [], | ||||||||||||
| }, | ||||||||||||
| defaultOptions: [], | ||||||||||||
| create(context) { | ||||||||||||
| const parserServices = | ||||||||||||
| context.sourceCode?.parserServices ?? context.parserServices | ||||||||||||
|
|
||||||||||||
| function isTanstackQueryResult(node: TSESTree.Node): boolean { | ||||||||||||
| if (!parserServices?.hasTypeInformation) { | ||||||||||||
| return false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const checker = parserServices.program.getTypeChecker() | ||||||||||||
| const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node) | ||||||||||||
| if (!tsNode) return false | ||||||||||||
|
|
||||||||||||
| const type = checker.getTypeAtLocation(tsNode) | ||||||||||||
| const symbol = type.symbol || type.aliasSymbol | ||||||||||||
| if (!symbol) return false | ||||||||||||
|
|
||||||||||||
| const typeName = symbol.escapedName.toString() | ||||||||||||
|
|
||||||||||||
| const queryResultTypes = [ | ||||||||||||
| 'UseQueryResult', | ||||||||||||
| 'UseInfiniteQueryResult', | ||||||||||||
| 'QueryObserverResult', | ||||||||||||
| 'InfiniteQueryObserverResult', | ||||||||||||
| 'UseBaseQueryResult', | ||||||||||||
| ] | ||||||||||||
|
|
||||||||||||
| if (queryResultTypes.some((t) => typeName.includes(t))) { | ||||||||||||
| return true | ||||||||||||
|
Comment on lines
+46
to
+47
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use exact type-name matching to reduce false positives.
Proposed fix- if (queryResultTypes.some((t) => typeName.includes(t))) {
+ if (queryResultTypes.includes(typeName)) {
return true
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| const declarations = symbol.declarations || [] | ||||||||||||
| for (const decl of declarations) { | ||||||||||||
| const fileName = decl.getSourceFile().fileName | ||||||||||||
| if (fileName.includes('@tanstack') && fileName.includes('query')) { | ||||||||||||
| return true | ||||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return false | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| return { | ||||||||||||
| VariableDeclarator(node: TSESTree.VariableDeclarator) { | ||||||||||||
| if (node.id.type !== 'ObjectPattern') return | ||||||||||||
|
|
||||||||||||
| const hasRest = node.id.properties.some( | ||||||||||||
| (prop) => prop.type === 'RestElement' | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| if (!hasRest) return | ||||||||||||
|
|
||||||||||||
| const init = node.init | ||||||||||||
|
|
||||||||||||
| if (!init || init.type !== 'CallExpression') return | ||||||||||||
|
|
||||||||||||
| const callee = init.callee | ||||||||||||
|
|
||||||||||||
| let isQueryHook = false | ||||||||||||
|
|
||||||||||||
| if (callee.type === 'Identifier' && callee.name.startsWith('use')) { | ||||||||||||
| const name = callee.name | ||||||||||||
| if (['useQuery', 'useInfiniteQuery'].includes(name)) { | ||||||||||||
| isQueryHook = true | ||||||||||||
| } else if (parserServices?.hasTypeInformation) { | ||||||||||||
| isQueryHook = isTanstackQueryResult(init) | ||||||||||||
| } | ||||||||||||
|
Comment on lines
+79
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t short-circuit on hook name when type info is available. On Line 81–83, Proposed fix- if (callee.type === 'Identifier' && callee.name.startsWith('use')) {
- const name = callee.name
- if (['useQuery', 'useInfiniteQuery'].includes(name)) {
- isQueryHook = true
- } else if (parserServices?.hasTypeInformation) {
- isQueryHook = isTanstackQueryResult(init)
- }
- }
+ if (callee.type === 'Identifier' && callee.name.startsWith('use')) {
+ if (parserServices?.hasTypeInformation) {
+ isQueryHook = isTanstackQueryResult(init)
+ } else {
+ isQueryHook = ['useQuery', 'useInfiniteQuery'].includes(callee.name)
+ }
+ }🤖 Prompt for AI Agents |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if (isQueryHook) { | ||||||||||||
| context.report({ | ||||||||||||
| node, | ||||||||||||
| messageId: 'noRestDestructuring', | ||||||||||||
| }) | ||||||||||||
| } | ||||||||||||
| }, | ||||||||||||
| } | ||||||||||||
| }, | ||||||||||||
| }) | ||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: TanStack/query
Length of output: 5834
The rule export in this file is not wired into plugin registry; the plugin imports from a different file structure.
The plugin's
rules.tsimports the rule from./rules/no-rest-destructuring/no-rest-destructuring.rule, not from this file. This means thenoRestDestructuringexport here is unused and the rule's implementation must be provided by the separateno-rest-destructuring/subdirectory file. Verify whether this file is a duplicate, stale, or represents an incomplete refactor.🤖 Prompt for AI Agents