@tanstack/eslint-plugin-query 5.53.0 → 5.57.0
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/index.d.cts +0 -1
- package/dist/cjs/rules/exhaustive-deps/exhaustive-deps.rule.d.cts +0 -1
- package/dist/cjs/rules/exhaustive-deps/exhaustive-deps.utils.d.cts +0 -1
- package/dist/cjs/rules/infinite-query-property-order/constants.cjs +15 -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 +3 -0
- package/dist/cjs/rules/infinite-query-property-order/infinite-query-property-order.rule.cjs +99 -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 +28 -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-rest-destructuring/no-rest-destructuring.rule.d.cts +0 -1
- package/dist/cjs/rules/no-rest-destructuring/no-rest-destructuring.utils.d.cts +0 -1
- 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/no-unstable-deps/no-unstable-deps.rule.d.cts +0 -1
- package/dist/cjs/rules/stable-query-client/stable-query-client.rule.d.cts +0 -1
- package/dist/cjs/rules.cjs +3 -1
- package/dist/cjs/rules.cjs.map +1 -1
- package/dist/cjs/rules.d.cts +0 -1
- package/dist/cjs/utils/ast-utils.d.cts +0 -1
- package/dist/cjs/utils/detect-react-query-imports.d.cts +0 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rules/exhaustive-deps/exhaustive-deps.rule.d.ts +0 -1
- package/dist/esm/rules/exhaustive-deps/exhaustive-deps.utils.d.ts +0 -1
- package/dist/esm/rules/infinite-query-property-order/constants.d.ts +3 -0
- package/dist/esm/rules/infinite-query-property-order/constants.js +15 -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 +99 -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 +28 -0
- package/dist/esm/rules/infinite-query-property-order/infinite-query-property-order.utils.js.map +1 -0
- package/dist/esm/rules/no-rest-destructuring/no-rest-destructuring.rule.d.ts +0 -1
- package/dist/esm/rules/no-rest-destructuring/no-rest-destructuring.utils.d.ts +0 -1
- package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.d.ts +0 -1
- 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/stable-query-client/stable-query-client.rule.d.ts +0 -1
- package/dist/esm/rules.d.ts +0 -1
- package/dist/esm/rules.js +3 -1
- package/dist/esm/rules.js.map +1 -1
- package/dist/esm/utils/ast-utils.d.ts +0 -1
- package/dist/esm/utils/detect-react-query-imports.d.ts +0 -1
- package/package.json +2 -1
- package/src/__tests__/infinite-query-property-order.rule.test.ts +140 -0
- package/src/__tests__/infinite-query-property-order.utils.test.ts +58 -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 +13 -0
- package/src/rules/infinite-query-property-order/infinite-query-property-order.rule.ts +114 -0
- package/src/rules/infinite-query-property-order/infinite-query-property-order.utils.ts +38 -0
- package/src/rules/no-unstable-deps/no-unstable-deps.rule.ts +1 -1
- package/src/rules.ts +2 -0
|
@@ -18,7 +18,7 @@ const rule = createRule({
|
|
|
18
18
|
meta: {
|
|
19
19
|
type: "problem",
|
|
20
20
|
docs: {
|
|
21
|
-
description: "Disallow putting the result of
|
|
21
|
+
description: "Disallow putting the result of query hooks directly in a React hook dependency array",
|
|
22
22
|
recommended: "error"
|
|
23
23
|
},
|
|
24
24
|
messages: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unstable-deps.rule.js","sources":["../../../../src/rules/no-unstable-deps/no-unstable-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nexport const name = 'no-unstable-deps'\n\nexport const reactHookNames = ['useEffect', 'useCallback', 'useMemo']\nexport const useQueryHookNames = [\n 'useQuery',\n 'useSuspenseQuery',\n 'useQueries',\n 'useSuspenseQueries',\n 'useInfiniteQuery',\n 'useSuspenseInfiniteQuery',\n]\nconst allHookNames = ['useMutation', ...useQueryHookNames]\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow putting the result of
|
|
1
|
+
{"version":3,"file":"no-unstable-deps.rule.js","sources":["../../../../src/rules/no-unstable-deps/no-unstable-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport type { TSESTree } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nexport const name = 'no-unstable-deps'\n\nexport const reactHookNames = ['useEffect', 'useCallback', 'useMemo']\nexport const useQueryHookNames = [\n 'useQuery',\n 'useSuspenseQuery',\n 'useQueries',\n 'useSuspenseQueries',\n 'useInfiniteQuery',\n 'useSuspenseInfiniteQuery',\n]\nconst allHookNames = ['useMutation', ...useQueryHookNames]\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow putting the result of query hooks directly in a React hook dependency array',\n recommended: 'error',\n },\n messages: {\n noUnstableDeps: `The result of {{queryHook}} is not referentially stable, so don't pass it directly into the dependencies array of {{reactHook}}. Instead, destructure the return value of {{queryHook}} and pass the destructured values into the dependency array of {{reactHook}}.`,\n },\n schema: [],\n },\n defaultOptions: [],\n\n create: detectTanstackQueryImports((context) => {\n const trackedVariables: Record<string, string> = {}\n const hookAliasMap: Record<string, string> = {}\n\n function getReactHook(node: TSESTree.CallExpression): string | undefined {\n if (node.callee.type === 'Identifier') {\n const calleeName = node.callee.name\n // Check if the identifier is a known React hook or an alias\n if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) {\n return calleeName\n }\n } else if (\n node.callee.type === 'MemberExpression' &&\n node.callee.object.type === 'Identifier' &&\n node.callee.object.name === 'React' &&\n node.callee.property.type === 'Identifier' &&\n reactHookNames.includes(node.callee.property.name)\n ) {\n // Member expression case: `React.useCallback`\n return node.callee.property.name\n }\n return undefined\n }\n\n function collectVariableNames(\n pattern: TSESTree.BindingName,\n queryHook: string,\n ) {\n if (pattern.type === AST_NODE_TYPES.Identifier) {\n trackedVariables[pattern.name] = queryHook\n }\n }\n\n return {\n ImportDeclaration(node: TSESTree.ImportDeclaration) {\n if (\n node.specifiers.length > 0 &&\n node.importKind === 'value' &&\n node.source.value === 'React'\n ) {\n node.specifiers.forEach((specifier) => {\n if (\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n reactHookNames.includes(specifier.imported.name)\n ) {\n // Track alias or direct import\n hookAliasMap[specifier.local.name] = specifier.imported.name\n }\n })\n }\n },\n\n VariableDeclarator(node) {\n if (\n node.init !== null &&\n node.init.type === AST_NODE_TYPES.CallExpression &&\n node.init.callee.type === AST_NODE_TYPES.Identifier &&\n allHookNames.includes(node.init.callee.name)\n ) {\n collectVariableNames(node.id, node.init.callee.name)\n }\n },\n CallExpression: (node) => {\n const reactHook = getReactHook(node)\n if (\n reactHook !== undefined &&\n node.arguments.length > 1 &&\n node.arguments[1]?.type === AST_NODE_TYPES.ArrayExpression\n ) {\n const depsArray = node.arguments[1].elements\n depsArray.forEach((dep) => {\n if (\n dep !== null &&\n dep.type === AST_NODE_TYPES.Identifier &&\n trackedVariables[dep.name] !== undefined\n ) {\n const queryHook = trackedVariables[dep.name]\n context.report({\n node: dep,\n messageId: 'noUnstableDeps',\n data: {\n queryHook,\n reactHook,\n },\n })\n }\n })\n }\n },\n }\n }),\n})\n"],"names":[],"mappings":";;;AAMO,MAAM,OAAO;AAEb,MAAM,iBAAiB,CAAC,aAAa,eAAe,SAAS;AAC7D,MAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AACA,MAAM,eAAe,CAAC,eAAe,GAAG,iBAAiB;AACzD,MAAM,aAAa,YAAY,YAA2B,UAAU;AAE7D,MAAM,OAAO,WAAW;AAAA,EAC7B;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aACE;AAAA,MACF,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,gBAAgB;AAAA,IAClB;AAAA,IACA,QAAQ,CAAC;AAAA,EACX;AAAA,EACA,gBAAgB,CAAC;AAAA,EAEjB,QAAQ,2BAA2B,CAAC,YAAY;AAC9C,UAAM,mBAA2C,CAAA;AACjD,UAAM,eAAuC,CAAA;AAE7C,aAAS,aAAa,MAAmD;AACnE,UAAA,KAAK,OAAO,SAAS,cAAc;AAC/B,cAAA,aAAa,KAAK,OAAO;AAE/B,YAAI,eAAe,SAAS,UAAU,KAAK,cAAc,cAAc;AAC9D,iBAAA;AAAA,QACT;AAAA,MACF,WACE,KAAK,OAAO,SAAS,sBACrB,KAAK,OAAO,OAAO,SAAS,gBAC5B,KAAK,OAAO,OAAO,SAAS,WAC5B,KAAK,OAAO,SAAS,SAAS,gBAC9B,eAAe,SAAS,KAAK,OAAO,SAAS,IAAI,GACjD;AAEO,eAAA,KAAK,OAAO,SAAS;AAAA,MAC9B;AACO,aAAA;AAAA,IACT;AAES,aAAA,qBACP,SACA,WACA;AACI,UAAA,QAAQ,SAAS,eAAe,YAAY;AAC7B,yBAAA,QAAQ,IAAI,IAAI;AAAA,MACnC;AAAA,IACF;AAEO,WAAA;AAAA,MACL,kBAAkB,MAAkC;AAEhD,YAAA,KAAK,WAAW,SAAS,KACzB,KAAK,eAAe,WACpB,KAAK,OAAO,UAAU,SACtB;AACK,eAAA,WAAW,QAAQ,CAAC,cAAc;AAEnC,gBAAA,UAAU,SAAS,eAAe,mBAClC,eAAe,SAAS,UAAU,SAAS,IAAI,GAC/C;AAEA,2BAAa,UAAU,MAAM,IAAI,IAAI,UAAU,SAAS;AAAA,YAC1D;AAAA,UAAA,CACD;AAAA,QACH;AAAA,MACF;AAAA,MAEA,mBAAmB,MAAM;AAErB,YAAA,KAAK,SAAS,QACd,KAAK,KAAK,SAAS,eAAe,kBAClC,KAAK,KAAK,OAAO,SAAS,eAAe,cACzC,aAAa,SAAS,KAAK,KAAK,OAAO,IAAI,GAC3C;AACA,+BAAqB,KAAK,IAAI,KAAK,KAAK,OAAO,IAAI;AAAA,QACrD;AAAA,MACF;AAAA,MACA,gBAAgB,CAAC,SAAS;;AAClB,cAAA,YAAY,aAAa,IAAI;AACnC,YACE,cAAc,UACd,KAAK,UAAU,SAAS,OACxB,UAAK,UAAU,CAAC,MAAhB,mBAAmB,UAAS,eAAe,iBAC3C;AACA,gBAAM,YAAY,KAAK,UAAU,CAAC,EAAE;AAC1B,oBAAA,QAAQ,CAAC,QAAQ;AAEvB,gBAAA,QAAQ,QACR,IAAI,SAAS,eAAe,cAC5B,iBAAiB,IAAI,IAAI,MAAM,QAC/B;AACM,oBAAA,YAAY,iBAAiB,IAAI,IAAI;AAC3C,sBAAQ,OAAO;AAAA,gBACb,MAAM;AAAA,gBACN,WAAW;AAAA,gBACX,MAAM;AAAA,kBACJ;AAAA,kBACA;AAAA,gBACF;AAAA,cAAA,CACD;AAAA,YACH;AAAA,UAAA,CACD;AAAA,QACH;AAAA,MACF;AAAA,IAAA;AAAA,EACF,CACD;AACH,CAAC;"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
2
|
import { ExtraRuleDocs } from '../../types';
|
|
3
|
-
|
|
4
3
|
export declare const name = "stable-query-client";
|
|
5
4
|
export declare const rule: ESLintUtils.RuleModule<string, readonly unknown[], ExtraRuleDocs, ESLintUtils.RuleListener>;
|
package/dist/esm/rules.d.ts
CHANGED
package/dist/esm/rules.js
CHANGED
|
@@ -2,11 +2,13 @@ import { name, rule } from "./rules/exhaustive-deps/exhaustive-deps.rule.js";
|
|
|
2
2
|
import { name as name$1, rule as rule$1 } from "./rules/stable-query-client/stable-query-client.rule.js";
|
|
3
3
|
import { name as name$2, rule as rule$2 } from "./rules/no-rest-destructuring/no-rest-destructuring.rule.js";
|
|
4
4
|
import { name as name$3, rule as rule$3 } from "./rules/no-unstable-deps/no-unstable-deps.rule.js";
|
|
5
|
+
import { name as name$4, rule as rule$4 } from "./rules/infinite-query-property-order/infinite-query-property-order.rule.js";
|
|
5
6
|
const rules = {
|
|
6
7
|
[name]: rule,
|
|
7
8
|
[name$1]: rule$1,
|
|
8
9
|
[name$2]: rule$2,
|
|
9
|
-
[name$3]: rule$3
|
|
10
|
+
[name$3]: rule$3,
|
|
11
|
+
[name$4]: rule$4
|
|
10
12
|
};
|
|
11
13
|
export {
|
|
12
14
|
rules
|
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.0",
|
|
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,140 @@
|
|
|
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<T>(
|
|
36
|
+
arr: ReadonlyArray<T>,
|
|
37
|
+
): Array<{ invalid: Array<T>; valid: Array<T> }> {
|
|
38
|
+
const combinations = generatePartialCombinations(arr, 2)
|
|
39
|
+
const allPermutations: Array<{ invalid: Array<T>; valid: Array<T> }> = []
|
|
40
|
+
|
|
41
|
+
for (const combination of combinations) {
|
|
42
|
+
const permutations = generatePermutations(combination)
|
|
43
|
+
// skip the first permutation as it matches the original combination
|
|
44
|
+
const invalidPermutations = permutations.slice(1)
|
|
45
|
+
allPermutations.push(
|
|
46
|
+
...invalidPermutations.map((p) => ({ invalid: p, valid: combination })),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return allPermutations
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const invalidPermutations = generateInvalidPermutations(checkedProperties)
|
|
54
|
+
|
|
55
|
+
type Interleaved = CheckedProperties | OrderIndependentProps
|
|
56
|
+
const interleavedInvalidPermutations: Array<{
|
|
57
|
+
invalid: Array<Interleaved>
|
|
58
|
+
valid: Array<Interleaved>
|
|
59
|
+
}> = []
|
|
60
|
+
for (const invalidPermutation of invalidPermutations) {
|
|
61
|
+
const invalid = generateInterleavedCombinations(
|
|
62
|
+
invalidPermutation.invalid,
|
|
63
|
+
orderIndependentProps,
|
|
64
|
+
)
|
|
65
|
+
const valid = generateInterleavedCombinations(
|
|
66
|
+
invalidPermutation.valid,
|
|
67
|
+
orderIndependentProps,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < invalid.length; i++) {
|
|
71
|
+
interleavedInvalidPermutations.push({
|
|
72
|
+
invalid: invalid[i]!,
|
|
73
|
+
valid: valid[i]!,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const invalidTestMatrix = combinate({
|
|
79
|
+
infiniteQueryFunction: [...infiniteQueryFunctions],
|
|
80
|
+
properties: interleavedInvalidPermutations,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function getCode({
|
|
84
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
85
|
+
properties,
|
|
86
|
+
}: TestCase) {
|
|
87
|
+
function getPropertyCode(
|
|
88
|
+
property: CheckedProperties | OrderIndependentProps,
|
|
89
|
+
) {
|
|
90
|
+
if (property.startsWith('...')) {
|
|
91
|
+
return property
|
|
92
|
+
}
|
|
93
|
+
switch (property) {
|
|
94
|
+
case 'queryKey':
|
|
95
|
+
return `queryKey: ['projects']`
|
|
96
|
+
case 'queryFn':
|
|
97
|
+
return 'queryFn: async ({ pageParam }) => { \n await fetch(`/api/projects?cursor=${pageParam}`) \n return await response.json() \n }'
|
|
98
|
+
case 'getPreviousPageParam':
|
|
99
|
+
return 'getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined'
|
|
100
|
+
case 'getNextPageParam':
|
|
101
|
+
return 'getNextPageParam: (lastPage) => lastPage.nextId ?? undefined'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `${property}: () => null`
|
|
105
|
+
}
|
|
106
|
+
return `
|
|
107
|
+
import { ${infiniteQueryFunction} } from '@tanstack/react-query'
|
|
108
|
+
|
|
109
|
+
${infiniteQueryFunction}({
|
|
110
|
+
${properties.map(getPropertyCode).join(',\n ')}
|
|
111
|
+
})
|
|
112
|
+
`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const validTestCases = validTestMatrix.map(
|
|
116
|
+
({ infiniteQueryFunction, properties }) => ({
|
|
117
|
+
name: `should pass when order is correct for ${infiniteQueryFunction} with order: ${properties.join(', ')}`,
|
|
118
|
+
code: getCode({ infiniteQueryFunction, properties }),
|
|
119
|
+
}),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
const invalidTestCases = invalidTestMatrix.map(
|
|
123
|
+
({ infiniteQueryFunction, properties }) => ({
|
|
124
|
+
name: `incorrect property order is detected for ${infiniteQueryFunction} with order: ${properties.invalid.join(', ')}`,
|
|
125
|
+
code: getCode({
|
|
126
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
127
|
+
properties: properties.invalid,
|
|
128
|
+
}),
|
|
129
|
+
errors: [{ messageId: 'invalidOrder' }],
|
|
130
|
+
output: getCode({
|
|
131
|
+
infiniteQueryFunction: infiniteQueryFunction,
|
|
132
|
+
properties: properties.valid,
|
|
133
|
+
}),
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
ruleTester.run(name, rule, {
|
|
138
|
+
valid: validTestCases,
|
|
139
|
+
invalid: invalidTestCases,
|
|
140
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
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: ['a', 'b', 'c'],
|
|
10
|
+
key: 'key',
|
|
11
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
data: [{ key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
15
|
+
orderArray: ['a', 'b', 'c'],
|
|
16
|
+
key: 'key',
|
|
17
|
+
expected: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
21
|
+
orderArray: ['a', 'b', 'c'],
|
|
22
|
+
key: 'key',
|
|
23
|
+
expected: null,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'c' }, { key: 'd' }],
|
|
27
|
+
orderArray: ['a', 'b', 'c'],
|
|
28
|
+
key: 'key',
|
|
29
|
+
expected: null,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
data: [{ key: 'a' }, { key: 'b' }, { key: 'd' }, { key: 'c' }],
|
|
33
|
+
orderArray: ['a', 'b', 'c'],
|
|
34
|
+
key: 'key',
|
|
35
|
+
expected: null,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
data: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
39
|
+
orderArray: ['a', 'b', 'c'],
|
|
40
|
+
key: 'key',
|
|
41
|
+
expected: null,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
data: [{ key: 'd' }, { key: 'b' }, { key: 'a' }, { key: 'c' }],
|
|
45
|
+
orderArray: ['a', 'b', 'c'],
|
|
46
|
+
key: 'key',
|
|
47
|
+
expected: [{ key: 'd' }, { key: 'a' }, { key: 'b' }, { key: 'c' }],
|
|
48
|
+
},
|
|
49
|
+
] as const
|
|
50
|
+
test.each(testCases)(
|
|
51
|
+
'$data $orderArray $key $expected',
|
|
52
|
+
({ data, orderArray, key, expected }) => {
|
|
53
|
+
const sortedData = sortDataByOrder(data, orderArray, key)
|
|
54
|
+
expect(sortedData).toEqual(expected)
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -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,13 @@
|
|
|
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
|