Skip to content

Commit d4fa2b9

Browse files
authored
feat(eslint-plugin): prefer-object-syntax support useMutation and createMutation (#4893)
* support useMutation and createMutation * fix tests * format
1 parent 9c15b32 commit d4fa2b9

File tree

4 files changed

+101
-13
lines changed

4 files changed

+101
-13
lines changed

packages/eslint-plugin-query/src/rules/prefer-query-object-syntax/prefer-query-object-syntax.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ ruleTester.run(name, rule, {
3535
{
3636
code: normalizeIndent`
3737
import { createQuery } from "@tanstack/solid-query";
38-
const result = useQuery({ queryKey, queryFn, enabled });
38+
const result = createQuery({ queryKey, queryFn, enabled });
3939
`,
4040
},
4141
{
@@ -94,6 +94,49 @@ ruleTester.run(name, rule, {
9494
useQuery(getQuery())
9595
`,
9696
},
97+
{
98+
code: normalizeIndent`
99+
useMutation()
100+
`,
101+
},
102+
{
103+
code: normalizeIndent`
104+
import { useMutation } from "@tanstack/react-query";
105+
useMutation();
106+
`,
107+
},
108+
{
109+
code: normalizeIndent`
110+
import { useMutation } from "@tanstack/react-query";
111+
useMutation({ mutationKey, mutationFn, enabled });
112+
`,
113+
},
114+
{
115+
code: normalizeIndent`
116+
import { useMutation } from "@tanstack/react-query";
117+
const result = useMutation({ mutationKey, mutationFn, enabled });
118+
`,
119+
},
120+
{
121+
code: normalizeIndent`
122+
import { createMutation } from "@tanstack/solid-query";
123+
const result = createMutation({ mutationKey, mutationFn, enabled });
124+
`,
125+
},
126+
{
127+
code: normalizeIndent`
128+
import { useMutation } from "somewhere-else";
129+
useMutation(mutationKey, mutationFn, { enabled });
130+
`,
131+
},
132+
{
133+
code: normalizeIndent`
134+
import { useMutation } from "@tanstack/react-query";
135+
const getPosts = async () => Promise.resolve([]);
136+
const postsQuery = { mutationKey: ["posts"], mutationFn: () => getPosts() };
137+
const usePosts = () => useMutation(postsQuery);
138+
`,
139+
},
97140
],
98141

99142
invalid: [
@@ -359,5 +402,27 @@ ruleTester.run(name, rule, {
359402
createQuery({ queryKey: queryKeys.userById(userId), queryFn: async () => await fetchUserById(userId) });
360403
`,
361404
},
405+
{
406+
code: normalizeIndent`
407+
import { useMutation } from "@tanstack/react-query";
408+
useMutation(["mutation", "key"], async () => await fetchUserById(userId));
409+
`,
410+
errors: [{ messageId: 'preferObjectSyntax' }],
411+
output: normalizeIndent`
412+
import { useMutation } from "@tanstack/react-query";
413+
useMutation({ mutationKey: ["mutation", "key"], mutationFn: async () => await fetchUserById(userId) });
414+
`,
415+
},
416+
{
417+
code: normalizeIndent`
418+
import { createMutation } from "@tanstack/solid-query";
419+
createMutation(["mutation", "key"], async () => await fetchUserById(userId));
420+
`,
421+
errors: [{ messageId: 'preferObjectSyntax' }],
422+
output: normalizeIndent`
423+
import { createMutation } from "@tanstack/solid-query";
424+
createMutation({ mutationKey: ["mutation", "key"], mutationFn: async () => await fetchUserById(userId) });
425+
`,
426+
},
362427
],
363428
})

packages/eslint-plugin-query/src/rules/prefer-query-object-syntax/prefer-query-object-syntax.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
22
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
33
import { createRule } from '../../utils/create-rule'
44
import { ASTUtils } from '../../utils/ast-utils'
5+
import { objectKeys } from '../../utils/object-utils'
56

6-
const QUERY_CALLS = ['useQuery', 'createQuery']
7+
const QUERY_CALLS = {
8+
useQuery: { key: 'queryKey', fn: 'queryFn' },
9+
createQuery: { key: 'queryKey', fn: 'queryFn' },
10+
useMutation: { key: 'mutationKey', fn: 'mutationFn' },
11+
createMutation: { key: 'mutationKey', fn: 'mutationFn' },
12+
}
713

814
const messages = {
915
preferObjectSyntax: `Objects syntax for query is preferred`,
@@ -32,11 +38,13 @@ export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
3238
create(context, _, helpers) {
3339
return {
3440
CallExpression(node) {
35-
const isTanstackQueryCall =
36-
ASTUtils.isIdentifierWithOneOfNames(node.callee, QUERY_CALLS) &&
37-
helpers.isTanstackQueryImport(node.callee)
38-
39-
if (!isTanstackQueryCall) {
41+
if (
42+
!ASTUtils.isIdentifierWithOneOfNames(
43+
node.callee,
44+
objectKeys(QUERY_CALLS),
45+
) ||
46+
!helpers.isTanstackQueryImport(node.callee)
47+
) {
4048
return
4149
}
4250

@@ -101,6 +109,7 @@ export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
101109
runCheckOnNode({
102110
context: context,
103111
callNode: node,
112+
callString: node.callee.name,
104113
expression: stmt.argument,
105114
messageId: 'returnTypeAreNotObjectSyntax',
106115
})
@@ -121,6 +130,7 @@ export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
121130
return runCheckOnNode({
122131
context: context,
123132
callNode: node,
133+
callString: node.callee.name,
124134
expression: referencedNode,
125135
messageId: 'preferObjectSyntax',
126136
})
@@ -130,6 +140,7 @@ export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
130140
runCheckOnNode({
131141
context: context,
132142
callNode: node,
143+
callString: node.callee.name,
133144
expression: firstArgument,
134145
messageId: 'preferObjectSyntax',
135146
})
@@ -141,10 +152,11 @@ export const rule: TSESLint.RuleModule<MessageKey, readonly unknown[]> =
141152
function runCheckOnNode(params: {
142153
context: Readonly<TSESLint.RuleContext<MessageKey, readonly unknown[]>>
143154
callNode: TSESTree.CallExpression
155+
callString: keyof typeof QUERY_CALLS
144156
expression: TSESTree.Node
145157
messageId: MessageKey
146158
}) {
147-
const { context, expression, messageId, callNode } = params
159+
const { context, expression, messageId, callNode, callString } = params
148160
const sourceCode = context.getSourceCode()
149161

150162
if (expression.type === AST_NODE_TYPES.ObjectExpression) {
@@ -184,6 +196,8 @@ function runCheckOnNode(params: {
184196
return
185197
}
186198

199+
const callProps = QUERY_CALLS[callString]
200+
187201
context.report({
188202
node: callNode,
189203
messageId: 'preferObjectSyntax',
@@ -194,15 +208,19 @@ function runCheckOnNode(params: {
194208
const firstArgument = callNode.arguments[0]
195209
const queryKey = sourceCode.getText(firstArgument)
196210
const queryKeyProperty =
197-
queryKey === 'queryKey' ? 'queryKey' : `queryKey: ${queryKey}`
211+
queryKey === callProps.key
212+
? callProps.key
213+
: `${callProps.key}: ${queryKey}`
198214

199215
optionsObjectProperties.push(queryKeyProperty)
200216

201217
// queryFn
202218
if (secondArgument && secondArgument !== optionsObject) {
203219
const queryFn = sourceCode.getText(secondArgument)
204220
const queryFnProperty =
205-
queryFn === 'queryFn' ? 'queryFn' : `queryFn: ${queryFn}`
221+
queryFn === callProps.fn
222+
? callProps.fn
223+
: `${callProps.fn}: ${queryFn}`
206224

207225
optionsObjectProperties.push(queryFnProperty)
208226
}

packages/eslint-plugin-query/src/utils/ast-utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ export const ASTUtils = {
1919
): node is TSESTree.Identifier {
2020
return ASTUtils.isIdentifier(node) && node.name === name
2121
},
22-
isIdentifierWithOneOfNames(
22+
isIdentifierWithOneOfNames<T extends string[]>(
2323
node: TSESTree.Node,
24-
name: string[],
25-
): node is TSESTree.Identifier {
24+
name: T,
25+
): node is TSESTree.Identifier & { name: T[number] } {
2626
return ASTUtils.isIdentifier(node) && name.includes(node.name)
2727
},
2828
isProperty(node: TSESTree.Node): node is TSESTree.Property {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function objectKeys<T extends Record<string, unknown>>(
2+
obj: T,
3+
): Array<keyof T> {
4+
return Object.keys(obj) as Array<keyof T>
5+
}

0 commit comments

Comments
 (0)