@tanstack/eslint-plugin-query 5.51.14 → 5.52.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/rules/no-unstable-deps/no-unstable-deps.rule.cjs +93 -0
- package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.cjs.map +1 -0
- package/dist/cjs/rules/no-unstable-deps/no-unstable-deps.rule.d.cts +7 -0
- 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/no-unstable-deps/no-unstable-deps.rule.d.ts +7 -0
- package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js +93 -0
- package/dist/esm/rules/no-unstable-deps/no-unstable-deps.rule.js.map +1 -0
- package/dist/esm/rules.js +3 -1
- package/dist/esm/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/no-unstable-deps.test.ts +132 -0
- package/src/index.ts +2 -0
- package/src/rules/no-unstable-deps/no-unstable-deps.rule.ts +128 -0
- package/src/rules.ts +2 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -13,7 +13,8 @@ Object.assign(plugin.configs, {
|
|
|
13
13
|
rules: {
|
|
14
14
|
"@tanstack/query/exhaustive-deps": "error",
|
|
15
15
|
"@tanstack/query/no-rest-destructuring": "warn",
|
|
16
|
-
"@tanstack/query/stable-query-client": "error"
|
|
16
|
+
"@tanstack/query/stable-query-client": "error",
|
|
17
|
+
"@tanstack/query/no-unstable-deps": "error"
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
"flat/recommended": [
|
|
@@ -24,7 +25,8 @@ Object.assign(plugin.configs, {
|
|
|
24
25
|
rules: {
|
|
25
26
|
"@tanstack/query/exhaustive-deps": "error",
|
|
26
27
|
"@tanstack/query/no-rest-destructuring": "warn",
|
|
27
|
-
"@tanstack/query/stable-query-client": "error"
|
|
28
|
+
"@tanstack/query/stable-query-client": "error",
|
|
29
|
+
"@tanstack/query/no-unstable-deps": "error"
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
]
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","sources":["../../src/index.ts"],"sourcesContent":["import { rules } from './rules'\nimport type { ESLint, Linter } from 'eslint'\nimport type { RuleModule } from '@typescript-eslint/utils/ts-eslint'\n\ntype RuleKey = keyof typeof rules\n\ninterface Plugin extends Omit<ESLint.Plugin, 'rules'> {\n rules: Record<RuleKey, RuleModule<any, any, any>>\n configs: {\n recommended: ESLint.ConfigData\n 'flat/recommended': Array<Linter.FlatConfig>\n }\n}\n\nconst plugin: Plugin = {\n meta: {\n name: '@tanstack/eslint-plugin-query',\n },\n configs: {} as Plugin['configs'],\n rules,\n}\n\n// Assign configs here so we can reference `plugin`\nObject.assign(plugin.configs, {\n recommended: {\n plugins: ['@tanstack/eslint-plugin-query'],\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n },\n },\n 'flat/recommended': [\n {\n plugins: {\n '@tanstack/query': plugin,\n },\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n },\n },\n ],\n})\n\nexport default plugin\n"],"names":["rules"],"mappings":";;AAcA,MAAM,SAAiB;AAAA,EACrB,MAAM;AAAA,IACJ,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC;AAAA,EAAA,OACVA,MAAA;AACF;AAGA,OAAO,OAAO,OAAO,SAAS;AAAA,EAC5B,aAAa;AAAA,IACX,SAAS,CAAC,+BAA+B;AAAA,IACzC,OAAO;AAAA,MACL,mCAAmC;AAAA,MACnC,yCAAyC;AAAA,MACzC,uCAAuC;AAAA,
|
|
1
|
+
{"version":3,"file":"index.cjs","sources":["../../src/index.ts"],"sourcesContent":["import { rules } from './rules'\nimport type { ESLint, Linter } from 'eslint'\nimport type { RuleModule } from '@typescript-eslint/utils/ts-eslint'\n\ntype RuleKey = keyof typeof rules\n\ninterface Plugin extends Omit<ESLint.Plugin, 'rules'> {\n rules: Record<RuleKey, RuleModule<any, any, any>>\n configs: {\n recommended: ESLint.ConfigData\n 'flat/recommended': Array<Linter.FlatConfig>\n }\n}\n\nconst plugin: Plugin = {\n meta: {\n name: '@tanstack/eslint-plugin-query',\n },\n configs: {} as Plugin['configs'],\n rules,\n}\n\n// Assign configs here so we can reference `plugin`\nObject.assign(plugin.configs, {\n recommended: {\n plugins: ['@tanstack/eslint-plugin-query'],\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n '@tanstack/query/no-unstable-deps': 'error',\n },\n },\n 'flat/recommended': [\n {\n plugins: {\n '@tanstack/query': plugin,\n },\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n '@tanstack/query/no-unstable-deps': 'error',\n },\n },\n ],\n})\n\nexport default plugin\n"],"names":["rules"],"mappings":";;AAcA,MAAM,SAAiB;AAAA,EACrB,MAAM;AAAA,IACJ,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC;AAAA,EAAA,OACVA,MAAA;AACF;AAGA,OAAO,OAAO,OAAO,SAAS;AAAA,EAC5B,aAAa;AAAA,IACX,SAAS,CAAC,+BAA+B;AAAA,IACzC,OAAO;AAAA,MACL,mCAAmC;AAAA,MACnC,yCAAyC;AAAA,MACzC,uCAAuC;AAAA,MACvC,oCAAoC;AAAA,IACtC;AAAA,EACF;AAAA,EACA,oBAAoB;AAAA,IAClB;AAAA,MACE,SAAS;AAAA,QACP,mBAAmB;AAAA,MACrB;AAAA,MACA,OAAO;AAAA,QACL,mCAAmC;AAAA,QACnC,yCAAyC;AAAA,QACzC,uCAAuC;AAAA,QACvC,oCAAoC;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACF,CAAC;;"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const utils = require("@typescript-eslint/utils");
|
|
4
|
+
const getDocsUrl = require("../../utils/get-docs-url.cjs");
|
|
5
|
+
const detectReactQueryImports = require("../../utils/detect-react-query-imports.cjs");
|
|
6
|
+
const name = "no-unstable-deps";
|
|
7
|
+
const reactHookNames = ["useEffect", "useCallback", "useMemo"];
|
|
8
|
+
const useQueryHookNames = [
|
|
9
|
+
"useQuery",
|
|
10
|
+
"useSuspenseQuery",
|
|
11
|
+
"useQueries",
|
|
12
|
+
"useSuspenseQueries",
|
|
13
|
+
"useInfiniteQuery",
|
|
14
|
+
"useSuspenseInfiniteQuery"
|
|
15
|
+
];
|
|
16
|
+
const allHookNames = ["useMutation", ...useQueryHookNames];
|
|
17
|
+
const createRule = utils.ESLintUtils.RuleCreator(getDocsUrl.getDocsUrl);
|
|
18
|
+
const rule = createRule({
|
|
19
|
+
name,
|
|
20
|
+
meta: {
|
|
21
|
+
type: "problem",
|
|
22
|
+
docs: {
|
|
23
|
+
description: "Disallow putting the result of useMutation directly in a React hook dependency array",
|
|
24
|
+
recommended: "error"
|
|
25
|
+
},
|
|
26
|
+
messages: {
|
|
27
|
+
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}}.`
|
|
28
|
+
},
|
|
29
|
+
schema: []
|
|
30
|
+
},
|
|
31
|
+
defaultOptions: [],
|
|
32
|
+
create: detectReactQueryImports.detectTanstackQueryImports((context) => {
|
|
33
|
+
const trackedVariables = {};
|
|
34
|
+
const hookAliasMap = {};
|
|
35
|
+
function getReactHook(node) {
|
|
36
|
+
if (node.callee.type === "Identifier") {
|
|
37
|
+
const calleeName = node.callee.name;
|
|
38
|
+
if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) {
|
|
39
|
+
return calleeName;
|
|
40
|
+
}
|
|
41
|
+
} else if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "React" && node.callee.property.type === "Identifier" && reactHookNames.includes(node.callee.property.name)) {
|
|
42
|
+
return node.callee.property.name;
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
function collectVariableNames(pattern, queryHook) {
|
|
47
|
+
if (pattern.type === utils.AST_NODE_TYPES.Identifier) {
|
|
48
|
+
trackedVariables[pattern.name] = queryHook;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ImportDeclaration(node) {
|
|
53
|
+
if (node.specifiers.length > 0 && node.importKind === "value" && node.source.value === "React") {
|
|
54
|
+
node.specifiers.forEach((specifier) => {
|
|
55
|
+
if (specifier.type === utils.AST_NODE_TYPES.ImportSpecifier && reactHookNames.includes(specifier.imported.name)) {
|
|
56
|
+
hookAliasMap[specifier.local.name] = specifier.imported.name;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
VariableDeclarator(node) {
|
|
62
|
+
if (node.init !== null && node.init.type === utils.AST_NODE_TYPES.CallExpression && node.init.callee.type === utils.AST_NODE_TYPES.Identifier && allHookNames.includes(node.init.callee.name)) {
|
|
63
|
+
collectVariableNames(node.id, node.init.callee.name);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
CallExpression: (node) => {
|
|
67
|
+
var _a;
|
|
68
|
+
const reactHook = getReactHook(node);
|
|
69
|
+
if (reactHook !== void 0 && node.arguments.length > 1 && ((_a = node.arguments[1]) == null ? void 0 : _a.type) === utils.AST_NODE_TYPES.ArrayExpression) {
|
|
70
|
+
const depsArray = node.arguments[1].elements;
|
|
71
|
+
depsArray.forEach((dep) => {
|
|
72
|
+
if (dep !== null && dep.type === utils.AST_NODE_TYPES.Identifier && trackedVariables[dep.name] !== void 0) {
|
|
73
|
+
const queryHook = trackedVariables[dep.name];
|
|
74
|
+
context.report({
|
|
75
|
+
node: dep,
|
|
76
|
+
messageId: "noUnstableDeps",
|
|
77
|
+
data: {
|
|
78
|
+
queryHook,
|
|
79
|
+
reactHook
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
exports.name = name;
|
|
90
|
+
exports.reactHookNames = reactHookNames;
|
|
91
|
+
exports.rule = rule;
|
|
92
|
+
exports.useQueryHookNames = useQueryHookNames;
|
|
93
|
+
//# sourceMappingURL=no-unstable-deps.rule.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unstable-deps.rule.cjs","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 useMutation 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":["ESLintUtils","getDocsUrl","detectTanstackQueryImports","AST_NODE_TYPES"],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import { ExtraRuleDocs } from '../../types';
|
|
3
|
+
|
|
4
|
+
export declare const name = "no-unstable-deps";
|
|
5
|
+
export declare const reactHookNames: string[];
|
|
6
|
+
export declare const useQueryHookNames: string[];
|
|
7
|
+
export declare const rule: ESLintUtils.RuleModule<string, readonly unknown[], ExtraRuleDocs, ESLintUtils.RuleListener>;
|
package/dist/cjs/rules.cjs
CHANGED
|
@@ -3,10 +3,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const exhaustiveDeps_rule = require("./rules/exhaustive-deps/exhaustive-deps.rule.cjs");
|
|
4
4
|
const stableQueryClient_rule = require("./rules/stable-query-client/stable-query-client.rule.cjs");
|
|
5
5
|
const noRestDestructuring_rule = require("./rules/no-rest-destructuring/no-rest-destructuring.rule.cjs");
|
|
6
|
+
const noUnstableDeps_rule = require("./rules/no-unstable-deps/no-unstable-deps.rule.cjs");
|
|
6
7
|
const rules = {
|
|
7
8
|
[exhaustiveDeps_rule.name]: exhaustiveDeps_rule.rule,
|
|
8
9
|
[stableQueryClient_rule.name]: stableQueryClient_rule.rule,
|
|
9
|
-
[noRestDestructuring_rule.name]: noRestDestructuring_rule.rule
|
|
10
|
+
[noRestDestructuring_rule.name]: noRestDestructuring_rule.rule,
|
|
11
|
+
[noUnstableDeps_rule.name]: noUnstableDeps_rule.rule
|
|
10
12
|
};
|
|
11
13
|
exports.rules = rules;
|
|
12
14
|
//# sourceMappingURL=rules.cjs.map
|
package/dist/cjs/rules.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.cjs","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 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}\n"],"names":["exhaustiveDeps.name","exhaustiveDeps.rule","stableQueryClient.name","stableQueryClient.rule","noRestDestructuring.name","noRestDestructuring.rule"],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.cjs","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,oBAAmB,IAAA,GAAGC,oBAAe;AAAA,EACtC,CAACC,uBAAsB,IAAA,GAAGC,uBAAkB;AAAA,EAC5C,CAACC,yBAAwB,IAAA,GAAGC,yBAAoB;AAAA,EAChD,CAACC,oBAAmB,IAAA,GAAGC,oBAAe;AACxC;;"}
|
package/dist/esm/index.js
CHANGED
|
@@ -12,7 +12,8 @@ Object.assign(plugin.configs, {
|
|
|
12
12
|
rules: {
|
|
13
13
|
"@tanstack/query/exhaustive-deps": "error",
|
|
14
14
|
"@tanstack/query/no-rest-destructuring": "warn",
|
|
15
|
-
"@tanstack/query/stable-query-client": "error"
|
|
15
|
+
"@tanstack/query/stable-query-client": "error",
|
|
16
|
+
"@tanstack/query/no-unstable-deps": "error"
|
|
16
17
|
}
|
|
17
18
|
},
|
|
18
19
|
"flat/recommended": [
|
|
@@ -23,7 +24,8 @@ Object.assign(plugin.configs, {
|
|
|
23
24
|
rules: {
|
|
24
25
|
"@tanstack/query/exhaustive-deps": "error",
|
|
25
26
|
"@tanstack/query/no-rest-destructuring": "warn",
|
|
26
|
-
"@tanstack/query/stable-query-client": "error"
|
|
27
|
+
"@tanstack/query/stable-query-client": "error",
|
|
28
|
+
"@tanstack/query/no-unstable-deps": "error"
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
]
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/index.ts"],"sourcesContent":["import { rules } from './rules'\nimport type { ESLint, Linter } from 'eslint'\nimport type { RuleModule } from '@typescript-eslint/utils/ts-eslint'\n\ntype RuleKey = keyof typeof rules\n\ninterface Plugin extends Omit<ESLint.Plugin, 'rules'> {\n rules: Record<RuleKey, RuleModule<any, any, any>>\n configs: {\n recommended: ESLint.ConfigData\n 'flat/recommended': Array<Linter.FlatConfig>\n }\n}\n\nconst plugin: Plugin = {\n meta: {\n name: '@tanstack/eslint-plugin-query',\n },\n configs: {} as Plugin['configs'],\n rules,\n}\n\n// Assign configs here so we can reference `plugin`\nObject.assign(plugin.configs, {\n recommended: {\n plugins: ['@tanstack/eslint-plugin-query'],\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n },\n },\n 'flat/recommended': [\n {\n plugins: {\n '@tanstack/query': plugin,\n },\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n },\n },\n ],\n})\n\nexport default plugin\n"],"names":[],"mappings":";AAcA,MAAM,SAAiB;AAAA,EACrB,MAAM;AAAA,IACJ,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC;AAAA,EACV;AACF;AAGA,OAAO,OAAO,OAAO,SAAS;AAAA,EAC5B,aAAa;AAAA,IACX,SAAS,CAAC,+BAA+B;AAAA,IACzC,OAAO;AAAA,MACL,mCAAmC;AAAA,MACnC,yCAAyC;AAAA,MACzC,uCAAuC;AAAA,
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/index.ts"],"sourcesContent":["import { rules } from './rules'\nimport type { ESLint, Linter } from 'eslint'\nimport type { RuleModule } from '@typescript-eslint/utils/ts-eslint'\n\ntype RuleKey = keyof typeof rules\n\ninterface Plugin extends Omit<ESLint.Plugin, 'rules'> {\n rules: Record<RuleKey, RuleModule<any, any, any>>\n configs: {\n recommended: ESLint.ConfigData\n 'flat/recommended': Array<Linter.FlatConfig>\n }\n}\n\nconst plugin: Plugin = {\n meta: {\n name: '@tanstack/eslint-plugin-query',\n },\n configs: {} as Plugin['configs'],\n rules,\n}\n\n// Assign configs here so we can reference `plugin`\nObject.assign(plugin.configs, {\n recommended: {\n plugins: ['@tanstack/eslint-plugin-query'],\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n '@tanstack/query/no-unstable-deps': 'error',\n },\n },\n 'flat/recommended': [\n {\n plugins: {\n '@tanstack/query': plugin,\n },\n rules: {\n '@tanstack/query/exhaustive-deps': 'error',\n '@tanstack/query/no-rest-destructuring': 'warn',\n '@tanstack/query/stable-query-client': 'error',\n '@tanstack/query/no-unstable-deps': 'error',\n },\n },\n ],\n})\n\nexport default plugin\n"],"names":[],"mappings":";AAcA,MAAM,SAAiB;AAAA,EACrB,MAAM;AAAA,IACJ,MAAM;AAAA,EACR;AAAA,EACA,SAAS,CAAC;AAAA,EACV;AACF;AAGA,OAAO,OAAO,OAAO,SAAS;AAAA,EAC5B,aAAa;AAAA,IACX,SAAS,CAAC,+BAA+B;AAAA,IACzC,OAAO;AAAA,MACL,mCAAmC;AAAA,MACnC,yCAAyC;AAAA,MACzC,uCAAuC;AAAA,MACvC,oCAAoC;AAAA,IACtC;AAAA,EACF;AAAA,EACA,oBAAoB;AAAA,IAClB;AAAA,MACE,SAAS;AAAA,QACP,mBAAmB;AAAA,MACrB;AAAA,MACA,OAAO;AAAA,QACL,mCAAmC;AAAA,QACnC,yCAAyC;AAAA,QACzC,uCAAuC;AAAA,QACvC,oCAAoC;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AACF,CAAC;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import { ExtraRuleDocs } from '../../types';
|
|
3
|
+
|
|
4
|
+
export declare const name = "no-unstable-deps";
|
|
5
|
+
export declare const reactHookNames: string[];
|
|
6
|
+
export declare const useQueryHookNames: string[];
|
|
7
|
+
export declare const rule: ESLintUtils.RuleModule<string, readonly unknown[], ExtraRuleDocs, ESLintUtils.RuleListener>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ESLintUtils, AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import { getDocsUrl } from "../../utils/get-docs-url.js";
|
|
3
|
+
import { detectTanstackQueryImports } from "../../utils/detect-react-query-imports.js";
|
|
4
|
+
const name = "no-unstable-deps";
|
|
5
|
+
const reactHookNames = ["useEffect", "useCallback", "useMemo"];
|
|
6
|
+
const useQueryHookNames = [
|
|
7
|
+
"useQuery",
|
|
8
|
+
"useSuspenseQuery",
|
|
9
|
+
"useQueries",
|
|
10
|
+
"useSuspenseQueries",
|
|
11
|
+
"useInfiniteQuery",
|
|
12
|
+
"useSuspenseInfiniteQuery"
|
|
13
|
+
];
|
|
14
|
+
const allHookNames = ["useMutation", ...useQueryHookNames];
|
|
15
|
+
const createRule = ESLintUtils.RuleCreator(getDocsUrl);
|
|
16
|
+
const rule = createRule({
|
|
17
|
+
name,
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Disallow putting the result of useMutation directly in a React hook dependency array",
|
|
22
|
+
recommended: "error"
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
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}}.`
|
|
26
|
+
},
|
|
27
|
+
schema: []
|
|
28
|
+
},
|
|
29
|
+
defaultOptions: [],
|
|
30
|
+
create: detectTanstackQueryImports((context) => {
|
|
31
|
+
const trackedVariables = {};
|
|
32
|
+
const hookAliasMap = {};
|
|
33
|
+
function getReactHook(node) {
|
|
34
|
+
if (node.callee.type === "Identifier") {
|
|
35
|
+
const calleeName = node.callee.name;
|
|
36
|
+
if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) {
|
|
37
|
+
return calleeName;
|
|
38
|
+
}
|
|
39
|
+
} else if (node.callee.type === "MemberExpression" && node.callee.object.type === "Identifier" && node.callee.object.name === "React" && node.callee.property.type === "Identifier" && reactHookNames.includes(node.callee.property.name)) {
|
|
40
|
+
return node.callee.property.name;
|
|
41
|
+
}
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
function collectVariableNames(pattern, queryHook) {
|
|
45
|
+
if (pattern.type === AST_NODE_TYPES.Identifier) {
|
|
46
|
+
trackedVariables[pattern.name] = queryHook;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ImportDeclaration(node) {
|
|
51
|
+
if (node.specifiers.length > 0 && node.importKind === "value" && node.source.value === "React") {
|
|
52
|
+
node.specifiers.forEach((specifier) => {
|
|
53
|
+
if (specifier.type === AST_NODE_TYPES.ImportSpecifier && reactHookNames.includes(specifier.imported.name)) {
|
|
54
|
+
hookAliasMap[specifier.local.name] = specifier.imported.name;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
VariableDeclarator(node) {
|
|
60
|
+
if (node.init !== null && node.init.type === AST_NODE_TYPES.CallExpression && node.init.callee.type === AST_NODE_TYPES.Identifier && allHookNames.includes(node.init.callee.name)) {
|
|
61
|
+
collectVariableNames(node.id, node.init.callee.name);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
CallExpression: (node) => {
|
|
65
|
+
var _a;
|
|
66
|
+
const reactHook = getReactHook(node);
|
|
67
|
+
if (reactHook !== void 0 && node.arguments.length > 1 && ((_a = node.arguments[1]) == null ? void 0 : _a.type) === AST_NODE_TYPES.ArrayExpression) {
|
|
68
|
+
const depsArray = node.arguments[1].elements;
|
|
69
|
+
depsArray.forEach((dep) => {
|
|
70
|
+
if (dep !== null && dep.type === AST_NODE_TYPES.Identifier && trackedVariables[dep.name] !== void 0) {
|
|
71
|
+
const queryHook = trackedVariables[dep.name];
|
|
72
|
+
context.report({
|
|
73
|
+
node: dep,
|
|
74
|
+
messageId: "noUnstableDeps",
|
|
75
|
+
data: {
|
|
76
|
+
queryHook,
|
|
77
|
+
reactHook
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
export {
|
|
88
|
+
name,
|
|
89
|
+
reactHookNames,
|
|
90
|
+
rule,
|
|
91
|
+
useQueryHookNames
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=no-unstable-deps.rule.js.map
|
|
@@ -0,0 +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 useMutation 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;"}
|
package/dist/esm/rules.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
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
|
+
import { name as name$3, rule as rule$3 } from "./rules/no-unstable-deps/no-unstable-deps.rule.js";
|
|
4
5
|
const rules = {
|
|
5
6
|
[name]: rule,
|
|
6
7
|
[name$1]: rule$1,
|
|
7
|
-
[name$2]: rule$2
|
|
8
|
+
[name$2]: rule$2,
|
|
9
|
+
[name$3]: rule$3
|
|
8
10
|
};
|
|
9
11
|
export {
|
|
10
12
|
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 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}\n"],"names":["exhaustiveDeps.name","exhaustiveDeps.rule","stableQueryClient.name","stableQueryClient.rule","noRestDestructuring.name","noRestDestructuring.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 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;"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester'
|
|
2
|
+
import {
|
|
3
|
+
reactHookNames,
|
|
4
|
+
rule,
|
|
5
|
+
useQueryHookNames,
|
|
6
|
+
} from '../rules/no-unstable-deps/no-unstable-deps.rule'
|
|
7
|
+
|
|
8
|
+
const ruleTester = new RuleTester({
|
|
9
|
+
parser: '@typescript-eslint/parser',
|
|
10
|
+
settings: {},
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
interface TestCase {
|
|
14
|
+
reactHookImport: string
|
|
15
|
+
reactHookInvocation: string
|
|
16
|
+
reactHookAlias: string
|
|
17
|
+
}
|
|
18
|
+
const baseTestCases = {
|
|
19
|
+
valid: ({ reactHookImport, reactHookInvocation, reactHookAlias }: TestCase) =>
|
|
20
|
+
[
|
|
21
|
+
{
|
|
22
|
+
name: `should pass when destructured mutate is passed to ${reactHookAlias} as dependency`,
|
|
23
|
+
code: `
|
|
24
|
+
${reactHookImport}
|
|
25
|
+
import { useMutation } from "@tanstack/react-query";
|
|
26
|
+
|
|
27
|
+
function Component() {
|
|
28
|
+
const { mutate } = useMutation({ mutationFn: (value: string) => value });
|
|
29
|
+
const callback = ${reactHookInvocation}(() => { mutate('hello') }, [mutate]);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
`,
|
|
33
|
+
},
|
|
34
|
+
].concat(
|
|
35
|
+
useQueryHookNames.map((queryHook) => ({
|
|
36
|
+
name: `should pass result of ${queryHook} is passed to ${reactHookInvocation} as dependency`,
|
|
37
|
+
code: `
|
|
38
|
+
${reactHookImport}
|
|
39
|
+
import { ${queryHook} } from "@tanstack/react-query";
|
|
40
|
+
|
|
41
|
+
function Component() {
|
|
42
|
+
const { refetch } = ${queryHook}({ queryFn: (value: string) => value });
|
|
43
|
+
const callback = ${reactHookInvocation}(() => { query.refetch() }, [refetch]);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
})),
|
|
48
|
+
),
|
|
49
|
+
invalid: ({
|
|
50
|
+
reactHookImport,
|
|
51
|
+
reactHookInvocation,
|
|
52
|
+
reactHookAlias,
|
|
53
|
+
}: TestCase) =>
|
|
54
|
+
[
|
|
55
|
+
{
|
|
56
|
+
name: `result of useMutation is passed to ${reactHookInvocation} as dependency `,
|
|
57
|
+
code: `
|
|
58
|
+
${reactHookImport}
|
|
59
|
+
import { useMutation } from "@tanstack/react-query";
|
|
60
|
+
|
|
61
|
+
function Component() {
|
|
62
|
+
const mutation = useMutation({ mutationFn: (value: string) => value });
|
|
63
|
+
const callback = ${reactHookInvocation}(() => { mutation.mutate('hello') }, [mutation]);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
`,
|
|
67
|
+
errors: [
|
|
68
|
+
{
|
|
69
|
+
messageId: 'noUnstableDeps',
|
|
70
|
+
data: { reactHook: reactHookAlias, queryHook: 'useMutation' },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
].concat(
|
|
75
|
+
useQueryHookNames.map((queryHook) => ({
|
|
76
|
+
name: `result of ${queryHook} is passed to ${reactHookInvocation} as dependency`,
|
|
77
|
+
code: `
|
|
78
|
+
${reactHookImport}
|
|
79
|
+
import { ${queryHook} } from "@tanstack/react-query";
|
|
80
|
+
|
|
81
|
+
function Component() {
|
|
82
|
+
const query = ${queryHook}({ queryFn: (value: string) => value });
|
|
83
|
+
const callback = ${reactHookInvocation}(() => { query.refetch() }, [query]);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
`,
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
messageId: 'noUnstableDeps',
|
|
90
|
+
data: { reactHook: reactHookAlias, queryHook },
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
})),
|
|
94
|
+
),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const testCases = (reactHookName: string) => [
|
|
98
|
+
{
|
|
99
|
+
reactHookImport: 'import * as React from "React";',
|
|
100
|
+
reactHookInvocation: `React.${reactHookName}`,
|
|
101
|
+
reactHookAlias: reactHookName,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
reactHookImport: `import { ${reactHookName} } from "React";`,
|
|
105
|
+
reactHookInvocation: reactHookName,
|
|
106
|
+
reactHookAlias: reactHookName,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
reactHookImport: `import { ${reactHookName} as useAlias } from "React";`,
|
|
110
|
+
reactHookInvocation: 'useAlias',
|
|
111
|
+
reactHookAlias: 'useAlias',
|
|
112
|
+
},
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
reactHookNames.forEach((reactHookName) => {
|
|
116
|
+
testCases(reactHookName).forEach(
|
|
117
|
+
({ reactHookInvocation, reactHookAlias, reactHookImport }) => {
|
|
118
|
+
ruleTester.run('no-unstable-deps', rule, {
|
|
119
|
+
valid: baseTestCases.valid({
|
|
120
|
+
reactHookImport,
|
|
121
|
+
reactHookInvocation,
|
|
122
|
+
reactHookAlias,
|
|
123
|
+
}),
|
|
124
|
+
invalid: baseTestCases.invalid({
|
|
125
|
+
reactHookImport,
|
|
126
|
+
reactHookInvocation,
|
|
127
|
+
reactHookAlias,
|
|
128
|
+
}),
|
|
129
|
+
})
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ Object.assign(plugin.configs, {
|
|
|
28
28
|
'@tanstack/query/exhaustive-deps': 'error',
|
|
29
29
|
'@tanstack/query/no-rest-destructuring': 'warn',
|
|
30
30
|
'@tanstack/query/stable-query-client': 'error',
|
|
31
|
+
'@tanstack/query/no-unstable-deps': 'error',
|
|
31
32
|
},
|
|
32
33
|
},
|
|
33
34
|
'flat/recommended': [
|
|
@@ -39,6 +40,7 @@ Object.assign(plugin.configs, {
|
|
|
39
40
|
'@tanstack/query/exhaustive-deps': 'error',
|
|
40
41
|
'@tanstack/query/no-rest-destructuring': 'warn',
|
|
41
42
|
'@tanstack/query/stable-query-client': 'error',
|
|
43
|
+
'@tanstack/query/no-unstable-deps': 'error',
|
|
42
44
|
},
|
|
43
45
|
},
|
|
44
46
|
],
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
|
|
2
|
+
import { getDocsUrl } from '../../utils/get-docs-url'
|
|
3
|
+
import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
|
|
4
|
+
import type { TSESTree } from '@typescript-eslint/utils'
|
|
5
|
+
import type { ExtraRuleDocs } from '../../types'
|
|
6
|
+
|
|
7
|
+
export const name = 'no-unstable-deps'
|
|
8
|
+
|
|
9
|
+
export const reactHookNames = ['useEffect', 'useCallback', 'useMemo']
|
|
10
|
+
export const useQueryHookNames = [
|
|
11
|
+
'useQuery',
|
|
12
|
+
'useSuspenseQuery',
|
|
13
|
+
'useQueries',
|
|
14
|
+
'useSuspenseQueries',
|
|
15
|
+
'useInfiniteQuery',
|
|
16
|
+
'useSuspenseInfiniteQuery',
|
|
17
|
+
]
|
|
18
|
+
const allHookNames = ['useMutation', ...useQueryHookNames]
|
|
19
|
+
const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
|
|
20
|
+
|
|
21
|
+
export const rule = createRule({
|
|
22
|
+
name,
|
|
23
|
+
meta: {
|
|
24
|
+
type: 'problem',
|
|
25
|
+
docs: {
|
|
26
|
+
description:
|
|
27
|
+
'Disallow putting the result of useMutation directly in a React hook dependency array',
|
|
28
|
+
recommended: 'error',
|
|
29
|
+
},
|
|
30
|
+
messages: {
|
|
31
|
+
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}}.`,
|
|
32
|
+
},
|
|
33
|
+
schema: [],
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
|
|
37
|
+
create: detectTanstackQueryImports((context) => {
|
|
38
|
+
const trackedVariables: Record<string, string> = {}
|
|
39
|
+
const hookAliasMap: Record<string, string> = {}
|
|
40
|
+
|
|
41
|
+
function getReactHook(node: TSESTree.CallExpression): string | undefined {
|
|
42
|
+
if (node.callee.type === 'Identifier') {
|
|
43
|
+
const calleeName = node.callee.name
|
|
44
|
+
// Check if the identifier is a known React hook or an alias
|
|
45
|
+
if (reactHookNames.includes(calleeName) || calleeName in hookAliasMap) {
|
|
46
|
+
return calleeName
|
|
47
|
+
}
|
|
48
|
+
} else if (
|
|
49
|
+
node.callee.type === 'MemberExpression' &&
|
|
50
|
+
node.callee.object.type === 'Identifier' &&
|
|
51
|
+
node.callee.object.name === 'React' &&
|
|
52
|
+
node.callee.property.type === 'Identifier' &&
|
|
53
|
+
reactHookNames.includes(node.callee.property.name)
|
|
54
|
+
) {
|
|
55
|
+
// Member expression case: `React.useCallback`
|
|
56
|
+
return node.callee.property.name
|
|
57
|
+
}
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function collectVariableNames(
|
|
62
|
+
pattern: TSESTree.BindingName,
|
|
63
|
+
queryHook: string,
|
|
64
|
+
) {
|
|
65
|
+
if (pattern.type === AST_NODE_TYPES.Identifier) {
|
|
66
|
+
trackedVariables[pattern.name] = queryHook
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ImportDeclaration(node: TSESTree.ImportDeclaration) {
|
|
72
|
+
if (
|
|
73
|
+
node.specifiers.length > 0 &&
|
|
74
|
+
node.importKind === 'value' &&
|
|
75
|
+
node.source.value === 'React'
|
|
76
|
+
) {
|
|
77
|
+
node.specifiers.forEach((specifier) => {
|
|
78
|
+
if (
|
|
79
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
80
|
+
reactHookNames.includes(specifier.imported.name)
|
|
81
|
+
) {
|
|
82
|
+
// Track alias or direct import
|
|
83
|
+
hookAliasMap[specifier.local.name] = specifier.imported.name
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
VariableDeclarator(node) {
|
|
90
|
+
if (
|
|
91
|
+
node.init !== null &&
|
|
92
|
+
node.init.type === AST_NODE_TYPES.CallExpression &&
|
|
93
|
+
node.init.callee.type === AST_NODE_TYPES.Identifier &&
|
|
94
|
+
allHookNames.includes(node.init.callee.name)
|
|
95
|
+
) {
|
|
96
|
+
collectVariableNames(node.id, node.init.callee.name)
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
CallExpression: (node) => {
|
|
100
|
+
const reactHook = getReactHook(node)
|
|
101
|
+
if (
|
|
102
|
+
reactHook !== undefined &&
|
|
103
|
+
node.arguments.length > 1 &&
|
|
104
|
+
node.arguments[1]?.type === AST_NODE_TYPES.ArrayExpression
|
|
105
|
+
) {
|
|
106
|
+
const depsArray = node.arguments[1].elements
|
|
107
|
+
depsArray.forEach((dep) => {
|
|
108
|
+
if (
|
|
109
|
+
dep !== null &&
|
|
110
|
+
dep.type === AST_NODE_TYPES.Identifier &&
|
|
111
|
+
trackedVariables[dep.name] !== undefined
|
|
112
|
+
) {
|
|
113
|
+
const queryHook = trackedVariables[dep.name]
|
|
114
|
+
context.report({
|
|
115
|
+
node: dep,
|
|
116
|
+
messageId: 'noUnstableDeps',
|
|
117
|
+
data: {
|
|
118
|
+
queryHook,
|
|
119
|
+
reactHook,
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
}),
|
|
128
|
+
})
|
package/src/rules.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as exhaustiveDeps from './rules/exhaustive-deps/exhaustive-deps.rule'
|
|
2
2
|
import * as stableQueryClient from './rules/stable-query-client/stable-query-client.rule'
|
|
3
3
|
import * as noRestDestructuring from './rules/no-rest-destructuring/no-rest-destructuring.rule'
|
|
4
|
+
import * as noUnstableDeps from './rules/no-unstable-deps/no-unstable-deps.rule'
|
|
4
5
|
import type { ESLintUtils } from '@typescript-eslint/utils'
|
|
5
6
|
import type { ExtraRuleDocs } from './types'
|
|
6
7
|
|
|
@@ -16,4 +17,5 @@ export const rules: Record<
|
|
|
16
17
|
[exhaustiveDeps.name]: exhaustiveDeps.rule,
|
|
17
18
|
[stableQueryClient.name]: stableQueryClient.rule,
|
|
18
19
|
[noRestDestructuring.name]: noRestDestructuring.rule,
|
|
20
|
+
[noUnstableDeps.name]: noUnstableDeps.rule,
|
|
19
21
|
}
|