@tanstack/eslint-plugin-query 5.51.1 → 5.51.7
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/rules/exhaustive-deps/exhaustive-deps.rule.cjs +13 -3
- package/dist/cjs/rules/exhaustive-deps/exhaustive-deps.rule.cjs.map +1 -1
- package/dist/esm/rules/exhaustive-deps/exhaustive-deps.rule.js +13 -3
- package/dist/esm/rules/exhaustive-deps/exhaustive-deps.rule.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/exhaustive-deps.test.ts +34 -0
- package/src/rules/exhaustive-deps/exhaustive-deps.rule.ts +19 -3
|
@@ -44,7 +44,8 @@ const rule = createRule({
|
|
|
44
44
|
);
|
|
45
45
|
if (scopeManager === null || queryKey === void 0 || queryFn === void 0 || !astUtils.ASTUtils.isNodeOfOneOf(queryFn.value, [
|
|
46
46
|
utils.AST_NODE_TYPES.ArrowFunctionExpression,
|
|
47
|
-
utils.AST_NODE_TYPES.FunctionExpression
|
|
47
|
+
utils.AST_NODE_TYPES.FunctionExpression,
|
|
48
|
+
utils.AST_NODE_TYPES.ConditionalExpression
|
|
48
49
|
])) {
|
|
49
50
|
return;
|
|
50
51
|
}
|
|
@@ -65,14 +66,14 @@ const rule = createRule({
|
|
|
65
66
|
const externalRefs = astUtils.ASTUtils.getExternalRefs({
|
|
66
67
|
scopeManager,
|
|
67
68
|
sourceCode: context.sourceCode,
|
|
68
|
-
node: queryFn
|
|
69
|
+
node: getQueryFnRelevantNode(queryFn)
|
|
69
70
|
});
|
|
70
71
|
const relevantRefs = externalRefs.filter(
|
|
71
72
|
(reference) => exhaustiveDeps_utils.ExhaustiveDepsUtils.isRelevantReference({
|
|
72
73
|
sourceCode: context.sourceCode,
|
|
73
74
|
reference,
|
|
74
75
|
scopeManager,
|
|
75
|
-
node: queryFn
|
|
76
|
+
node: getQueryFnRelevantNode(queryFn)
|
|
76
77
|
})
|
|
77
78
|
);
|
|
78
79
|
const existingKeys = astUtils.ASTUtils.getNestedIdentifiers(queryKeyValue).map(
|
|
@@ -116,6 +117,15 @@ const rule = createRule({
|
|
|
116
117
|
};
|
|
117
118
|
})
|
|
118
119
|
});
|
|
120
|
+
function getQueryFnRelevantNode(queryFn) {
|
|
121
|
+
if (queryFn.value.type !== utils.AST_NODE_TYPES.ConditionalExpression) {
|
|
122
|
+
return queryFn.value;
|
|
123
|
+
}
|
|
124
|
+
if (queryFn.value.consequent.type === utils.AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === "skipToken") {
|
|
125
|
+
return queryFn.value.alternate;
|
|
126
|
+
}
|
|
127
|
+
return queryFn.value.consequent;
|
|
128
|
+
}
|
|
119
129
|
exports.name = name;
|
|
120
130
|
exports.rule = rule;
|
|
121
131
|
//# sourceMappingURL=exhaustive-deps.rule.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exhaustive-deps.rule.cjs","sources":["../../../../src/rules/exhaustive-deps/exhaustive-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { ASTUtils } from '../../utils/ast-utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { uniqueBy } from '../../utils/unique-by'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport { ExhaustiveDepsUtils } from './exhaustive-deps.utils'\nimport type { TSESLint } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nconst QUERY_KEY = 'queryKey'\nconst QUERY_FN = 'queryFn'\n\nexport const name = 'exhaustive-deps'\n\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description: 'Exhaustive deps rule for useQuery',\n recommended: 'error',\n },\n messages: {\n missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`,\n fixTo: 'Fix to {{result}}',\n },\n hasSuggestions: true,\n fixable: 'code',\n schema: [],\n },\n defaultOptions: [],\n\n create: detectTanstackQueryImports((context) => {\n return {\n Property: (node) => {\n if (\n !ASTUtils.isObjectExpression(node.parent) ||\n !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)\n ) {\n return\n }\n\n const scopeManager = context.sourceCode.scopeManager\n const queryKey = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_KEY,\n )\n const queryFn = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_FN,\n )\n\n if (\n scopeManager === null ||\n queryKey === undefined ||\n queryFn === undefined ||\n !ASTUtils.isNodeOfOneOf(queryFn.value, [\n AST_NODE_TYPES.ArrowFunctionExpression,\n AST_NODE_TYPES.FunctionExpression,\n ])\n ) {\n return\n }\n\n let queryKeyNode = queryKey.value\n\n if (\n queryKeyNode.type === AST_NODE_TYPES.TSAsExpression &&\n queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression\n ) {\n queryKeyNode = queryKeyNode.expression\n }\n\n if (queryKeyNode.type === AST_NODE_TYPES.Identifier) {\n const expression = ASTUtils.getReferencedExpressionByIdentifier({\n context,\n node: queryKeyNode,\n })\n\n if (expression?.type === AST_NODE_TYPES.ArrayExpression) {\n queryKeyNode = expression\n }\n }\n\n const queryKeyValue = queryKeyNode\n const externalRefs = ASTUtils.getExternalRefs({\n scopeManager,\n sourceCode: context.sourceCode,\n node: queryFn
|
|
1
|
+
{"version":3,"file":"exhaustive-deps.rule.cjs","sources":["../../../../src/rules/exhaustive-deps/exhaustive-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { ASTUtils } from '../../utils/ast-utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { uniqueBy } from '../../utils/unique-by'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport { ExhaustiveDepsUtils } from './exhaustive-deps.utils'\nimport type { TSESLint, TSESTree } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nconst QUERY_KEY = 'queryKey'\nconst QUERY_FN = 'queryFn'\n\nexport const name = 'exhaustive-deps'\n\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description: 'Exhaustive deps rule for useQuery',\n recommended: 'error',\n },\n messages: {\n missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`,\n fixTo: 'Fix to {{result}}',\n },\n hasSuggestions: true,\n fixable: 'code',\n schema: [],\n },\n defaultOptions: [],\n\n create: detectTanstackQueryImports((context) => {\n return {\n Property: (node) => {\n if (\n !ASTUtils.isObjectExpression(node.parent) ||\n !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)\n ) {\n return\n }\n\n const scopeManager = context.sourceCode.scopeManager\n const queryKey = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_KEY,\n )\n const queryFn = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_FN,\n )\n\n if (\n scopeManager === null ||\n queryKey === undefined ||\n queryFn === undefined ||\n !ASTUtils.isNodeOfOneOf(queryFn.value, [\n AST_NODE_TYPES.ArrowFunctionExpression,\n AST_NODE_TYPES.FunctionExpression,\n AST_NODE_TYPES.ConditionalExpression,\n ])\n ) {\n return\n }\n\n let queryKeyNode = queryKey.value\n\n if (\n queryKeyNode.type === AST_NODE_TYPES.TSAsExpression &&\n queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression\n ) {\n queryKeyNode = queryKeyNode.expression\n }\n\n if (queryKeyNode.type === AST_NODE_TYPES.Identifier) {\n const expression = ASTUtils.getReferencedExpressionByIdentifier({\n context,\n node: queryKeyNode,\n })\n\n if (expression?.type === AST_NODE_TYPES.ArrayExpression) {\n queryKeyNode = expression\n }\n }\n\n const queryKeyValue = queryKeyNode\n const externalRefs = ASTUtils.getExternalRefs({\n scopeManager,\n sourceCode: context.sourceCode,\n node: getQueryFnRelevantNode(queryFn),\n })\n\n const relevantRefs = externalRefs.filter((reference) =>\n ExhaustiveDepsUtils.isRelevantReference({\n sourceCode: context.sourceCode,\n reference,\n scopeManager,\n node: getQueryFnRelevantNode(queryFn),\n }),\n )\n\n const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyValue).map(\n (identifier) =>\n ASTUtils.mapKeyNodeToText(identifier, context.sourceCode),\n )\n\n const missingRefs = relevantRefs\n .map((ref) => ({\n ref: ref,\n text: ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode),\n }))\n .filter(({ ref, text }) => {\n return (\n !ref.isTypeReference &&\n !ASTUtils.isAncestorIsCallee(ref.identifier) &&\n !existingKeys.some((existingKey) => existingKey === text) &&\n !existingKeys.includes(text.split('.')[0] ?? '')\n )\n })\n .map(({ ref, text }) => ({\n identifier: ref.identifier,\n text: text,\n }))\n\n const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text)\n\n if (uniqueMissingRefs.length > 0) {\n const missingAsText = uniqueMissingRefs\n .map((ref) =>\n ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode),\n )\n .join(', ')\n\n const existingWithMissing = context.sourceCode\n .getText(queryKeyValue)\n .replace(/\\]$/, `, ${missingAsText}]`)\n\n const suggestions: TSESLint.ReportSuggestionArray<string> = []\n\n if (queryKeyNode.type === AST_NODE_TYPES.ArrayExpression) {\n suggestions.push({\n messageId: 'fixTo',\n data: { result: existingWithMissing },\n fix(fixer) {\n return fixer.replaceText(queryKeyValue, existingWithMissing)\n },\n })\n }\n\n context.report({\n node: node,\n messageId: 'missingDeps',\n data: {\n deps: uniqueMissingRefs.map((ref) => ref.text).join(', '),\n },\n suggest: suggestions,\n })\n }\n },\n }\n }),\n})\n\nfunction getQueryFnRelevantNode(queryFn: TSESTree.Property) {\n if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {\n return queryFn.value\n }\n\n if (\n queryFn.value.consequent.type === AST_NODE_TYPES.Identifier &&\n queryFn.value.consequent.name === 'skipToken'\n ) {\n return queryFn.value.alternate\n }\n\n return queryFn.value.consequent\n}\n"],"names":["ESLintUtils","getDocsUrl","detectTanstackQueryImports","ASTUtils","AST_NODE_TYPES","ExhaustiveDepsUtils","uniqueBy"],"mappings":";;;;;;;;AASA,MAAM,YAAY;AAClB,MAAM,WAAW;AAEV,MAAM,OAAO;AAEpB,MAAM,aAAaA,MAAY,YAAA,YAA2BC,WAAAA,UAAU;AAE7D,MAAM,OAAO,WAAW;AAAA,EAC7B;AAAA,EACA,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,MACJ,aAAa;AAAA,MACb,aAAa;AAAA,IACf;AAAA,IACA,UAAU;AAAA,MACR,aAAa;AAAA,MACb,OAAO;AAAA,IACT;AAAA,IACA,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,QAAQ,CAAC;AAAA,EACX;AAAA,EACA,gBAAgB,CAAC;AAAA,EAEjB,QAAQC,wBAAAA,2BAA2B,CAAC,YAAY;AACvC,WAAA;AAAA,MACL,UAAU,CAAC,SAAS;AAClB,YACE,CAACC,SAAA,SAAS,mBAAmB,KAAK,MAAM,KACxC,CAACA,SAAA,SAAS,qBAAqB,KAAK,KAAK,SAAS,GAClD;AACA;AAAA,QACF;AAEM,cAAA,eAAe,QAAQ,WAAW;AACxC,cAAM,WAAWA,SAAAA,SAAS;AAAA,UACxB,KAAK,OAAO;AAAA,UACZ;AAAA,QAAA;AAEF,cAAM,UAAUA,SAAAA,SAAS;AAAA,UACvB,KAAK,OAAO;AAAA,UACZ;AAAA,QAAA;AAIA,YAAA,iBAAiB,QACjB,aAAa,UACb,YAAY,UACZ,CAACA,SAAA,SAAS,cAAc,QAAQ,OAAO;AAAA,UACrCC,MAAAA,eAAe;AAAA,UACfA,MAAAA,eAAe;AAAA,UACfA,MAAAA,eAAe;AAAA,QAAA,CAChB,GACD;AACA;AAAA,QACF;AAEA,YAAI,eAAe,SAAS;AAG1B,YAAA,aAAa,SAASA,MAAAA,eAAe,kBACrC,aAAa,WAAW,SAASA,qBAAe,iBAChD;AACA,yBAAe,aAAa;AAAA,QAC9B;AAEI,YAAA,aAAa,SAASA,MAAA,eAAe,YAAY;AAC7C,gBAAA,aAAaD,kBAAS,oCAAoC;AAAA,YAC9D;AAAA,YACA,MAAM;AAAA,UAAA,CACP;AAEG,eAAA,yCAAY,UAASC,MAAA,eAAe,iBAAiB;AACxC,2BAAA;AAAA,UACjB;AAAA,QACF;AAEA,cAAM,gBAAgB;AAChB,cAAA,eAAeD,kBAAS,gBAAgB;AAAA,UAC5C;AAAA,UACA,YAAY,QAAQ;AAAA,UACpB,MAAM,uBAAuB,OAAO;AAAA,QAAA,CACrC;AAED,cAAM,eAAe,aAAa;AAAA,UAAO,CAAC,cACxCE,qBAAA,oBAAoB,oBAAoB;AAAA,YACtC,YAAY,QAAQ;AAAA,YACpB;AAAA,YACA;AAAA,YACA,MAAM,uBAAuB,OAAO;AAAA,UAAA,CACrC;AAAA,QAAA;AAGH,cAAM,eAAeF,SAAA,SAAS,qBAAqB,aAAa,EAAE;AAAA,UAChE,CAAC,eACCA,SAAAA,SAAS,iBAAiB,YAAY,QAAQ,UAAU;AAAA,QAAA;AAG5D,cAAM,cAAc,aACjB,IAAI,CAAC,SAAS;AAAA,UACb;AAAA,UACA,MAAMA,SAAS,SAAA,iBAAiB,IAAI,YAAY,QAAQ,UAAU;AAAA,UAClE,EACD,OAAO,CAAC,EAAE,KAAK,WAAW;AAEvB,iBAAA,CAAC,IAAI,mBACL,CAACA,kBAAS,mBAAmB,IAAI,UAAU,KAC3C,CAAC,aAAa,KAAK,CAAC,gBAAgB,gBAAgB,IAAI,KACxD,CAAC,aAAa,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE;AAAA,QAElD,CAAA,EACA,IAAI,CAAC,EAAE,KAAK,YAAY;AAAA,UACvB,YAAY,IAAI;AAAA,UAChB;AAAA,QACA,EAAA;AAEJ,cAAM,oBAAoBG,SAAAA,SAAS,aAAa,CAAC,MAAM,EAAE,IAAI;AAEzD,YAAA,kBAAkB,SAAS,GAAG;AAChC,gBAAM,gBAAgB,kBACnB;AAAA,YAAI,CAAC,QACJH,SAAAA,SAAS,iBAAiB,IAAI,YAAY,QAAQ,UAAU;AAAA,UAAA,EAE7D,KAAK,IAAI;AAEN,gBAAA,sBAAsB,QAAQ,WACjC,QAAQ,aAAa,EACrB,QAAQ,OAAO,KAAK,aAAa,GAAG;AAEvC,gBAAM,cAAsD,CAAA;AAExD,cAAA,aAAa,SAASC,MAAA,eAAe,iBAAiB;AACxD,wBAAY,KAAK;AAAA,cACf,WAAW;AAAA,cACX,MAAM,EAAE,QAAQ,oBAAoB;AAAA,cACpC,IAAI,OAAO;AACF,uBAAA,MAAM,YAAY,eAAe,mBAAmB;AAAA,cAC7D;AAAA,YAAA,CACD;AAAA,UACH;AAEA,kBAAQ,OAAO;AAAA,YACb;AAAA,YACA,WAAW;AAAA,YACX,MAAM;AAAA,cACJ,MAAM,kBAAkB,IAAI,CAAC,QAAQ,IAAI,IAAI,EAAE,KAAK,IAAI;AAAA,YAC1D;AAAA,YACA,SAAS;AAAA,UAAA,CACV;AAAA,QACH;AAAA,MACF;AAAA,IAAA;AAAA,EACF,CACD;AACH,CAAC;AAED,SAAS,uBAAuB,SAA4B;AAC1D,MAAI,QAAQ,MAAM,SAASA,MAAAA,eAAe,uBAAuB;AAC/D,WAAO,QAAQ;AAAA,EACjB;AAGE,MAAA,QAAQ,MAAM,WAAW,SAASA,MAAA,eAAe,cACjD,QAAQ,MAAM,WAAW,SAAS,aAClC;AACA,WAAO,QAAQ,MAAM;AAAA,EACvB;AAEA,SAAO,QAAQ,MAAM;AACvB;;;"}
|
|
@@ -42,7 +42,8 @@ const rule = createRule({
|
|
|
42
42
|
);
|
|
43
43
|
if (scopeManager === null || queryKey === void 0 || queryFn === void 0 || !ASTUtils.isNodeOfOneOf(queryFn.value, [
|
|
44
44
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
45
|
-
AST_NODE_TYPES.FunctionExpression
|
|
45
|
+
AST_NODE_TYPES.FunctionExpression,
|
|
46
|
+
AST_NODE_TYPES.ConditionalExpression
|
|
46
47
|
])) {
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
@@ -63,14 +64,14 @@ const rule = createRule({
|
|
|
63
64
|
const externalRefs = ASTUtils.getExternalRefs({
|
|
64
65
|
scopeManager,
|
|
65
66
|
sourceCode: context.sourceCode,
|
|
66
|
-
node: queryFn
|
|
67
|
+
node: getQueryFnRelevantNode(queryFn)
|
|
67
68
|
});
|
|
68
69
|
const relevantRefs = externalRefs.filter(
|
|
69
70
|
(reference) => ExhaustiveDepsUtils.isRelevantReference({
|
|
70
71
|
sourceCode: context.sourceCode,
|
|
71
72
|
reference,
|
|
72
73
|
scopeManager,
|
|
73
|
-
node: queryFn
|
|
74
|
+
node: getQueryFnRelevantNode(queryFn)
|
|
74
75
|
})
|
|
75
76
|
);
|
|
76
77
|
const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyValue).map(
|
|
@@ -114,6 +115,15 @@ const rule = createRule({
|
|
|
114
115
|
};
|
|
115
116
|
})
|
|
116
117
|
});
|
|
118
|
+
function getQueryFnRelevantNode(queryFn) {
|
|
119
|
+
if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {
|
|
120
|
+
return queryFn.value;
|
|
121
|
+
}
|
|
122
|
+
if (queryFn.value.consequent.type === AST_NODE_TYPES.Identifier && queryFn.value.consequent.name === "skipToken") {
|
|
123
|
+
return queryFn.value.alternate;
|
|
124
|
+
}
|
|
125
|
+
return queryFn.value.consequent;
|
|
126
|
+
}
|
|
117
127
|
export {
|
|
118
128
|
name,
|
|
119
129
|
rule
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exhaustive-deps.rule.js","sources":["../../../../src/rules/exhaustive-deps/exhaustive-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { ASTUtils } from '../../utils/ast-utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { uniqueBy } from '../../utils/unique-by'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport { ExhaustiveDepsUtils } from './exhaustive-deps.utils'\nimport type { TSESLint } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nconst QUERY_KEY = 'queryKey'\nconst QUERY_FN = 'queryFn'\n\nexport const name = 'exhaustive-deps'\n\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description: 'Exhaustive deps rule for useQuery',\n recommended: 'error',\n },\n messages: {\n missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`,\n fixTo: 'Fix to {{result}}',\n },\n hasSuggestions: true,\n fixable: 'code',\n schema: [],\n },\n defaultOptions: [],\n\n create: detectTanstackQueryImports((context) => {\n return {\n Property: (node) => {\n if (\n !ASTUtils.isObjectExpression(node.parent) ||\n !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)\n ) {\n return\n }\n\n const scopeManager = context.sourceCode.scopeManager\n const queryKey = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_KEY,\n )\n const queryFn = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_FN,\n )\n\n if (\n scopeManager === null ||\n queryKey === undefined ||\n queryFn === undefined ||\n !ASTUtils.isNodeOfOneOf(queryFn.value, [\n AST_NODE_TYPES.ArrowFunctionExpression,\n AST_NODE_TYPES.FunctionExpression,\n ])\n ) {\n return\n }\n\n let queryKeyNode = queryKey.value\n\n if (\n queryKeyNode.type === AST_NODE_TYPES.TSAsExpression &&\n queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression\n ) {\n queryKeyNode = queryKeyNode.expression\n }\n\n if (queryKeyNode.type === AST_NODE_TYPES.Identifier) {\n const expression = ASTUtils.getReferencedExpressionByIdentifier({\n context,\n node: queryKeyNode,\n })\n\n if (expression?.type === AST_NODE_TYPES.ArrayExpression) {\n queryKeyNode = expression\n }\n }\n\n const queryKeyValue = queryKeyNode\n const externalRefs = ASTUtils.getExternalRefs({\n scopeManager,\n sourceCode: context.sourceCode,\n node: queryFn
|
|
1
|
+
{"version":3,"file":"exhaustive-deps.rule.js","sources":["../../../../src/rules/exhaustive-deps/exhaustive-deps.rule.ts"],"sourcesContent":["import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'\nimport { ASTUtils } from '../../utils/ast-utils'\nimport { getDocsUrl } from '../../utils/get-docs-url'\nimport { uniqueBy } from '../../utils/unique-by'\nimport { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'\nimport { ExhaustiveDepsUtils } from './exhaustive-deps.utils'\nimport type { TSESLint, TSESTree } from '@typescript-eslint/utils'\nimport type { ExtraRuleDocs } from '../../types'\n\nconst QUERY_KEY = 'queryKey'\nconst QUERY_FN = 'queryFn'\n\nexport const name = 'exhaustive-deps'\n\nconst createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)\n\nexport const rule = createRule({\n name,\n meta: {\n type: 'problem',\n docs: {\n description: 'Exhaustive deps rule for useQuery',\n recommended: 'error',\n },\n messages: {\n missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`,\n fixTo: 'Fix to {{result}}',\n },\n hasSuggestions: true,\n fixable: 'code',\n schema: [],\n },\n defaultOptions: [],\n\n create: detectTanstackQueryImports((context) => {\n return {\n Property: (node) => {\n if (\n !ASTUtils.isObjectExpression(node.parent) ||\n !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)\n ) {\n return\n }\n\n const scopeManager = context.sourceCode.scopeManager\n const queryKey = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_KEY,\n )\n const queryFn = ASTUtils.findPropertyWithIdentifierKey(\n node.parent.properties,\n QUERY_FN,\n )\n\n if (\n scopeManager === null ||\n queryKey === undefined ||\n queryFn === undefined ||\n !ASTUtils.isNodeOfOneOf(queryFn.value, [\n AST_NODE_TYPES.ArrowFunctionExpression,\n AST_NODE_TYPES.FunctionExpression,\n AST_NODE_TYPES.ConditionalExpression,\n ])\n ) {\n return\n }\n\n let queryKeyNode = queryKey.value\n\n if (\n queryKeyNode.type === AST_NODE_TYPES.TSAsExpression &&\n queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression\n ) {\n queryKeyNode = queryKeyNode.expression\n }\n\n if (queryKeyNode.type === AST_NODE_TYPES.Identifier) {\n const expression = ASTUtils.getReferencedExpressionByIdentifier({\n context,\n node: queryKeyNode,\n })\n\n if (expression?.type === AST_NODE_TYPES.ArrayExpression) {\n queryKeyNode = expression\n }\n }\n\n const queryKeyValue = queryKeyNode\n const externalRefs = ASTUtils.getExternalRefs({\n scopeManager,\n sourceCode: context.sourceCode,\n node: getQueryFnRelevantNode(queryFn),\n })\n\n const relevantRefs = externalRefs.filter((reference) =>\n ExhaustiveDepsUtils.isRelevantReference({\n sourceCode: context.sourceCode,\n reference,\n scopeManager,\n node: getQueryFnRelevantNode(queryFn),\n }),\n )\n\n const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyValue).map(\n (identifier) =>\n ASTUtils.mapKeyNodeToText(identifier, context.sourceCode),\n )\n\n const missingRefs = relevantRefs\n .map((ref) => ({\n ref: ref,\n text: ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode),\n }))\n .filter(({ ref, text }) => {\n return (\n !ref.isTypeReference &&\n !ASTUtils.isAncestorIsCallee(ref.identifier) &&\n !existingKeys.some((existingKey) => existingKey === text) &&\n !existingKeys.includes(text.split('.')[0] ?? '')\n )\n })\n .map(({ ref, text }) => ({\n identifier: ref.identifier,\n text: text,\n }))\n\n const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text)\n\n if (uniqueMissingRefs.length > 0) {\n const missingAsText = uniqueMissingRefs\n .map((ref) =>\n ASTUtils.mapKeyNodeToText(ref.identifier, context.sourceCode),\n )\n .join(', ')\n\n const existingWithMissing = context.sourceCode\n .getText(queryKeyValue)\n .replace(/\\]$/, `, ${missingAsText}]`)\n\n const suggestions: TSESLint.ReportSuggestionArray<string> = []\n\n if (queryKeyNode.type === AST_NODE_TYPES.ArrayExpression) {\n suggestions.push({\n messageId: 'fixTo',\n data: { result: existingWithMissing },\n fix(fixer) {\n return fixer.replaceText(queryKeyValue, existingWithMissing)\n },\n })\n }\n\n context.report({\n node: node,\n messageId: 'missingDeps',\n data: {\n deps: uniqueMissingRefs.map((ref) => ref.text).join(', '),\n },\n suggest: suggestions,\n })\n }\n },\n }\n }),\n})\n\nfunction getQueryFnRelevantNode(queryFn: TSESTree.Property) {\n if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {\n return queryFn.value\n }\n\n if (\n queryFn.value.consequent.type === AST_NODE_TYPES.Identifier &&\n queryFn.value.consequent.name === 'skipToken'\n ) {\n return queryFn.value.alternate\n }\n\n return queryFn.value.consequent\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -402,6 +402,20 @@ ruleTester.run('exhaustive-deps', rule, {
|
|
|
402
402
|
}
|
|
403
403
|
`,
|
|
404
404
|
},
|
|
405
|
+
{
|
|
406
|
+
name: 'queryFn as a ternary expression with dep and a skipToken',
|
|
407
|
+
code: normalizeIndent`
|
|
408
|
+
import { useQuery, skipToken } from "@tanstack/react-query";
|
|
409
|
+
const fetch = true
|
|
410
|
+
|
|
411
|
+
function Component({ id }) {
|
|
412
|
+
useQuery({
|
|
413
|
+
queryKey: [id],
|
|
414
|
+
queryFn: fetch ? () => Promise.resolve(id) : skipToken
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
`,
|
|
418
|
+
},
|
|
405
419
|
],
|
|
406
420
|
invalid: [
|
|
407
421
|
{
|
|
@@ -733,5 +747,25 @@ ruleTester.run('exhaustive-deps', rule, {
|
|
|
733
747
|
},
|
|
734
748
|
],
|
|
735
749
|
},
|
|
750
|
+
{
|
|
751
|
+
name: 'should fail if queryFn is a ternary expression with missing dep and a skipToken',
|
|
752
|
+
code: normalizeIndent`
|
|
753
|
+
import { useQuery, skipToken } from "@tanstack/react-query";
|
|
754
|
+
const fetch = true
|
|
755
|
+
|
|
756
|
+
function Component({ id }) {
|
|
757
|
+
useQuery({
|
|
758
|
+
queryKey: [],
|
|
759
|
+
queryFn: fetch ? () => Promise.resolve(id) : skipToken
|
|
760
|
+
})
|
|
761
|
+
}
|
|
762
|
+
`,
|
|
763
|
+
errors: [
|
|
764
|
+
{
|
|
765
|
+
messageId: 'missingDeps',
|
|
766
|
+
data: { deps: 'id' },
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
736
770
|
],
|
|
737
771
|
})
|
|
@@ -4,7 +4,7 @@ import { getDocsUrl } from '../../utils/get-docs-url'
|
|
|
4
4
|
import { uniqueBy } from '../../utils/unique-by'
|
|
5
5
|
import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
|
|
6
6
|
import { ExhaustiveDepsUtils } from './exhaustive-deps.utils'
|
|
7
|
-
import type { TSESLint } from '@typescript-eslint/utils'
|
|
7
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
|
|
8
8
|
import type { ExtraRuleDocs } from '../../types'
|
|
9
9
|
|
|
10
10
|
const QUERY_KEY = 'queryKey'
|
|
@@ -59,6 +59,7 @@ export const rule = createRule({
|
|
|
59
59
|
!ASTUtils.isNodeOfOneOf(queryFn.value, [
|
|
60
60
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
61
61
|
AST_NODE_TYPES.FunctionExpression,
|
|
62
|
+
AST_NODE_TYPES.ConditionalExpression,
|
|
62
63
|
])
|
|
63
64
|
) {
|
|
64
65
|
return
|
|
@@ -88,7 +89,7 @@ export const rule = createRule({
|
|
|
88
89
|
const externalRefs = ASTUtils.getExternalRefs({
|
|
89
90
|
scopeManager,
|
|
90
91
|
sourceCode: context.sourceCode,
|
|
91
|
-
node: queryFn
|
|
92
|
+
node: getQueryFnRelevantNode(queryFn),
|
|
92
93
|
})
|
|
93
94
|
|
|
94
95
|
const relevantRefs = externalRefs.filter((reference) =>
|
|
@@ -96,7 +97,7 @@ export const rule = createRule({
|
|
|
96
97
|
sourceCode: context.sourceCode,
|
|
97
98
|
reference,
|
|
98
99
|
scopeManager,
|
|
99
|
-
node: queryFn
|
|
100
|
+
node: getQueryFnRelevantNode(queryFn),
|
|
100
101
|
}),
|
|
101
102
|
)
|
|
102
103
|
|
|
@@ -161,3 +162,18 @@ export const rule = createRule({
|
|
|
161
162
|
}
|
|
162
163
|
}),
|
|
163
164
|
})
|
|
165
|
+
|
|
166
|
+
function getQueryFnRelevantNode(queryFn: TSESTree.Property) {
|
|
167
|
+
if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {
|
|
168
|
+
return queryFn.value
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
queryFn.value.consequent.type === AST_NODE_TYPES.Identifier &&
|
|
173
|
+
queryFn.value.consequent.name === 'skipToken'
|
|
174
|
+
) {
|
|
175
|
+
return queryFn.value.alternate
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return queryFn.value.consequent
|
|
179
|
+
}
|