@tanstack/eslint-plugin-query 5.56.1 → 5.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/index.cjs +4 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/rules/infinite-query-property-order/constants.cjs +13 -0
- package/dist/cjs/rules/infinite-query-property-order/constants.cjs.map +1 -0
- package/dist/cjs/rules/infinite-query-property-order/constants.d.cts +4 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.cjs +95 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.cjs.map +1 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.d.cts +4 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.cjs +47 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.cjs.map +1 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.d.cts +1 -0
- package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.cjs +1 -1
- package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.cjs.map +1 -1
- package/dist/cjs/rules.cjs +3 -1
- package/dist/cjs/rules.cjs.map +1 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rules/infinite-query-property-order/constants.d.ts +4 -0
- package/dist/esm/rules/infinite-query-property-order/constants.js +13 -0
- package/dist/esm/rules/infinite-query-property-order/constants.js.map +1 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.d.ts +4 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.js +95 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.js.map +1 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.d.ts +1 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.js +47 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.js.map +1 -0
- package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js +1 -1
- package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js.map +1 -1
- package/dist/esm/rules.js +3 -1
- package/dist/esm/rules.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/infinite-query-property-order.rule.test.ts +182 -0
- package/src/__tests__/infinite-query-property-order.utils.test.ts +79 -0
- package/src/__tests__/test-utils.test.ts +104 -0
- package/src/__tests__/test-utils.ts +103 -0
- package/src/index.ts +2 -0
- package/src/rules/infinite-query-property-order/constants.ts +17 -0
- package/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts +112 -0
- package/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts +67 -0
- package/src/rules/no-unstable-deps/no-unstable-deps.rule.ts +1 -1
- package/src/rules.ts +2 -0
package/dist/esm/rules.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.js","sources":["../../src/rules.ts"],"sourcesContent":["import * as exhaustiveDeps from './rules/exhaustive-deps/exhaustive-deps.rule'\nimport * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'\nimport * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-destructuring.rule'\nimport * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule'\nimport type { ESLintUtils } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from './types'\n\nexport const rules: Record<\n string,\n ESLintUtils.RuleModule<\n string,\n ReadonlyArray<unknown>,\n ExtraRuleDocs,\n ESLintUtils.RuleListener\n >\n> = {\n [exhaustiveDeps.name]: exhaustiveDeps.rule,\n [stableQueryClient.name]: stableQueryClient.rule,\n [noRestDestructuring.name]: noRestDestructuring.rule,\n [noUnstableDeps.name]: noUnstableDeps.rule,\n}\n"],"names":["exhaustiveDeps.name","exhaustiveDeps.rule","stableQueryClient.name","stableQueryClient.rule","noRestDestructuring.name","noRestDestructuring.rule","noUnstableDeps.name","noUnstableDeps.rule"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.js","sources":["../../src/rules.ts"],"sourcesContent":["import * as exhaustiveDeps from './rules/exhaustive-deps/exhaustive-deps.rule'\nimport * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'\nimport * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-destructuring.rule'\nimport * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule'\nimport * as infiniteQueryPropertyOrder from './rules/infinite-query-property-order/infinite-query-property-order.rule'\nimport type { ESLintUtils } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from './types'\n\nexport const rules: Record<\n string,\n ESLintUtils.RuleModule<\n string,\n ReadonlyArray<unknown>,\n ExtraRuleDocs,\n ESLintUtils.RuleListener\n >\n> = {\n [exhaustiveDeps.name]: exhaustiveDeps.rule,\n [stableQueryClient.name]: stableQueryClient.rule,\n [noRestDestructuring.name]: noRestDestructuring.rule,\n [noUnstableDeps.name]: noUnstableDeps.rule,\n [infiniteQueryPropertyOrder.name]: infiniteQueryPropertyOrder.rule,\n}\n"],"names":["exhaustiveDeps.name","exhaustiveDeps.rule","stableQueryClient.name","stableQueryClient.rule","noRestDestructuring.name","noRestDestructuring.rule","noUnstableDeps.name","noUnstableDeps.rule","infiniteQueryPropertyOrder.name","infiniteQueryPropertyOrder.rule"],"mappings":";;;;;AAQO,MAAM,QAQT;AAAA,EACF,CAACA,IAAmB,GAAGC;AAAAA,EACvB,CAACC,MAAsB,GAAGC;AAAAA,EAC1B,CAACC,MAAwB,GAAGC;AAAAA,EAC5B,CAACC,MAAmB,GAAGC;AAAAA,EACvB,CAACC,MAA+B,GAAGC;AACrC;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/eslint-plugin-query",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.57.1",
|
|
4
4
|
"description": "ESLint plugin for TanStack Query",
|
|
5
5
|
"author": "Eliya Cohen",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@typescript-eslint/rule-tester": "^8.3.0",
|
|
44
|
+
"combinate": "^1.1.11",
|
|
44
45
|
"eslint": "^9.9.1"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester'
|
|
2
|
+
import combinate from 'combinate'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
checkedProperties,
|
|
6
|
+
infiniteQueryFunctions,
|
|
7
|
+
} from '../rules/infinite-query-property-order/constants'
|
|
8
|
+
import {
|
|
9
|
+
name,
|
|
10
|
+
rule,
|
|
11
|
+
} from '../rules/infinite-query-property-order/infinite-query-property-order.rule'
|
|
12
|
+
import {
|
|
13
|
+
generateInterleavedCombinations,
|
|
14
|
+
generatePartialCombinations,
|
|
15
|
+
generatePermutations,
|
|
16
|
+
} from './test-utils'
|
|
17
|
+
import type { InfiniteQueryFunctions } from '../rules/infinite-query-property-order/constants'
|
|
18
|
+
|
|
19
|
+
const ruleTester = new RuleTester()
|
|
20
|
+
|
|
21
|
+
type CheckedProperties = (typeof checkedProperties)[number]
|
|
22
|
+
const orderIndependentProps = ['queryKey', '...foo'] as const
|
|
23
|
+
type OrderIndependentProps = (typeof orderIndependentProps)[number]
|
|
24
|
+
|
|
25
|
+
interface TestCase {
|
|
26
|
+
infiniteQueryFunction: InfiniteQueryFunctions
|
|
27
|
+
properties: Array<CheckedProperties | OrderIndependentProps>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const validTestMatrix = combinate({
|
|
31
|
+
infiniteQueryFunction: [...infiniteQueryFunctions],
|
|
32
|
+
properties: generatePartialCombinations(checkedProperties, 2),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export function generateInvalidPermutations(
|
|
36
|
+
arr: ReadonlyArray<CheckedProperties>,
|
|
37
|
+
): Array<{
|
|
38
|
+
invalid: Array<CheckedProperties>
|
|
39
|
+
valid: Array<CheckedProperties>
|
|
40
|
+
}> {
|
|
41
|
+
const combinations = generatePartialCombinations(arr, 2)
|
|
42
|
+
const allPermutations: Array<{
|
|
43
|
+
invalid: Array<CheckedProperties>
|
|
44
|
+
valid: Array<CheckedProperties>
|
|
45
|
+
}> = []
|
|
46
|
+
|
|
47
|
+
for (const combination of combinations) {
|
|
48
|
+
const permutations = generatePermutations(combination)
|
|
49
|
+
// skip the first permutation as it matches the original combination
|
|
50
|
+
const invalidPermutations = permutations.slice(1)
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
combination.includes('getNextPageParam') &&
|
|
54
|
+
combination.includes('getPreviousPageParam')
|
|
55
|
+
) {
|
|
56
|
+
if (
|
|
57
|
+
combination.indexOf('getNextPageParam') <
|
|
58
|
+
combination.indexOf('getPreviousPageParam')
|
|
59
|
+
) {
|
|
60
|
+
// since we ignore the relative order of 'getPreviousPageParam' and 'getNextPageParam', we skip this combination (but keep the other one where `getPreviousPageParam` is before `getNextPageParam`)
|
|
61
|
+
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
allPermutations.push(
|
|
67
|
+
...invalidPermutations
|
|
68
|
+
.map((p) => {
|
|
69
|
+
// ignore the relative order of 'getPreviousPageParam' and 'getNextPageParam'
|
|
70
|
+
const correctedValid = [...combination].sort((a, b) => {
|
|
71
|
+
if (
|
|
72
|
+
(a === 'getNextPageParam' && b === 'getPreviousPageParam') ||
|
|
73
|
+
(a === 'getPreviousPageParam' && b === 'getNextPageParam')
|
|
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 `getPreviousPageParam` and `getNextPageParam` are next to each other and `queryFn` is not present, we skip this invalid permutation
|
|
84
|
+
Math.abs(
|
|
85
|
+
invalid.indexOf('getNextPageParam') -
|
|
86
|
+
invalid.indexOf('getPreviousPageParam'),
|
|
87
|
+
) !== 1 && !invalid.includes('queryFn'),
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return allPermutations
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const invalidPermutations = generateInvalidPermutations(checkedProperties)
|
|
96
|
+
|
|
97
|
+
type Interleaved = CheckedProperties | OrderIndependentProps
|
|
98
|
+
const interleavedInvalidPermutations: Array<{
|
|
99
|
+
invalid: Array<Interleaved>
|
|
100
|
+
valid: Array<Interleaved>
|
|
101
|
+
}> = []
|
|
102
|
+
for (const invalidPermutation of invalidPermutations) {
|
|
103
|
+
const invalid = generateInterleavedCombinations(
|
|
104
|
+
invalidPermutation.invalid,
|
|
105
|
+
orderIndependentProps,
|
|
106
|
+
)
|
|
107
|
+
const valid = generateInterleavedCombinations(
|
|
108
|
+
invalidPermutation.valid,
|
|
109
|
+
orderIndependentProps,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < invalid.length; i++) {
|
|
113
|
+
interleavedInvalidPermutations.push({
|
|
114
|
+
invalid: invalid[i]!,
|
|
115
|
+
valid: valid[i]!,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const invalidTestMatrix = combinate({
|
|
121
|
+
infiniteQueryFunction: [...infiniteQueryFunctions],
|
|
122
|
+
properties: interleavedInvalidPermutations,
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
function getCode({
|
|
126
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
127
|
+
properties,
|
|
128
|
+
}: TestCase) {
|
|
129
|
+
function getPropertyCode(
|
|
130
|
+
property: CheckedProperties | OrderIndependentProps,
|
|
131
|
+
) {
|
|
132
|
+
if (property.startsWith('...')) {
|
|
133
|
+
return property
|
|
134
|
+
}
|
|
135
|
+
switch (property) {
|
|
136
|
+
case 'queryKey':
|
|
137
|
+
return `queryKey: ['projects']`
|
|
138
|
+
case 'queryFn':
|
|
139
|
+
return 'queryFn: async ({ pageParam }) => { \n await fetch(`/api/projects?cursor=${pageParam}`) \n return await response.json() \n }'
|
|
140
|
+
case 'getPreviousPageParam':
|
|
141
|
+
return 'getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined'
|
|
142
|
+
case 'getNextPageParam':
|
|
143
|
+
return 'getNextPageParam: (lastPage) => lastPage.nextId ?? undefined'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return `${property}: () => null`
|
|
147
|
+
}
|
|
148
|
+
return `
|
|
149
|
+
import { ${infiniteQueryFunction} } from '@tanstack/react-query'
|
|
150
|
+
|
|
151
|
+
${infiniteQueryFunction}({
|
|
152
|
+
${properties.map(getPropertyCode).join(',\n ')}
|
|
153
|
+
})
|
|
154
|
+
`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const validTestCases = validTestMatrix.map(
|
|
158
|
+
({ infiniteQueryFunction, properties }) => ({
|
|
159
|
+
name: `should pass when order is correct for ${infiniteQueryFunction} with order: ${properties.join(', ')}`,
|
|
160
|
+
code: getCode({ infiniteQueryFunction, properties }),
|
|
161
|
+
}),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const invalidTestCases = invalidTestMatrix.map(
|
|
165
|
+
({ infiniteQueryFunction, properties }) => ({
|
|
166
|
+
name: `incorrect property order is detected for ${infiniteQueryFunction} with invalid order: ${properties.invalid.join(', ')}, valid order: ${properties.valid.join(', ')}`,
|
|
167
|
+
code: getCode({
|
|
168
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
169
|
+
properties: properties.invalid,
|
|
170
|
+
}),
|
|
171
|
+
errors: [{ messageId: 'invalidOrder' }],
|
|
172
|
+
output: getCode({
|
|
173
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
174
|
+
properties: properties.valid,
|
|
175
|
+
}),
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
ruleTester.run(name, rule, {
|
|
180
|
+
valid: validTestCases,
|
|
181
|
+
invalid: invalidTestCases,
|
|
182
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { sortDataByOrder } from '../rules/infinite-query-property-order/infinite-query-property-order.utils'
|
|
3
|
+
|
|
4
|
+
describe('create-route-property-order utils', () => {
|
|
5
|
+
describe('sortDataByOrder', () => {
|
|
6
|
+
const testCases = [
|
|
7
|
+
{
|
|
8
|
+
data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
|
|
9
|
+
orderArray: [
|
|
10
|
+
[['a'], ['b']],
|
|
11
|
+
[['b'], ['c']],
|
|
12
|
+
],
|
|
13
|
+
key: 'key',
|
|
14
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
18
|
+
orderArray: [
|
|
19
|
+
[['a'], ['b']],
|
|
20
|
+
[['b'], ['c']],
|
|
21
|
+
],
|
|
22
|
+
key: 'key',
|
|
23
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
27
|
+
orderArray: [
|
|
28
|
+
[['a'], ['b']],
|
|
29
|
+
[['b'], ['c']],
|
|
30
|
+
],
|
|
31
|
+
key: 'key',
|
|
32
|
+
expected: null,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
|
|
36
|
+
orderArray: [
|
|
37
|
+
[['a'], ['b']],
|
|
38
|
+
[['b'], ['c']],
|
|
39
|
+
],
|
|
40
|
+
key: 'key',
|
|
41
|
+
expected: null,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
|
|
45
|
+
orderArray: [
|
|
46
|
+
[['a'], ['b']],
|
|
47
|
+
[['b'], ['c']],
|
|
48
|
+
],
|
|
49
|
+
key: 'key',
|
|
50
|
+
expected: null,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
54
|
+
orderArray: [
|
|
55
|
+
[['a'], ['b']],
|
|
56
|
+
[['b'], ['c']],
|
|
57
|
+
],
|
|
58
|
+
key: 'key',
|
|
59
|
+
expected: null,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
63
|
+
orderArray: [
|
|
64
|
+
[['a'], ['b']],
|
|
65
|
+
[['b'], ['c']],
|
|
66
|
+
],
|
|
67
|
+
key: 'key',
|
|
68
|
+
expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
69
|
+
},
|
|
70
|
+
] as const
|
|
71
|
+
test.each(testCases)(
|
|
72
|
+
'$data $orderArray $key $expected',
|
|
73
|
+
({ data, orderArray, key, expected }) => {
|
|
74
|
+
const sortedData = sortDataByOrder(data, orderArray, key)
|
|
75
|
+
expect(sortedData).toEqual(expected)
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
expectArrayEqualIgnoreOrder,
|
|
4
|
+
generateInterleavedCombinations,
|
|
5
|
+
generatePartialCombinations,
|
|
6
|
+
generatePermutations,
|
|
7
|
+
} from './test-utils'
|
|
8
|
+
|
|
9
|
+
describe('test-utils', () => {
|
|
10
|
+
describe('generatePermutations', () => {
|
|
11
|
+
const testCases = [
|
|
12
|
+
{
|
|
13
|
+
input: ['a', 'b', 'c'],
|
|
14
|
+
expected: [
|
|
15
|
+
['a', 'b', 'c'],
|
|
16
|
+
['a', 'c', 'b'],
|
|
17
|
+
['b', 'a', 'c'],
|
|
18
|
+
['b', 'c', 'a'],
|
|
19
|
+
['c', 'a', 'b'],
|
|
20
|
+
['c', 'b', 'a'],
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
input: ['a', 'b'],
|
|
25
|
+
expected: [
|
|
26
|
+
['a', 'b'],
|
|
27
|
+
['b', 'a'],
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
input: ['a'],
|
|
32
|
+
expected: [['a']],
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
test.each(testCases)('$input $expected', ({ input, expected }) => {
|
|
36
|
+
const permutations = generatePermutations(input)
|
|
37
|
+
expect(permutations).toEqual(expected)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('generatePartialCombinations', () => {
|
|
42
|
+
const testCases = [
|
|
43
|
+
{
|
|
44
|
+
input: ['a', 'b', 'c'],
|
|
45
|
+
minLength: 2,
|
|
46
|
+
expected: [
|
|
47
|
+
['a', 'b'],
|
|
48
|
+
['a', 'c'],
|
|
49
|
+
['b', 'c'],
|
|
50
|
+
['a', 'b', 'c'],
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
input: ['a', 'b'],
|
|
55
|
+
expected: [['a', 'b']],
|
|
56
|
+
minLength: 2,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
input: ['a'],
|
|
60
|
+
expected: [],
|
|
61
|
+
minLength: 2,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
input: ['a'],
|
|
65
|
+
expected: [['a']],
|
|
66
|
+
minLength: 1,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
input: ['a'],
|
|
70
|
+
expected: [[], ['a']],
|
|
71
|
+
minLength: 0,
|
|
72
|
+
},
|
|
73
|
+
]
|
|
74
|
+
test.each(testCases)(
|
|
75
|
+
'$input $minLength $expected',
|
|
76
|
+
({ input, minLength, expected }) => {
|
|
77
|
+
const combinations = generatePartialCombinations(input, minLength)
|
|
78
|
+
expectArrayEqualIgnoreOrder(combinations, expected)
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('generateInterleavedCombinations', () => {
|
|
84
|
+
const testCases = [
|
|
85
|
+
{
|
|
86
|
+
data: ['a', 'b'],
|
|
87
|
+
additional: ['x'],
|
|
88
|
+
expected: [
|
|
89
|
+
['a', 'b'],
|
|
90
|
+
['x', 'a', 'b'],
|
|
91
|
+
['a', 'x', 'b'],
|
|
92
|
+
['a', 'b', 'x'],
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
test.each(testCases)(
|
|
97
|
+
'$input $expected',
|
|
98
|
+
({ data, additional, expected }) => {
|
|
99
|
+
const combinations = generateInterleavedCombinations(data, additional)
|
|
100
|
+
expectArrayEqualIgnoreOrder(combinations, expected)
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
@@ -1,5 +1,108 @@
|
|
|
1
|
+
import { expect } from 'vitest'
|
|
2
|
+
|
|
1
3
|
export function normalizeIndent(template: TemplateStringsArray) {
|
|
2
4
|
const codeLines = template[0]?.split('\n') ?? ['']
|
|
3
5
|
const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? ''
|
|
4
6
|
return codeLines.map((line) => line.slice(leftPadding.length)).join('\n')
|
|
5
7
|
}
|
|
8
|
+
|
|
9
|
+
export function generatePermutations<T>(arr: Array<T>): Array<Array<T>> {
|
|
10
|
+
if (arr.length <= 1) {
|
|
11
|
+
return [arr]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const result: Array<Array<T>> = []
|
|
15
|
+
for (let i = 0; i < arr.length; i++) {
|
|
16
|
+
const rest = arr.slice(0, i).concat(arr.slice(i + 1))
|
|
17
|
+
const restPermutations = generatePermutations(rest)
|
|
18
|
+
for (const perm of restPermutations) {
|
|
19
|
+
result.push([arr[i]!, ...perm])
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return result
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function generatePartialCombinations<T>(
|
|
27
|
+
arr: ReadonlyArray<T>,
|
|
28
|
+
minLength: number,
|
|
29
|
+
): Array<Array<T>> {
|
|
30
|
+
const result: Array<Array<T>> = []
|
|
31
|
+
|
|
32
|
+
function backtrack(start: number, current: Array<T>) {
|
|
33
|
+
if (current.length > minLength - 1) {
|
|
34
|
+
result.push([...current])
|
|
35
|
+
}
|
|
36
|
+
for (let i = start; i < arr.length; i++) {
|
|
37
|
+
current.push(arr[i]!)
|
|
38
|
+
backtrack(i + 1, current)
|
|
39
|
+
current.pop()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
backtrack(0, [])
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expectArrayEqualIgnoreOrder<T>(a: Array<T>, b: Array<T>) {
|
|
47
|
+
expect([...a].sort()).toEqual([...b].sort())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function generateInterleavedCombinations<
|
|
51
|
+
TData,
|
|
52
|
+
TAdditional,
|
|
53
|
+
TResult extends TData | TAdditional,
|
|
54
|
+
>(
|
|
55
|
+
data: Array<TData> | ReadonlyArray<TData>,
|
|
56
|
+
additional: Array<TAdditional> | ReadonlyArray<TAdditional>,
|
|
57
|
+
): Array<Array<TResult>> {
|
|
58
|
+
const result: Array<Array<TResult>> = []
|
|
59
|
+
|
|
60
|
+
function getSubsets(array: Array<TAdditional>): Array<Array<TAdditional>> {
|
|
61
|
+
return array.reduce(
|
|
62
|
+
(subsets, value) => {
|
|
63
|
+
return subsets.concat(subsets.map((set) => [...set, value]))
|
|
64
|
+
},
|
|
65
|
+
[[]] as Array<Array<TAdditional>>,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function insertAtPositions(
|
|
70
|
+
data: Array<TResult>,
|
|
71
|
+
subset: Array<TResult>,
|
|
72
|
+
): Array<Array<TResult>> {
|
|
73
|
+
const combinations: Array<Array<TResult>> = []
|
|
74
|
+
|
|
75
|
+
const recurse = (
|
|
76
|
+
currentData: Array<TResult>,
|
|
77
|
+
currentSubset: Array<TResult>,
|
|
78
|
+
start: number,
|
|
79
|
+
): void => {
|
|
80
|
+
if (currentSubset.length === 0) {
|
|
81
|
+
combinations.push([...currentData])
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (let i = start; i <= currentData.length; i++) {
|
|
86
|
+
const newData = [
|
|
87
|
+
...currentData.slice(0, i),
|
|
88
|
+
currentSubset[0]!,
|
|
89
|
+
...currentData.slice(i),
|
|
90
|
+
]
|
|
91
|
+
recurse(newData, currentSubset.slice(1), i + 1)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
recurse(data, subset, 0)
|
|
96
|
+
return combinations
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const subsets = getSubsets(additional as Array<TAdditional>)
|
|
100
|
+
|
|
101
|
+
subsets.forEach((subset) => {
|
|
102
|
+
result.push(
|
|
103
|
+
...insertAtPositions(data as Array<TResult>, subset as Array<TResult>),
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,7 @@ Object.assign(plugin.configs, {
|
|
|
29
29
|
'@tanstack/query/no-rest-destructuring': 'warn',
|
|
30
30
|
'@tanstack/query/stable-query-client': 'error',
|
|
31
31
|
'@tanstack/query/no-unstable-deps': 'error',
|
|
32
|
+
'@tanstack/query/infinite-query-property-order': 'error',
|
|
32
33
|
},
|
|
33
34
|
},
|
|
34
35
|
'flat/recommended': [
|
|
@@ -41,6 +42,7 @@ Object.assign(plugin.configs, {
|
|
|
41
42
|
'@tanstack/query/no-rest-destructuring': 'warn',
|
|
42
43
|
'@tanstack/query/stable-query-client': 'error',
|
|
43
44
|
'@tanstack/query/no-unstable-deps': 'error',
|
|
45
|
+
'@tanstack/query/infinite-query-property-order': 'error',
|
|
44
46
|
},
|
|
45
47
|
},
|
|
46
48
|
],
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const infiniteQueryFunctions = [
|
|
2
|
+
'infiniteQueryOptions',
|
|
3
|
+
'useInfiniteQuery',
|
|
4
|
+
'useSuspenseInfiniteQuery',
|
|
5
|
+
] as const
|
|
6
|
+
|
|
7
|
+
export type InfiniteQueryFunctions = (typeof infiniteQueryFunctions)[number]
|
|
8
|
+
|
|
9
|
+
export const checkedProperties = [
|
|
10
|
+
'queryFn',
|
|
11
|
+
'getPreviousPageParam',
|
|
12
|
+
'getNextPageParam',
|
|
13
|
+
] as const
|
|
14
|
+
|
|
15
|
+
export const sortRules = [
|
|
16
|
+
[['queryFn'], ['getPreviousPageParam', 'getNextPageParam']],
|
|
17
|
+
] as const
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
|
|
2
|
+
|
|
3
|
+
import { getDocsUrl } from '../../utils/get-docs-url'
|
|
4
|
+
import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
|
|
5
|
+
import { sortDataByOrder } from './infinite-query-property-order.utils'
|
|
6
|
+
import { infiniteQueryFunctions, sortRules } from './constants'
|
|
7
|
+
import type { InfiniteQueryFunctions } from './constants'
|
|
8
|
+
import type { ExtraRuleDocs } from '../../types'
|
|
9
|
+
|
|
10
|
+
const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
|
|
11
|
+
|
|
12
|
+
const infiniteQueryFunctionsSet = new Set(infiniteQueryFunctions)
|
|
13
|
+
function isInfiniteQueryFunction(node: any): node is InfiniteQueryFunctions {
|
|
14
|
+
return infiniteQueryFunctionsSet.has(node)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const name = 'infinite-query-property-order'
|
|
18
|
+
|
|
19
|
+
export const rule = createRule({
|
|
20
|
+
name,
|
|
21
|
+
meta: {
|
|
22
|
+
type: 'problem',
|
|
23
|
+
docs: {
|
|
24
|
+
description:
|
|
25
|
+
'Ensure correct order of inference sensitive properties for infinite queries',
|
|
26
|
+
recommended: 'error',
|
|
27
|
+
},
|
|
28
|
+
messages: {
|
|
29
|
+
invalidOrder: 'Invalid order of properties for `{{function}}`.',
|
|
30
|
+
},
|
|
31
|
+
schema: [],
|
|
32
|
+
hasSuggestions: true,
|
|
33
|
+
fixable: 'code',
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
|
|
37
|
+
create: detectTanstackQueryImports((context) => {
|
|
38
|
+
return {
|
|
39
|
+
CallExpression(node) {
|
|
40
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
const infiniteQueryFunction = node.callee.name
|
|
44
|
+
if (!isInfiniteQueryFunction(infiniteQueryFunction)) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
const argument = node.arguments[0]
|
|
48
|
+
if (argument === undefined || argument.type !== 'ObjectExpression') {
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const allProperties = argument.properties
|
|
53
|
+
|
|
54
|
+
// no need to sort if there is at max 1 property
|
|
55
|
+
if (allProperties.length < 2) {
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const properties = allProperties.flatMap((p) => {
|
|
60
|
+
if (
|
|
61
|
+
p.type === AST_NODE_TYPES.Property &&
|
|
62
|
+
p.key.type === AST_NODE_TYPES.Identifier
|
|
63
|
+
) {
|
|
64
|
+
return { name: p.key.name, property: p }
|
|
65
|
+
} else if (p.type === AST_NODE_TYPES.SpreadElement) {
|
|
66
|
+
if (p.argument.type === AST_NODE_TYPES.Identifier) {
|
|
67
|
+
return { name: p.argument.name, property: p }
|
|
68
|
+
} else {
|
|
69
|
+
throw new Error('Unsupported spread element')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return []
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const sortedProperties = sortDataByOrder(properties, sortRules, 'name')
|
|
76
|
+
if (sortedProperties === null) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
context.report({
|
|
80
|
+
node: argument,
|
|
81
|
+
data: { function: node.callee.name },
|
|
82
|
+
messageId: 'invalidOrder',
|
|
83
|
+
fix(fixer) {
|
|
84
|
+
const sourceCode = context.sourceCode
|
|
85
|
+
|
|
86
|
+
const text = sortedProperties.reduce(
|
|
87
|
+
(sourceText, specifier, index) => {
|
|
88
|
+
let text = ''
|
|
89
|
+
if (index < allProperties.length - 1) {
|
|
90
|
+
text = sourceCode
|
|
91
|
+
.getText()
|
|
92
|
+
.slice(
|
|
93
|
+
allProperties[index]!.range[1],
|
|
94
|
+
allProperties[index + 1]!.range[0],
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
sourceText + sourceCode.getText(specifier.property) + text
|
|
99
|
+
)
|
|
100
|
+
},
|
|
101
|
+
'',
|
|
102
|
+
)
|
|
103
|
+
return fixer.replaceTextRange(
|
|
104
|
+
[allProperties[0]!.range[0], allProperties.at(-1)!.range[1]],
|
|
105
|
+
text,
|
|
106
|
+
)
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
})
|