diff --git a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts index cb6371aa08..ab675965e9 100644 --- a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts +++ b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts @@ -81,6 +81,7 @@ export const rule = createRule({ const queryKeyValue = queryKeyNode const refs = ASTUtils.getExternalRefs({ scopeManager, + sourceCode, node: queryFn.value, }) @@ -104,7 +105,8 @@ export const rule = createRule({ return ( !ref.isTypeReference && !ASTUtils.isAncestorIsCallee(ref.identifier) && - !existingKeys.some((existingKey) => existingKey === text) + !existingKeys.some((existingKey) => existingKey === text) && + !existingKeys.includes(text.split('.')[0] ?? '') ) }) .map(({ ref, text }) => ({ diff --git a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.test.ts b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.test.ts index 7b0a4ed033..bc67850833 100644 --- a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.test.ts +++ b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.test.ts @@ -87,7 +87,7 @@ ruleTester.run('exhaustive-deps', rule, { type Result = {}; function MyComponent(props) { useQuery({ - queryKey: ["foo", dep1], + queryKey: ["foo", dep], queryFn: () => api.get(dep), }); } @@ -236,6 +236,17 @@ ruleTester.run('exhaustive-deps', rule, { } `, }, + { + name: 'should not fail if queryKey is having the whole object while queryFn uses some props of it', + code: normalizeIndent` + const state = { foo: 'foo', bar: 'bar' } + + useQuery({ + queryKey: ['state', state], + queryFn: () => Promise.resolve({ foo: state.foo, bar: state.bar }) + }) + `, + }, ], invalid: [ { @@ -560,5 +571,22 @@ ruleTester.run('exhaustive-deps', rule, { }, ], }, + { + name: 'should fail if queryFn is using multiple object props when only one of them is in the queryKey', + code: normalizeIndent` + const state = { foo: 'foo', bar: 'bar' } + + useQuery({ + queryKey: ['state', state.foo], + queryFn: () => Promise.resolve({ foo: state.foo, bar: state.bar }) + }) + `, + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'state.bar' }, + }, + ], + }, ], }) diff --git a/packages/eslint-plugin-query/src/utils/ast-utils.ts b/packages/eslint-plugin-query/src/utils/ast-utils.ts index 32161a9570..f7ca8d4471 100644 --- a/packages/eslint-plugin-query/src/utils/ast-utils.ts +++ b/packages/eslint-plugin-query/src/utils/ast-utils.ts @@ -142,24 +142,40 @@ export const ASTUtils = { }, getExternalRefs(params: { scopeManager: TSESLint.Scope.ScopeManager + sourceCode: Readonly node: TSESTree.Node }): TSESLint.Scope.Reference[] { - const { scopeManager, node } = params + const { scopeManager, sourceCode, node } = params const scope = scopeManager.acquire(node) if (scope === null) { return [] } - const readOnlyRefs = scope.references.filter((x) => x.isRead()) + const references = scope.references + .filter((x) => x.isRead()) + .map((x) => { + const referenceNode = ASTUtils.traverseUpOnly(x.identifier, [ + AST_NODE_TYPES.MemberExpression, + AST_NODE_TYPES.Identifier, + ]) + + return { + variable: x, + node: referenceNode, + text: sourceCode.getText(referenceNode), + } + }) + const localRefIds = new Set( - [...scope.set.values()].map((x) => x.identifiers[0]), + [...scope.set.values()].map((x) => sourceCode.getText(x.identifiers[0])), ) - const externalRefs = readOnlyRefs.filter( - (x) => x.resolved === null || !localRefIds.has(x.resolved.identifiers[0]), + + const externalRefs = references.filter( + (x) => x.variable.resolved === null || !localRefIds.has(x.text), ) - return uniqueBy(externalRefs, (x) => x.resolved) + return uniqueBy(externalRefs, (x) => x.text).map((x) => x.variable) }, mapKeyNodeToText( node: TSESTree.Node,