Skip to content

Commit a1210cb

Browse files
jochongsTkDodo
andauthored
feat(eslint-plugin): refactor and add mutation-property-order rule (#9191)
Co-authored-by: Dominik Dorfmeister <[email protected]>
1 parent f2aa150 commit a1210cb

File tree

10 files changed

+396
-102
lines changed

10 files changed

+396
-102
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { RuleTester } from '@typescript-eslint/rule-tester'
2+
import combinate from 'combinate'
3+
4+
import {
5+
checkedProperties,
6+
mutationFunctions,
7+
} from '../rules/mutation-property-order/constants'
8+
import {
9+
name,
10+
rule,
11+
} from '../rules/mutation-property-order/mutation-property-order.rule'
12+
import {
13+
generateInterleavedCombinations,
14+
generatePartialCombinations,
15+
generatePermutations,
16+
normalizeIndent,
17+
} from './test-utils'
18+
import type { MutationFunctions } from '../rules/mutation-property-order/constants'
19+
20+
const ruleTester = new RuleTester()
21+
22+
type CheckedProperties = (typeof checkedProperties)[number]
23+
const orderIndependentProps = [
24+
'gcTime',
25+
'...objectExpressionSpread',
26+
'...callExpressionSpread',
27+
'...memberCallExpressionSpread',
28+
] as const
29+
type OrderIndependentProps = (typeof orderIndependentProps)[number]
30+
31+
interface TestCase {
32+
mutationFunction: MutationFunctions
33+
properties: Array<CheckedProperties | OrderIndependentProps>
34+
}
35+
36+
const validTestMatrix = combinate({
37+
mutationFunction: [...mutationFunctions],
38+
properties: generatePartialCombinations(checkedProperties, 2),
39+
})
40+
41+
export function generateInvalidPermutations(
42+
arr: ReadonlyArray<CheckedProperties>,
43+
): Array<{
44+
invalid: Array<CheckedProperties>
45+
valid: Array<CheckedProperties>
46+
}> {
47+
const combinations = generatePartialCombinations(arr, 2)
48+
const allPermutations: Array<{
49+
invalid: Array<CheckedProperties>
50+
valid: Array<CheckedProperties>
51+
}> = []
52+
53+
for (const combination of combinations) {
54+
const permutations = generatePermutations(combination)
55+
// skip the first permutation as it matches the original combination
56+
const invalidPermutations = permutations.slice(1)
57+
58+
if (combination.includes('onError') && combination.includes('onSettled')) {
59+
if (combination.indexOf('onError') < combination.indexOf('onSettled')) {
60+
// since we ignore the relative order of 'onError' and 'onSettled', we skip this combination (but keep the other one where `onSettled` is before `onError`)
61+
62+
continue
63+
}
64+
}
65+
66+
allPermutations.push(
67+
...invalidPermutations
68+
.map((p) => {
69+
// ignore the relative order of 'onError' and 'onSettled'
70+
const correctedValid = [...combination].sort((a, b) => {
71+
if (
72+
(a === 'onSettled' && b === 'onError') ||
73+
(a === 'onError' && b === 'onSettled')
74+
) {
75+
return p.indexOf(a) - p.indexOf(b)
76+
}
77+
return checkedProperties.indexOf(a) - checkedProperties.indexOf(b)
78+
})
79+
return { invalid: p, valid: correctedValid }
80+
})
81+
.filter(
82+
({ invalid }) =>
83+
// if `onError` and `onSettled` are next to each other and `onMutate` is not present, we skip this invalid permutation
84+
Math.abs(
85+
invalid.indexOf('onSettled') - invalid.indexOf('onError'),
86+
) !== 1,
87+
),
88+
)
89+
}
90+
91+
return allPermutations
92+
}
93+
94+
const invalidPermutations = generateInvalidPermutations(checkedProperties)
95+
96+
type Interleaved = CheckedProperties | OrderIndependentProps
97+
const interleavedInvalidPermutations: Array<{
98+
invalid: Array<Interleaved>
99+
valid: Array<Interleaved>
100+
}> = []
101+
for (const invalidPermutation of invalidPermutations) {
102+
const invalid = generateInterleavedCombinations(
103+
invalidPermutation.invalid,
104+
orderIndependentProps,
105+
)
106+
const valid = generateInterleavedCombinations(
107+
invalidPermutation.valid,
108+
orderIndependentProps,
109+
)
110+
111+
for (let i = 0; i < invalid.length; i++) {
112+
interleavedInvalidPermutations.push({
113+
invalid: invalid[i]!,
114+
valid: valid[i]!,
115+
})
116+
}
117+
}
118+
119+
const invalidTestMatrix = combinate({
120+
mutationFunction: [...mutationFunctions],
121+
properties: interleavedInvalidPermutations,
122+
})
123+
124+
const callExpressionSpread = normalizeIndent`
125+
...mutationOptions({
126+
onSuccess: () => {},
127+
retry: 3,
128+
})`
129+
130+
function getCode({ mutationFunction: mutationFunction, properties }: TestCase) {
131+
function getPropertyCode(
132+
property: CheckedProperties | OrderIndependentProps,
133+
) {
134+
switch (property) {
135+
case '...objectExpressionSpread':
136+
return `...objectExpressionSpread`
137+
case '...callExpressionSpread':
138+
return callExpressionSpread
139+
case '...memberCallExpressionSpread':
140+
return '...myOptions.mutationOptions()'
141+
case 'gcTime':
142+
return 'gcTime: 5 * 60 * 1000'
143+
case 'onMutate':
144+
return 'onMutate: (data) => {\n return { foo: data }\n}'
145+
case 'onError':
146+
return 'onError: (error, variables, context) => {\n console.log("error:", error, "context:", context)\n}'
147+
case 'onSettled':
148+
return 'onSettled: (data, error, variables, context) => {\n console.log("settled", context)\n}'
149+
}
150+
}
151+
return `
152+
import { ${mutationFunction} } from '@tanstack/react-query'
153+
154+
${mutationFunction}({
155+
${properties.map(getPropertyCode).join(',\n ')}
156+
})
157+
`
158+
}
159+
160+
const validTestCases = validTestMatrix.map(
161+
({ mutationFunction, properties }) => ({
162+
name: `should pass when order is correct for ${mutationFunction} with order: ${properties.join(', ')}`,
163+
code: getCode({
164+
mutationFunction: mutationFunction,
165+
properties,
166+
}),
167+
}),
168+
)
169+
170+
const invalidTestCases = invalidTestMatrix.map(
171+
({ mutationFunction, properties }) => ({
172+
name: `incorrect property order id detected for ${mutationFunction} with invalid order: ${properties.invalid.join(', ')}, valid order ${properties.valid.join(', ')}`,
173+
code: getCode({
174+
mutationFunction: mutationFunction,
175+
properties: properties.invalid,
176+
}),
177+
errors: [{ messageId: 'invalidOrder' }],
178+
output: getCode({
179+
mutationFunction: mutationFunction,
180+
properties: properties.valid,
181+
}),
182+
}),
183+
)
184+
185+
ruleTester.run(name, rule, {
186+
valid: validTestCases,
187+
invalid: invalidTestCases,
188+
})
189+
190+
const regressionTestCases = {
191+
valid: [
192+
{
193+
name: 'should pass with call expression spread in useMutation',
194+
code: normalizeIndent`
195+
import { useMutation } from '@tanstack/react-query'
196+
197+
const { mutate } = useMutation({
198+
...mutationOptions({
199+
retry: 3,
200+
onSuccess: () => console.log('success'),
201+
}),
202+
onMutate: (data) => {
203+
return { foo: data }
204+
},
205+
onError: (error, variables, context) => {
206+
console.log(error, context)
207+
},
208+
onSettled: (data, error, variables, context) => {
209+
console.log('settled', context)
210+
},
211+
})
212+
`,
213+
},
214+
],
215+
invalid: [],
216+
}
217+
218+
ruleTester.run(name, rule, regressionTestCases)

packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts renamed to packages/eslint-plugin-query/src/__tests__/sort-data-by-order.utils.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, test } from 'vitest'
2-
import { sortDataByOrder } from '../rules/infinite-query-property-order/infinite-query-property-order.utils'
2+
import { sortDataByOrder } from '../utils/sort-data-by-order'
33

