Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,10 @@
"label": "Exhaustive Deps",
"to": "react/eslint/exhaustive-deps"
},
{
"label": "No deprecated options",
"to": "react/eslint/no-deprecated-options"
},
{
"label": "Prefer object syntax",
"to": "react/eslint/prefer-query-object-syntax"
Expand Down
1 change: 1 addition & 0 deletions docs/react/eslint/eslint-plugin-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Then configure the rules you want to use under the rules section:
{
"rules": {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-deprecated-options": "error",
"@tanstack/query/prefer-query-object-syntax": "error",
"@tanstack/query/stable-query-client": "error"
}
Expand Down
65 changes: 65 additions & 0 deletions docs/react/eslint/no-deprecated-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
id: no-deprecated-options
title: Disallowing deprecated options
---

This rule warns about deprecated [`useQuery`](https://tanstack.com/query/v4/docs/reference/useQuery) options which will be removed in [TanStack Query v5](https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5).

## Rule Details

Examples of **incorrect** code for this rule:

```tsx
/* eslint "@tanstack/query/no-deprecated-options": "error" */

useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
onSuccess: () => {},
})

useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
onError: () => {},
})

useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
onSettled: () => {},
})

useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
isDataEqual: (oldData, newData) => customCheck(oldData, newData),
})
```

Examples of **correct** code for this rule:

```tsx
useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
})

useQuery({
queryKey: ['todo', todoId],
queryFn: () => api.getTodo(todoId),
structuralSharing: (oldData, newData) =>
customCheck(oldData, newData)
? oldData
: replaceEqualDeep(oldData, newData),
})
```

## When Not To Use It

If you don't plan to upgrade to TanStack Query v5, then you will not need this rule.

## Attributes

- [x] ✅ Recommended
- [ ] 🔧 Fixable
1 change: 1 addition & 0 deletions packages/eslint-plugin-query/src/configs/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('configs', () => {
],
"rules": {
"@tanstack/query/exhaustive-deps": "error",
"@tanstack/query/no-deprecated-options": "error",
"@tanstack/query/prefer-query-object-syntax": "error",
"@tanstack/query/stable-query-client": "error",
},
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin-query/src/rules/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import * as exhaustiveDeps from './exhaustive-deps/exhaustive-deps.rule'
import * as noDeprecatedOptions from './no-deprecated-options/no-deprecated-options.rule'
import * as preferObjectSyntax from './prefer-query-object-syntax/prefer-query-object-syntax'
import * as stableQueryClient from './stable-query-client/stable-query-client.rule'

export const rules = {
[exhaustiveDeps.name]: exhaustiveDeps.rule,
[noDeprecatedOptions.name]: noDeprecatedOptions.rule,
[preferObjectSyntax.name]: preferObjectSyntax.rule,
[stableQueryClient.name]: stableQueryClient.rule,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import { createRule } from '../../utils/create-rule'
import { ASTUtils } from '../../utils/ast-utils'
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'

const ON_SUCCESS = 'onSuccess'
const ON_ERROR = 'onError'
const ON_SETTLED = 'onSettled'
const IS_DATA_EQUAL = 'isDataEqual'

const DEPRECATED_OPTIONS = [
ON_SUCCESS,
ON_ERROR,
ON_SETTLED,
IS_DATA_EQUAL,
] as const

const QUERY_CALLS = ['useQuery' as const]

const messages = {
noDeprecatedOptions: `Option \`{{option}}\` will be removed in the next major version`,
}

type MessageKey = keyof typeof messages

export const name = 'no-deprecated-options'

export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
createRule({
name,
meta: {
type: 'problem',
docs: {
description: 'Disallows deprecated options',
recommended: 'error',
},
messages: messages,
schema: [],
},
defaultOptions: [],

create(context, _, helpers) {
return {
CallExpression(node) {
if (
!ASTUtils.isIdentifierWithOneOfNames(node.callee, QUERY_CALLS) ||
!helpers.isTanstackQueryImport(node.callee)
) {
return
}

const firstArgument = node.arguments[0]

if (!firstArgument) {
return
}

if (
node.arguments.length === 1 &&
firstArgument.type === AST_NODE_TYPES.CallExpression
) {
const referencedCallExpression =
ASTUtils.getReferencedExpressionByIdentifier({
context,
node: firstArgument.callee,
})

if (
referencedCallExpression === null ||
!ASTUtils.isNodeOfOneOf(referencedCallExpression, [
AST_NODE_TYPES.ArrowFunctionExpression,
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.FunctionExpression,
])
) {
return
}

if (
referencedCallExpression.type ===
AST_NODE_TYPES.ArrowFunctionExpression &&
referencedCallExpression.expression
) {
return runCheckOnNode({
context: context,
callNode: node,
expression: referencedCallExpression.body,
messageId: 'noDeprecatedOptions',
})
}

const returnStmts = ASTUtils.getNestedReturnStatements(
referencedCallExpression,
)

for (const stmt of returnStmts) {
if (stmt.argument === null) {
return
}

runCheckOnNode({
context: context,
callNode: node,
expression: stmt.argument,
messageId: 'noDeprecatedOptions',
})
}

return
}

if (firstArgument.type === AST_NODE_TYPES.Identifier) {
const referencedNode = ASTUtils.getReferencedExpressionByIdentifier(
{
context,
node: firstArgument,
},
)

if (referencedNode?.type === AST_NODE_TYPES.ObjectExpression) {
return runCheckOnNode({
context: context,
callNode: node,
expression: referencedNode,
messageId: 'noDeprecatedOptions',
})
}
}

runCheckOnNode({
context: context,
callNode: node,
expression: firstArgument,
messageId: 'noDeprecatedOptions',
})
},
}
},
})

function runCheckOnNode(params: {
context: Readonly<TSESLint.RuleContext<MessageKey, readonly unknown[]>>
callNode: TSESTree.CallExpression
expression: TSESTree.Node
messageId: MessageKey
}) {
const { context, expression, messageId, callNode } = params

const nodes = new Set([expression, ...callNode.arguments])

for (const node of nodes) {
for (const option of DEPRECATED_OPTIONS) {
if (node.type !== AST_NODE_TYPES.ObjectExpression) {
continue
}

const property = ASTUtils.findPropertyWithIdentifierKey(
node.properties,
option,
)
if (property) {
context.report({ node: property, messageId, data: { option } })
}
}
}
}
Loading