@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.
Files changed (41) hide show
  1. package/dist/cjs/index.cjs +4 -2
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/rules/infinite-query-property-order/constants.cjs +13 -0
  4. package/dist/cjs/rules/infinite-query-property-order/constants.cjs.map +1 -0
  5. package/dist/cjs/rules/infinite-query-property-order/constants.d.cts +4 -0
  6. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.cjs +95 -0
  7. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.cjs.map +1 -0
  8. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.d.cts +4 -0
  9. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.cjs +47 -0
  10. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.cjs.map +1 -0
  11. package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.utils.d.cts +1 -0
  12. package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.cjs +1 -1
  13. package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.cjs.map +1 -1
  14. package/dist/cjs/rules.cjs +3 -1
  15. package/dist/cjs/rules.cjs.map +1 -1
  16. package/dist/esm/index.js +4 -2
  17. package/dist/esm/index.js.map +1 -1
  18. package/dist/esm/rules/infinite-query-property-order/constants.d.ts +4 -0
  19. package/dist/esm/rules/infinite-query-property-order/constants.js +13 -0
  20. package/dist/esm/rules/infinite-query-property-order/constants.js.map +1 -0
  21. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.d.ts +4 -0
  22. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.js +95 -0
  23. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.rule.js.map +1 -0
  24. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.d.ts +1 -0
  25. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.js +47 -0
  26. package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.js.map +1 -0
  27. package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js +1 -1
  28. package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js.map +1 -1
  29. package/dist/esm/rules.js +3 -1
  30. package/dist/esm/rules.js.map +1 -1
  31. package/package.json +2 -1
  32. package/src/__tests__/infinite-query-property-order.rule.test.ts +182 -0
  33. package/src/__tests__/infinite-query-property-order.utils.test.ts +79 -0
  34. package/src/__tests__/test-utils.test.ts +104 -0
  35. package/src/__tests__/test-utils.ts +103 -0
  36. package/src/index.ts +2 -0
  37. package/src/rules/infinite-query-property-order/constants.ts +17 -0
  38. package/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts +112 -0
  39. package/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts +67 -0
  40. package/src/rules/no-unstable-deps/no-unstable-deps.rule.ts +1 -1
  41. package/src/rules.ts +2 -0
@@ -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":";;;;AAOO,MAAM,QAQT;AAAA,EACF,CAACA,IAAmB,GAAGC;AAAAA,EACvB,CAACC,MAAsB,GAAGC;AAAAA,EAC1B,CAACC,MAAwB,GAAGC;AAAAA,EAC5B,CAACC,MAAmB,GAAGC;AACzB;"}
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.56.1",
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
+ })