Skip to content

Commit 074dc30

Browse files
fix(eslint-plugin-query): relax property order rule to ignore relative order of getPreviousPageParam and getNextPageParam
1 parent 1d851dd commit 074dc30

File tree

5 files changed

+131
-33
lines changed

5 files changed

+131
-33
lines changed

packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,60 @@ const validTestMatrix = combinate({
3232
properties: generatePartialCombinations(checkedProperties, 2),
3333
})
3434

35-
export function generateInvalidPermutations<T>(
36-
arr: ReadonlyArray<T>,
37-
): Array<{ invalid: Array<T>; valid: Array<T> }> {
35+
export function generateInvalidPermutations(
36+
arr: ReadonlyArray<CheckedProperties>,
37+
): Array<{
38+
invalid: Array<CheckedProperties>
39+
valid: Array<CheckedProperties>
40+
}> {
3841
const combinations = generatePartialCombinations(arr, 2)
39-
const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = []
42+
const allPermutations: Array<{
43+
invalid: Array<CheckedProperties>
44+
valid: Array<CheckedProperties>
45+
}> = []
4046

4147
for (const combination of combinations) {
4248
const permutations = generatePermutations(combination)
4349
// skip the first permutation as it matches the original combination
4450
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+
4566
allPermutations.push(
46-
...invalidPermutations.map((p) => ({ invalid: p, valid: combination })),
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+
),
4789
)
4890
}
4991

@@ -121,7 +163,7 @@ const validTestCases = validTestMatrix.map(
121163

122164
const invalidTestCases = invalidTestMatrix.map(
123165
({ infiniteQueryFunction, properties }) => ({
124-
name: `incorrect property order is detected for ${infiniteQueryFunction} with order: ${properties.invalid.join(', ')}`,
166+
name: `incorrect property order is detected for ${infiniteQueryFunction} with invalid order: ${properties.invalid.join(', ')}, valid order: ${properties.valid.join(', ')}`,
125167
code: getCode({
126168
infiniteQueryFunction: infiniteQueryFunction,
127169
properties: properties.invalid,

packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,64 @@ describe('create-route-property-order utils', () => {
66
const testCases = [
77
{
88
data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }],
9-
orderArray: ['a', 'b', 'c'],
9+
orderArray: [
10+
[['a'], ['b']],
11+
[['b'], ['c']],
12+
],
1013
key: 'key',
1114
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
1215
},
1316
{
1417
data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
15-
orderArray: ['a', 'b', 'c'],
18+
orderArray: [
19+
[['a'], ['b']],
20+
[['b'], ['c']],
21+
],
1622
key: 'key',
1723
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
1824
},
1925
{
2026
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
21-
orderArray: ['a', 'b', 'c'],
27+
orderArray: [
28+
[['a'], ['b']],
29+
[['b'], ['c']],
30+
],
2231
key: 'key',
2332
expected: null,
2433
},
2534
{
2635
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
27-
orderArray: ['a', 'b', 'c'],
36+
orderArray: [
37+
[['a'], ['b']],
38+
[['b'], ['c']],
39+
],
2840
key: 'key',
2941
expected: null,
3042
},
3143
{
3244
data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
33-
orderArray: ['a', 'b', 'c'],
45+
orderArray: [
46+
[['a'], ['b']],
47+
[['b'], ['c']],
48+
],
3449
key: 'key',
3550
expected: null,
3651
},
3752
{
3853
data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
39-
orderArray: ['a', 'b', 'c'],
54+
orderArray: [
55+
[['a'], ['b']],
56+
[['b'], ['c']],
57+
],
4058
key: 'key',
4159
expected: null,
4260
},
4361
{
4462
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
45-
orderArray: ['a', 'b', 'c'],
63+
orderArray: [
64+
[['a'], ['b']],
65+
[['b'], ['c']],
66+
],
4667
key: 'key',
4768
expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
4869
},

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ export const checkedProperties = [
1111
'getPreviousPageParam',
1212
'getNextPageParam',
1313
] as const
14+
15+
export const sortRules = [
16+
[['queryFn'], ['getPreviousPageParam', 'getNextPageParam']],
17+
] as const

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
33
import { getDocsUrl } from '../../utils/get-docs-url'
44
import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
55
import { sortDataByOrder } from './infinite-query-property-order.utils'
6-
import { checkedProperties, infiniteQueryFunctions } from './constants'
6+
import {
7+
checkedProperties,
8+
infiniteQueryFunctions,
9+
sortRules,
10+
} from './constants'
711
import type { InfiniteQueryFunctions } from './constants'
812
import type { ExtraRuleDocs } from '../../types'
913

@@ -50,6 +54,8 @@ export const rule = createRule({
5054
}
5155

5256
const allProperties = argument.properties
57+
58+
// no need to sort if there is at max 1 property
5359
if (allProperties.length < 2) {
5460
return
5561
}
@@ -70,11 +76,7 @@ export const rule = createRule({
7076
return []
7177
})
7278

73-
const sortedProperties = sortDataByOrder(
74-
properties,
75-
checkedProperties,
76-
'name',
77-
)
79+
const sortedProperties = sortDataByOrder(properties, sortRules, 'name')
7880
if (sortedProperties === null) {
7981
return
8082
}

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

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,56 @@
11
export function sortDataByOrder<T, TKey extends keyof T>(
22
data: Array<T> | ReadonlyArray<T>,
3-
orderArray: Array<T[TKey]> | ReadonlyArray<T[TKey]>,
3+
orderRules: ReadonlyArray<
4+
Readonly<[ReadonlyArray<T[TKey]>, ReadonlyArray<T[TKey]>]>
5+
>,
46
key: TKey,
57
): Array<T> | null {
6-
const orderMap = new Map(orderArray.map((item, index) => [item, index]))
7-
8-
// Separate items that are in orderArray from those that are not
9-
const inOrderArray = data
10-
.filter((item) => orderMap.has(item[key]))
11-
.sort((a, b) => {
12-
const indexA = orderMap.get(a[key])!
13-
const indexB = orderMap.get(b[key])!
8+
const getSubsetIndex = (
9+
item: T[TKey],
10+
subsets: ReadonlyArray<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
11+
): number | null => {
12+
for (let i = 0; i < subsets.length; i++) {
13+
if (subsets[i]?.includes(item)) {
14+
return i
15+
}
16+
}
17+
return null
18+
}
1419

15-
return indexA - indexB
16-
})
20+
const orderSets = orderRules.reduce(
21+
(sets, [A, B]) => [...sets, A, B],
22+
[] as Array<ReadonlyArray<T[TKey]> | Array<T[TKey]>>,
23+
)
1724

18-
const inOrderIterator = inOrderArray.values()
25+
const inOrderArray = data.filter(
26+
(item) => getSubsetIndex(item[key], orderSets) !== null,
27+
)
1928

20-
// `as boolean` is needed to avoid TS incorrectly inferring that wasResorted is always `true`
2129
let wasResorted = false as boolean
2230

31+
// Sort by the relative order defined by the rules
32+
const sortedArray = inOrderArray.sort((a, b) => {
33+
const aKey = a[key],
34+
bKey = b[key]
35+
const aSubsetIndex = getSubsetIndex(aKey, orderSets)
36+
const bSubsetIndex = getSubsetIndex(bKey, orderSets)
37+
38+
// If both items belong to different subsets, sort by their subset order
39+
if (
40+
aSubsetIndex !== null &&
41+
bSubsetIndex !== null &&
42+
aSubsetIndex !== bSubsetIndex
43+
) {
44+
return aSubsetIndex - bSubsetIndex
45+
}
46+
47+
// If both items belong to the same subset or neither is in the subset, keep their relative order
48+
return 0
49+
})
50+
51+
const inOrderIterator = sortedArray.values()
2352
const result = data.map((item) => {
24-
if (orderMap.has(item[key])) {
53+
if (getSubsetIndex(item[key], orderSets) !== null) {
2554
const sortedItem = inOrderIterator.next().value!
2655
if (sortedItem[key] !== item[key]) {
2756
wasResorted = true

0 commit comments

Comments
 (0)