44
describe('create-route-property-order utils', () => {
55
describe('sortDataByOrder', () => {

packages/eslint-plugin-query/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Object.assign(plugin.configs, {
3131
'@tanstack/query/no-unstable-deps': 'error',
3232
'@tanstack/query/infinite-query-property-order': 'error',
3333
'@tanstack/query/no-void-query-fn': 'error',
34+
'@tanstack/query/mutation-property-order': 'error',
3435
},
3536
},
3637
'flat/recommended': [
@@ -46,6 +47,7 @@ Object.assign(plugin.configs, {
4647
'@tanstack/query/no-unstable-deps': 'error',
4748
'@tanstack/query/infinite-query-property-order': 'error',
4849
'@tanstack/query/no-void-query-fn': 'error',
50+
'@tanstack/query/mutation-property-order': 'error',
4951
},
5052
},
5153
],

packages/eslint-plugin-query/src/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-dest
44
import * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule'
55
import * as infiniteQueryPropertyOrder from './rules/infinite-query-property-order/infinite-query-property-order.rule'
66
import * as noVoidQueryFn from './rules/no-void-query-fn/no-void-query-fn.rule'
7+
import * as mutationPropertyOrder from './rules/mutation-property-order/mutation-property-order.rule'
78
import type { ESLintUtils } from '@typescript-eslint/utils'
89
import type { ExtraRuleDocs } from './types'
910

@@ -22,4 +23,5 @@ export const rules: Record<
2223
[noUnstableDeps.name]: noUnstableDeps.rule,
2324
[infiniteQueryPropertyOrder.name]: infiniteQueryPropertyOrder.rule,
2425
[noVoidQueryFn.name]: noVoidQueryFn.rule,
26+
[mutationPropertyOrder.name]: mutationPropertyOrder.rule,
2527
}

packages/eslint-plugin-query/src/rules/infinite-query-property-order/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export const checkedProperties = [
1212
'getNextPageParam',
1313
] as const
1414

15+
export type InfiniteQueryProperties = (typeof checkedProperties)[number]
16+
1517
export const sortRules = [
1618
[['queryFn'], ['getPreviousPageParam', 'getNextPageParam']],
1719
] as const

0 commit comments

Comments
 (0)