-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin-query): add rule to ensure property order of infin…
…ite query functions (#8072) * feat(eslint-plugin-query): add rule to ensure property order of infinite query functions * remove outdated comment --------- Co-authored-by: Dominik Dorfmeister <[email protected]>
- Loading branch information
1 parent
ca9e3c4
commit f8d65fb
Showing
14 changed files
with
728 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
--- | ||
id: infinite-query-property-order | ||
title: Ensure correct order of inference sensitive properties for infinite queries | ||
--- | ||
|
||
For the following functions, the property order of the passed in object matters due to type inference: | ||
|
||
- `useInfiniteQuery` | ||
- `useSuspenseInfiniteQuery` | ||
- `infiniteQueryOptions` | ||
|
||
The correct property order is as follows: | ||
|
||
- `queryFn` | ||
- `getPreviousPageParam` | ||
- `getNextPageParam` | ||
|
||
All other properties are insensitive to the order as they do not depend on type inference. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```tsx | ||
/* eslint "@tanstack/query/infinite-query-property-order": "warn" */ | ||
import { useInfiniteQuery } from '@tanstack/react-query' | ||
|
||
const query = useInfiniteQuery({ | ||
queryKey: ['projects'], | ||
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, | ||
queryFn: async ({ pageParam }) => { | ||
const response = await fetch(`/api/projects?cursor=${pageParam}`) | ||
return await response.json() | ||
}, | ||
initialPageParam: 0, | ||
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, | ||
maxPages: 3, | ||
}) | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```tsx | ||
/* eslint "@tanstack/query/infinite-query-property-order": "warn" */ | ||
import { useInfiniteQuery } from '@tanstack/react-query' | ||
|
||
const query = useInfiniteQuery({ | ||
queryKey: ['projects'], | ||
queryFn: async ({ pageParam }) => { | ||
const response = await fetch(`/api/projects?cursor=${pageParam}`) | ||
return await response.json() | ||
}, | ||
initialPageParam: 0, | ||
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined, | ||
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined, | ||
maxPages: 3, | ||
}) | ||
``` | ||
|
||
## Attributes | ||
|
||
- [x] ✅ Recommended | ||
- [x] 🔧 Fixable |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.rule.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { RuleTester } from '@typescript-eslint/rule-tester' | ||
import combinate from 'combinate' | ||
|
||
import { | ||
checkedProperties, | ||
infiniteQueryFunctions, | ||
} from '../rules/infinite-query-property-order/constants' | ||
import { | ||
name, | ||
rule, | ||
} from '../rules/infinite-query-property-order/infinite-query-property-order.rule' | ||
import { | ||
generateInterleavedCombinations, | ||
generatePartialCombinations, | ||
generatePermutations, | ||
} from './test-utils' | ||
import type { InfiniteQueryFunctions } from '../rules/infinite-query-property-order/constants' | ||
|
||
const ruleTester = new RuleTester() | ||
|
||
type CheckedProperties = (typeof checkedProperties)[number] | ||
const orderIndependentProps = ['queryKey', '...foo'] as const | ||
type OrderIndependentProps = (typeof orderIndependentProps)[number] | ||
|
||
interface TestCase { | ||
infiniteQueryFunction: InfiniteQueryFunctions | ||
properties: Array<CheckedProperties | OrderIndependentProps> | ||
} | ||
|
||
const validTestMatrix = combinate({ | ||
infiniteQueryFunction: [...infiniteQueryFunctions], | ||
properties: generatePartialCombinations(checkedProperties, 2), | ||
}) | ||
|
||
export function generateInvalidPermutations<T>( | ||
arr: ReadonlyArray<T>, | ||
): Array<{ invalid: Array<T>; valid: Array<T> }> { | ||
const combinations = generatePartialCombinations(arr, 2) | ||
const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = [] | ||
|
||
for (const combination of combinations) { | ||
const permutations = generatePermutations(combination) | ||
// skip the first permutation as it matches the original combination | ||
const invalidPermutations = permutations.slice(1) | ||
allPermutations.push( | ||
...invalidPermutations.map((p) => ({ invalid: p, valid: combination })), | ||
) | ||
} | ||
|
||
return allPermutations | ||
} | ||
|
||
const invalidPermutations = generateInvalidPermutations(checkedProperties) | ||
|
||
type Interleaved = CheckedProperties | OrderIndependentProps | ||
const interleavedInvalidPermutations: Array<{ | ||
invalid: Array<Interleaved> | ||
valid: Array<Interleaved> | ||
}> = [] | ||
for (const invalidPermutation of invalidPermutations) { | ||
const invalid = generateInterleavedCombinations( | ||
invalidPermutation.invalid, | ||
orderIndependentProps, | ||
) | ||
const valid = generateInterleavedCombinations( | ||
invalidPermutation.valid, | ||
orderIndependentProps, | ||
) | ||
|
||
for (let i = 0; i < invalid.length; i++) { | ||
interleavedInvalidPermutations.push({ | ||
invalid: invalid[i]!, | ||
valid: valid[i]!, | ||
}) | ||
} | ||
} | ||
|
||
const invalidTestMatrix = combinate({ | ||
infiniteQueryFunction: [...infiniteQueryFunctions], | ||
properties: interleavedInvalidPermutations, | ||
}) | ||
|
||
function getCode({ | ||
infiniteQueryFunction: infiniteQueryFunction, | ||
properties, | ||
}: TestCase) { | ||
function getPropertyCode( | ||
property: CheckedProperties | OrderIndependentProps, | ||
) { | ||
if (property.startsWith('...')) { | ||
return property | ||
} | ||
switch (property) { | ||
case 'queryKey': | ||
return `queryKey: ['projects']` | ||
case 'queryFn': | ||
return 'queryFn: async ({ pageParam }) => { \n await fetch(`/api/projects?cursor=${pageParam}`) \n return await response.json() \n }' | ||
case 'getPreviousPageParam': | ||
return 'getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined' | ||
case 'getNextPageParam': | ||
return 'getNextPageParam: (lastPage) => lastPage.nextId ?? undefined' | ||
} | ||
|
||
return `${property}: () => null` | ||
} | ||
return ` | ||
import { ${infiniteQueryFunction} } from '@tanstack/react-query' | ||
${infiniteQueryFunction}({ | ||
${properties.map(getPropertyCode).join(',\n ')} | ||
}) | ||
` | ||
} | ||
|
||
const validTestCases = validTestMatrix.map( | ||
({ infiniteQueryFunction, properties }) => ({ | ||
name: `should pass when order is correct for ${infiniteQueryFunction} with order: ${properties.join(', ')}`, | ||
code: getCode({ infiniteQueryFunction, properties }), | ||
}), | ||
) | ||
|
||
const invalidTestCases = invalidTestMatrix.map( | ||
({ infiniteQueryFunction, properties }) => ({ | ||
name: `incorrect property order is detected for ${infiniteQueryFunction} with order: ${properties.invalid.join(', ')}`, | ||
code: getCode({ | ||
infiniteQueryFunction: infiniteQueryFunction, | ||
properties: properties.invalid, | ||
}), | ||
errors: [{ messageId: 'invalidOrder' }], | ||
output: getCode({ | ||
infiniteQueryFunction: infiniteQueryFunction, | ||
properties: properties.valid, | ||
}), | ||
}), | ||
) | ||
|
||
ruleTester.run(name, rule, { | ||
valid: validTestCases, | ||
invalid: invalidTestCases, | ||
}) |
58 changes: 58 additions & 0 deletions
58
packages/eslint-plugin-query/src/__tests__/infinite-query-property-order.utils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import { sortDataByOrder } from '../rules/infinite-query-property-order/infinite-query-property-order.utils' | ||
|
||
describe('create-route-property-order utils', () => { | ||
describe('sortDataByOrder', () => { | ||
const testCases = [ | ||
{ | ||
data: [{ key: 'a' }, { key: 'c' }, { key: 'b' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], | ||
}, | ||
{ | ||
data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], | ||
}, | ||
{ | ||
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: null, | ||
}, | ||
{ | ||
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: null, | ||
}, | ||
{ | ||
data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: null, | ||
}, | ||
{ | ||
data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: null, | ||
}, | ||
{ | ||
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }], | ||
orderArray: ['a', 'b', 'c'], | ||
key: 'key', | ||
expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }], | ||
}, | ||
] as const | ||
test.each(testCases)( | ||
'$data $orderArray $key $expected', | ||
({ data, orderArray, key, expected }) => { | ||
const sortedData = sortDataByOrder(data, orderArray, key) | ||
expect(sortedData).toEqual(expected) | ||
}, | ||
) | ||
}) | ||
}) |
104 changes: 104 additions & 0 deletions
104
packages/eslint-plugin-query/src/__tests__/test-utils.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { describe, expect, test } from 'vitest' | ||
import { | ||
expectArrayEqualIgnoreOrder, | ||
generateInterleavedCombinations, | ||
generatePartialCombinations, | ||
generatePermutations, | ||
} from './test-utils' | ||
|
||
describe('test-utils', () => { | ||
describe('generatePermutations', () => { | ||
const testCases = [ | ||
{ | ||
input: ['a', 'b', 'c'], | ||
expected: [ | ||
['a', 'b', 'c'], | ||
['a', 'c', 'b'], | ||
['b', 'a', 'c'], | ||
['b', 'c', 'a'], | ||
['c', 'a', 'b'], | ||
['c', 'b', 'a'], | ||
], | ||
}, | ||
{ | ||
input: ['a', 'b'], | ||
expected: [ | ||
['a', 'b'], | ||
['b', 'a'], | ||
], | ||
}, | ||
{ | ||
input: ['a'], | ||
expected: [['a']], | ||
}, | ||
] | ||
test.each(testCases)('$input $expected', ({ input, expected }) => { | ||
const permutations = generatePermutations(input) | ||
expect(permutations).toEqual(expected) | ||
}) | ||
}) | ||
|
||
describe('generatePartialCombinations', () => { | ||
const testCases = [ | ||
{ | ||
input: ['a', 'b', 'c'], | ||
minLength: 2, | ||
expected: [ | ||
['a', 'b'], | ||
['a', 'c'], | ||
['b', 'c'], | ||
['a', 'b', 'c'], | ||
], | ||
}, | ||
{ | ||
input: ['a', 'b'], | ||
expected: [['a', 'b']], | ||
minLength: 2, | ||
}, | ||
{ | ||
input: ['a'], | ||
expected: [], | ||
minLength: 2, | ||
}, | ||
{ | ||
input: ['a'], | ||
expected: [['a']], | ||
minLength: 1, | ||
}, | ||
{ | ||
input: ['a'], | ||
expected: [[], ['a']], | ||
minLength: 0, | ||
}, | ||
] | ||
test.each(testCases)( | ||
'$input $minLength $expected', | ||
({ input, minLength, expected }) => { | ||
const combinations = generatePartialCombinations(input, minLength) | ||
expectArrayEqualIgnoreOrder(combinations, expected) | ||
}, | ||
) | ||
}) | ||
|
||
describe('generateInterleavedCombinations', () => { | ||
const testCases = [ | ||
{ | ||
data: ['a', 'b'], | ||
additional: ['x'], | ||
expected: [ | ||
['a', 'b'], | ||
['x', 'a', 'b'], | ||
['a', 'x', 'b'], | ||
['a', 'b', 'x'], | ||
], | ||
}, | ||
] | ||
test.each(testCases)( | ||
'$input $expected', | ||
({ data, additional, expected }) => { | ||
const combinations = generateInterleavedCombinations(data, additional) | ||
expectArrayEqualIgnoreOrder(combinations, expected) | ||
}, | ||
) | ||
}) | ||
}) |
Oops, something went wrong.