From 960b27dc339995e0aef2a49d0d73e0390a627b78 Mon Sep 17 00:00:00 2001 From: Fabian Bieler Date: Thu, 25 Sep 2025 11:03:37 +0200 Subject: [PATCH 1/5] fix(eslint-plugin): exhaustive-deps with variables and type assertions (#9643) Recursively dereference variables and type assertions in query keys. Also dereference even if the referenced expression is not an array expression. --- .../src/__tests__/exhaustive-deps.test.ts | 57 +++++++++++++++++++ .../exhaustive-deps/exhaustive-deps.rule.ts | 44 +++++++------- 2 files changed, 82 insertions(+), 19 deletions(-) diff --git a/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts b/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts index e3ea5790dd..f27b058ec5 100644 --- a/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts +++ b/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts @@ -226,6 +226,39 @@ ruleTester.run('exhaustive-deps', rule, { }) `, }, + { + name: 'should pass with queryKeyFactory result assigned to a variable', + code: ` + function fooQueryKeyFactory(dep: string) { + return ["foo", dep]; + } + + const useFoo = (dep: string) => { + const queryKey = fooQueryKeyFactory(dep); + return useQuery({ + queryKey, + queryFn: () => Promise.resolve(dep), + }) + } + `, + }, + { + name: 'should pass with queryKeyFactory result assigned to a variable 2', + code: ` + function fooQueryKeyFactory(dep: string) { + const x = ["foo", dep] as const; + return x as const; + } + + const useFoo = (dep: string) => { + const queryKey = fooQueryKeyFactory(dep); + return useQuery({ + queryKey, + queryFn: () => Promise.resolve(dep), + }) + } + `, + }, { name: 'should not treat new Error as missing dependency', code: normalizeIndent` @@ -246,6 +279,30 @@ ruleTester.run('exhaustive-deps', rule, { } `, }, + { + name: 'should see id when there is a const assertion of a variable dereference', + code: normalizeIndent` + const useX = (id: number) => { + const queryKey = ['foo', id] + return useQuery({ + queryKey: queryKey as const, + queryFn: async () => id, + }) + } + `, + }, + { + name: 'should see id when there is a const assertion assigned to a variable', + code: normalizeIndent` + const useX = (id: number) => { + const queryKey = ['foo', id] as const + return useQuery({ + queryKey, + queryFn: async () => id, + }) + } + `, + }, { name: 'should not fail if queryKey is having the whole object while queryFn uses some props of it', code: normalizeIndent` 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 bb87b04552..0b6d305ac6 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 @@ -65,25 +65,7 @@ export const rule = createRule({ return } - let queryKeyNode = queryKey.value - - if ( - queryKeyNode.type === AST_NODE_TYPES.TSAsExpression && - queryKeyNode.expression.type === AST_NODE_TYPES.ArrayExpression - ) { - queryKeyNode = queryKeyNode.expression - } - - if (queryKeyNode.type === AST_NODE_TYPES.Identifier) { - const expression = ASTUtils.getReferencedExpressionByIdentifier({ - context, - node: queryKeyNode, - }) - - if (expression?.type === AST_NODE_TYPES.ArrayExpression) { - queryKeyNode = expression - } - } + const queryKeyNode = dereferenceVariablesAndTypeAssertions(queryKey.value, context) const externalRefs = ASTUtils.getExternalRefs({ scopeManager, @@ -182,3 +164,27 @@ function getQueryFnRelevantNode(queryFn: TSESTree.Property) { return queryFn.value.consequent } + +function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, context: Readonly>>) { + for (let i = 0; i < (1 << 16); ++i) { + switch (queryKeyNode.type) { + case AST_NODE_TYPES.TSAsExpression: + queryKeyNode = queryKeyNode.expression + break + case AST_NODE_TYPES.Identifier: + const expression = ASTUtils.getReferencedExpressionByIdentifier({ + context, + node: queryKeyNode, + }) + + if (expression == null) { + return queryKeyNode + } + queryKeyNode = expression + break + default: + return queryKeyNode + } + } + throw new Error('Recursion limit reached.') +} From 48acd04fd7b97bc1420b34523071c987e86d7608 Mon Sep 17 00:00:00 2001 From: Fabian Bieler <61547179+fbieler@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:21:45 +0200 Subject: [PATCH 2/5] fixup! fix(eslint-plugin): exhaustive-deps with variables and type assertions (#9643) Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/rules/exhaustive-deps/exhaustive-deps.rule.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0b6d305ac6..9c826c689d 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 @@ -171,7 +171,7 @@ function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, cont case AST_NODE_TYPES.TSAsExpression: queryKeyNode = queryKeyNode.expression break - case AST_NODE_TYPES.Identifier: + case AST_NODE_TYPES.Identifier: { const expression = ASTUtils.getReferencedExpressionByIdentifier({ context, node: queryKeyNode, @@ -182,6 +182,7 @@ function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, cont } queryKeyNode = expression break + } default: return queryKeyNode } From 877669f6d600146d45c1899f33ea87ffb718fb60 Mon Sep 17 00:00:00 2001 From: Fabian Bieler Date: Thu, 25 Sep 2025 18:43:35 +0200 Subject: [PATCH 3/5] fixup! fix(eslint-plugin): exhaustive-deps with variables and type assertions (#9643) --- .../src/rules/exhaustive-deps/exhaustive-deps.rule.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 9c826c689d..0981b168ba 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 @@ -166,7 +166,14 @@ function getQueryFnRelevantNode(queryFn: TSESTree.Property) { } function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, context: Readonly>>) { - for (let i = 0; i < (1 << 16); ++i) { + const visitedNodes = new Set() + + for (let i = 0; i < (1 << 8); ++i) { + if (visitedNodes.has(queryKeyNode)) { + return queryKeyNode + } + visitedNodes.add(queryKeyNode) + switch (queryKeyNode.type) { case AST_NODE_TYPES.TSAsExpression: queryKeyNode = queryKeyNode.expression @@ -187,5 +194,5 @@ function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, cont return queryKeyNode } } - throw new Error('Recursion limit reached.') + return queryKeyNode } From 3e46ba3fe19c797fa93c8a1a9ef419b38187e987 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Fri, 26 Sep 2025 19:45:03 +1000 Subject: [PATCH 4/5] Add changeset --- .changeset/cold-falcons-warn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/cold-falcons-warn.md diff --git a/.changeset/cold-falcons-warn.md b/.changeset/cold-falcons-warn.md new file mode 100644 index 0000000000..d82b907d1b --- /dev/null +++ b/.changeset/cold-falcons-warn.md @@ -0,0 +1,5 @@ +--- +"@tanstack/eslint-plugin-query": patch +--- + +fix: exhaustive-deps with variables and type assertions From 0740c7eafc87f60691a7ded9346eacdbf5fe0488 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 26 Sep 2025 09:46:28 +0000 Subject: [PATCH 5/5] ci: apply automated fixes --- .changeset/cold-falcons-warn.md | 2 +- .../rules/exhaustive-deps/exhaustive-deps.rule.ts | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.changeset/cold-falcons-warn.md b/.changeset/cold-falcons-warn.md index d82b907d1b..fa005725e1 100644 --- a/.changeset/cold-falcons-warn.md +++ b/.changeset/cold-falcons-warn.md @@ -1,5 +1,5 @@ --- -"@tanstack/eslint-plugin-query": patch +'@tanstack/eslint-plugin-query': patch --- fix: exhaustive-deps with variables and type assertions 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 0981b168ba..15c0918e97 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 @@ -65,7 +65,10 @@ export const rule = createRule({ return } - const queryKeyNode = dereferenceVariablesAndTypeAssertions(queryKey.value, context) + const queryKeyNode = dereferenceVariablesAndTypeAssertions( + queryKey.value, + context, + ) const externalRefs = ASTUtils.getExternalRefs({ scopeManager, @@ -165,10 +168,13 @@ function getQueryFnRelevantNode(queryFn: TSESTree.Property) { return queryFn.value.consequent } -function dereferenceVariablesAndTypeAssertions(queryKeyNode: TSESTree.Node, context: Readonly>>) { +function dereferenceVariablesAndTypeAssertions( + queryKeyNode: TSESTree.Node, + context: Readonly>>, +) { const visitedNodes = new Set() - for (let i = 0; i < (1 << 8); ++i) { + for (let i = 0; i < 1 << 8; ++i) { if (visitedNodes.has(queryKeyNode)) { return queryKeyNode }