Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,19 @@ ruleTester.run('no-rest-destructuring', rule, {
}
`,
},
{
name: 'custom hook returning non-tanstack useQuery is destructured with rest',
code: normalizeIndent`
import { useQuery } from 'other-package'

const useTodos = () => useQuery()

function Component() {
const { data, ...rest } = useTodos()
return
}
`,
},
],
invalid: [
{
Expand Down Expand Up @@ -390,5 +403,87 @@ ruleTester.run('no-rest-destructuring', rule, {
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'custom hook that returns useQuery is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'

function useTodos() {
return useQuery()
}

function Component() {
const { data, ...rest } = useTodos()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'custom hook with arrow function is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'

const useTodos = () => useQuery()

function Component() {
const { data, ...rest } = useTodos()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'custom hook with arrow function block is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'

const useTodos = () => {
return useQuery({
queryKey: ['todos'],
queryFn: () => fetch('example.com/todos'),
})
}

function Component() {
const { data, ...rest } = useTodos()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'custom hook returning query result variable is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'

const useTodos = () => {
const query = useQuery()
return query
}

function Component() {
const { data, ...rest } = useTodos()
return
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
{
name: 'custom hook declared after component is destructured with rest',
code: normalizeIndent`
import { useQuery } from '@tanstack/react-query'

function Component() {
const { data, ...rest } = useTodos()
return
}

function useTodos() {
return useQuery()
}
`,
errors: [{ messageId: 'objectRestDestructure' }],
},
],
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getDocsUrl } from '../../utils/get-docs-url'
import { ASTUtils } from '../../utils/ast-utils'
import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
import { NoRestDestructuringUtils } from './no-rest-destructuring.utils'
import type { TSESTree } from '@typescript-eslint/utils'
import type { ExtraRuleDocs } from '../../types'

export const name = 'no-rest-destructuring'
Expand Down Expand Up @@ -36,16 +37,176 @@ export const rule = createRule({
create: detectTanstackQueryImports((context, _, helpers) => {
const queryResultVariables = new Set<string>()

const isTanstackQueryHook = (identifier: TSESTree.Identifier): boolean => {
return (
ASTUtils.isIdentifierWithOneOfNames(identifier, queryHooks) &&
helpers.isTanstackQueryImport(identifier)
)
}

const unwrap = (node: TSESTree.Node): TSESTree.Node => {
if (
node.type === AST_NODE_TYPES.TSAsExpression ||
node.type === AST_NODE_TYPES.TSSatisfiesExpression ||
node.type === AST_NODE_TYPES.TSTypeAssertion ||
node.type === AST_NODE_TYPES.ChainExpression ||
node.type === AST_NODE_TYPES.TSNonNullExpression
) {
return unwrap(node.expression)
}

return node
}

const getReferencedNode = (
identifier: TSESTree.Identifier,
): TSESTree.Node | null => {
const referencedExpression = ASTUtils.getReferencedExpressionByIdentifier(
{
context,
node: identifier,
},
)

if (referencedExpression !== null) {
return referencedExpression
}

const scope = context.sourceCode.getScope(identifier)
const reference = scope.references.find(
(ref) => ref.identifier === identifier,
)
const definition = reference?.resolved?.defs[0]?.node as
| TSESTree.Node
| undefined

if (definition?.type === AST_NODE_TYPES.VariableDeclarator) {
return definition.init ?? null
}

if (definition !== undefined) {
return definition
}

return null
}

const getDirectReturnExpression = (
fn:
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression
| TSESTree.ArrowFunctionExpression,
): TSESTree.Expression | null => {
if (fn.body.type !== AST_NODE_TYPES.BlockStatement) {
return fn.body
}

const returnStatements = fn.body.body.filter(
(statement): statement is TSESTree.ReturnStatement =>
statement.type === AST_NODE_TYPES.ReturnStatement,
)

if (returnStatements.length !== 1) {
return null
}

const [returnStatement] = returnStatements

return returnStatement?.argument ?? null
}

const isQueryResultNode = (
node: TSESTree.Node | null,
seen: Set<string>,
): boolean => {
if (node === null) {
return false
}

const unwrapped = unwrap(node)

if (unwrapped.type === AST_NODE_TYPES.Identifier) {
return isQueryResultIdentifier(unwrapped, seen)
}

if (unwrapped.type !== AST_NODE_TYPES.CallExpression) {
return false
}

if (unwrapped.callee.type !== AST_NODE_TYPES.Identifier) {
return false
}

if (isTanstackQueryHook(unwrapped.callee)) {
return true
}

return isQueryResultIdentifier(unwrapped.callee, seen)
}

const isQueryResultIdentifier = (
node: TSESTree.Identifier,
seen: Set<string>,
): boolean => {
if (isTanstackQueryHook(node)) {
return true
}

if (seen.has(node.name)) {
return false
}

seen.add(node.name)

const referenced = getReferencedNode(node)
if (referenced === null || referenced === node) {
return false
}

const unwrapped = unwrap(referenced)

if (
unwrapped.type === AST_NODE_TYPES.FunctionDeclaration ||
unwrapped.type === AST_NODE_TYPES.FunctionExpression ||
unwrapped.type === AST_NODE_TYPES.ArrowFunctionExpression
) {
const returned = getDirectReturnExpression(unwrapped)
return isQueryResultNode(returned, seen)
}

return isQueryResultNode(unwrapped, seen)
}

const isQueryResultHookCall = (node: TSESTree.CallExpression): boolean => {
return isQueryResultNode(node, new Set<string>())
}

return {
VariableDeclarator: (node) => {
if (
node.init?.type === AST_NODE_TYPES.Identifier &&
queryResultVariables.has(node.init.name) &&
NoRestDestructuringUtils.isObjectRestDestructuring(node.id)
) {
context.report({
node,
messageId: 'objectRestDestructure',
})
}
},

CallExpression: (node) => {
if (
!ASTUtils.isIdentifierWithOneOfNames(node.callee, queryHooks) ||
node.parent.type !== AST_NODE_TYPES.VariableDeclarator ||
!helpers.isTanstackQueryImport(node.callee)
node.callee.type !== AST_NODE_TYPES.Identifier ||
!isQueryResultHookCall(node)
) {
return
}

if (node.parent.type !== AST_NODE_TYPES.VariableDeclarator) {
return
}

const returnValue = node.parent.id

if (
Expand Down Expand Up @@ -77,6 +238,7 @@ export const rule = createRule({
if (queryResult === null) {
return
}

if (NoRestDestructuringUtils.isObjectRestDestructuring(queryResult)) {
context.report({
node: queryResult,
Expand All @@ -86,19 +248,6 @@ export const rule = createRule({
})
},

VariableDeclarator: (node) => {
if (
node.init?.type === AST_NODE_TYPES.Identifier &&
queryResultVariables.has(node.init.name) &&
NoRestDestructuringUtils.isObjectRestDestructuring(node.id)
) {
context.report({
node,
messageId: 'objectRestDestructure',
})
}
},

SpreadElement: (node) => {
if (
node.argument.type === AST_NODE_TYPES.Identifier &&
Expand Down