Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions packages/eslint-plugin-query/src/rules/noRestDestructuring.ts
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',
Comment on lines +4 to +5
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read-only verification of rule wiring and export usage
rg -n -C2 "noRestDestructuring|no-rest-destructuring" packages/eslint-plugin-query/src
fd -i "noRestDestructuring.ts" packages/eslint-plugin-query/src
fd -i "no-rest-destructuring.rule.ts" packages/eslint-plugin-query/src

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.ts imports the rule from ./rules/no-rest-destructuring/no-rest-destructuring.rule, not from this file. This means the noRestDestructuring export here is unused and the rule's implementation must be provided by the separate no-rest-destructuring/ subdirectory file. Verify whether this file is a duplicate, stale, or represents an incomplete refactor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin-query/src/rules/noRestDestructuring.ts` around lines 4
- 5, The exported symbol noRestDestructuring (created via createRule) in this
file appears to be unused because the plugin registry imports the rule from
./rules/no-rest-destructuring/no-rest-destructuring.rule; inspect the rules.ts
registry and the subdirectory implementation to determine if this file is a
duplicate or part of an incomplete refactor, then either (a) remove this stale
export/file if the canonical implementation lives in
no-rest-destructuring/no-rest-destructuring.rule, or (b) update the registry
(rules.ts) to import and expose this noRestDestructuring export instead of the
subfolder version so there is a single source of truth; ensure the unique symbol
noRestDestructuring and createRule usage are removed or unified accordingly.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use exact type-name matching to reduce false positives.

typeName.includes(...) can match unrelated symbols (e.g., wrapper/mock types). Exact match is safer here.

Proposed fix
-      if (queryResultTypes.some((t) => typeName.includes(t))) {
+      if (queryResultTypes.includes(typeName)) {
         return true
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (queryResultTypes.some((t) => typeName.includes(t))) {
return true
if (queryResultTypes.includes(typeName)) {
return true
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin-query/src/rules/noRestDestructuring.ts` around lines
46 - 47, The current check uses typeName.includes(...) which causes false
positives; change the condition in the noRestDestructuring rule so it only
matches exact type names (e.g., replace queryResultTypes.some((t) =>
typeName.includes(t)) with queryResultTypes.some((t) => t === typeName) or use
queryResultTypes.includes(typeName)) so only exact matches of typeName trigger
the return true in that if branch.

}

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t short-circuit on hook name when type info is available.

On Line 81–83, useQuery/useInfiniteQuery are accepted by name only, which can report non-TanStack hooks with the same names. Prefer type-based gating whenever parser services are available.

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
Verify each finding against the current code and only fix it if needed.

In `@packages/eslint-plugin-query/src/rules/noRestDestructuring.ts` around lines
79 - 85, The hook-detection currently accepts call expressions named 'useQuery'
or 'useInfiniteQuery' by name even when type information is available; change
the logic so that when parserServices?.hasTypeInformation is true you always set
isQueryHook by calling isTanstackQueryResult(init) (using the existing
isTanstackQueryResult function), and only fall back to the name-based check
(checking callee.type === 'Identifier' and callee.name in
['useQuery','useInfiniteQuery'] or startsWith('use')) when
parserServices?.hasTypeInformation is falsy; update the branch around callee,
parserServices, isQueryHook and isTanstackQueryResult accordingly.

}

if (isQueryHook) {
context.report({
node,
messageId: 'noRestDestructuring',
})
}
},
}
},
})
Loading