Skip to content

Conversation

fbieler
Copy link
Contributor

@fbieler fbieler commented Sep 25, 2025

🎯 Changes

Recursively dereference variables and type assertions in query keys.

Also dereference even if the referenced expression is not an array expression.

Fixes #9643

βœ… Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

πŸš€ Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Better recognition of stable query keys produced by factory functions and assigned variables.
  • Bug Fixes

    • Reduced false positives for exhaustive-deps by handling const assertions and dereferenced variable query keys.
  • Refactor

    • Centralized and safer query-key resolution to improve reliability and avoid recursion issues.
  • Tests

    • Added tests for factory-produced keys, variable assignments, and const-assertion scenarios.
  • Chores

    • Added changeset entry for a patch release.

TanStack#9643)

Recursively dereference variables and type assertions in query keys.

Also dereference even if the referenced expression is not an array
expression.
Copy link
Contributor

coderabbitai bot commented Sep 25, 2025

Walkthrough

Replaces inline unwrapping in the exhaustive-deps rule with a centralized dereference helper and adds valid tests for query key factories and const-assertion scenarios so dereferenced/const-asserted variables are treated as stable query keys.

Changes

Cohort / File(s) Summary
Rule logic and dereference helper
packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts
Extracts identifier and TS-as-expression unwrapping into dereferenceVariablesAndTypeAssertions(...) which iteratively unwraps TSAs and resolves Identifiers with a recursion guard; main rule uses the resolved node for dependency analysis.
Valid test additions for factories and const assertions
packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts
Adds four new valid tests: two that assign a query key produced by a factory to a variable before passing to useQuery, and two const-assertion cases (dereference and variable-assigned) recognized as stable.
Release metadata
.changeset/cold-falcons-warn.md
Adds a changeset entry describing a patch for exhaustive-deps handling with variables and type assertions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer Code
  participant Rule as ESLint: exhaustive-deps.rule
  participant Helper as dereferenceVariablesAndTypeAssertions
  participant Analyzer as Dependency Analyzer

  Dev->>Rule: useQuery({ queryKey, queryFn })
  Rule->>Helper: dereferenceVariablesAndTypeAssertions(queryKey.value, context)
  Helper-->>Rule: resolvedQueryKeyNode (or current node on cycle)
  Rule->>Analyzer: inspect resolvedQueryKeyNode for external refs / missing deps
  Analyzer-->>Rule: missing deps / none
  alt Missing dependencies
    Rule-->>Dev: report missing dependencies
  else
    Rule-->>Dev: no findings (valid)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hop through code with whiskers keen,
I unwrap keys both seen and unseen.
Factories and consts I gently probe,
Chasing false alarms from every lobe.
A carrot cheer for lint-time peace πŸ₯•

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (4 passed)
Check name Status Explanation
Title Check βœ… Passed The pull request title succinctly identifies the scope and nature of the fix by referencing the eslint-plugin, the exhaustive-deps rule, and the handling of variables and type assertions, directly reflecting the changes in both the rule implementation and tests. It clearly communicates the main change without extraneous details or unrelated information. The inclusion of the issue number is acceptable to link the fix context.
Linked Issues Check βœ… Passed The changes implement recursive dereferencing of identifiers and type assertions exactly as described in issue #9643, ensuring query keys are unwrapped regardless of expression type and preventing false-positive missing dependency reports. The new helper function and updated tests cover both factory-generated key variables and const assertions, directly satisfying the linked issue’s requirements.
Out of Scope Changes Check βœ… Passed All modifications are confined to the exhaustive-deps ESLint rule, its associated tests, and the relevant changeset entry, directly targeting the objectives of issue #9643 without introducing unrelated files or functionality. This ensures the changes remain focused solely on the intended linting behavior.
Description Check βœ… Passed The pull request description adheres to the repository template by including distinct 🎯 Changes, βœ… Checklist, and πŸš€ Release Impact sections and clearly explains the recursive dereferencing enhancements along with the resolution of issue #9643. It notes the addition of a changeset and outlines the motivation and scope of the fix. This structure provides reviewers with the required context and follows the approved format for PR descriptions.
✨ Finishing touches
  • πŸ“ Generate Docstrings
πŸ§ͺ Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between cd29063 and 960b27d.

πŸ“’ Files selected for processing (2)
  • packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts (2 hunks)
  • packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts (1)
packages/eslint-plugin-query/src/__tests__/test-utils.ts (1)
  • normalizeIndent (3-7)
πŸͺ› Biome (2.1.2)
packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts

[error] 175-178: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

πŸ”‡ Additional comments (2)
packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts (2)

229-260: Great coverage for the query key factory regression

Codifies the exact reproduction from #9643, and the paired variant with layered const assertions ensures the new dereferencing logic handles call expressions and returned tuples without flagging false positives. Nicely done.


283-305: Const assertion scenarios nicely captured

These two cases exercise both the β€œassert at usage site” and β€œassert at declaration” pathways, giving strong confidence that recursive dereferencing now accounts for TypeScript as const chains.

…sertions (TanStack#9643)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 960b27d and 48acd04.

πŸ“’ Files selected for processing (1)
  • packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (2 hunks)
πŸ”‡ Additional comments (2)
packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (2)

68-68: Good call centralizing dereferencing; confirm fixer intent across variable initializers.

Using the dereferenced node enables correct key analysis and now allows fixing array literals defined via variables. Please confirm it's intended that the fixer edits the variable’s initializer (when the array literal is defined elsewhere in the same file) rather than the queryKey property usage.


175-186: noSwitchDeclarations resolved.

Wrapping the Identifier case body in braces fixes the Biome lint error. LGTM.

@TkDodo TkDodo requested a review from Newbie012 September 25, 2025 10:35
@Newbie012
Copy link
Collaborator

Hi!

I agree that the current behavior (false positive) is an actual bug and should be fixed, but are we sure we want to support query key factory? it can get quite complicated; I'll give a basic example:

const createKey = (id) => ["foo"];

function useHook({ id }) {
  return useQuery({ queryKey: createKey({ id }), ... }); // Should it be an error? since `createKey` doesn't pass the `id` to the final array.
}

I'm not against supporting it, but it's important to note that if we're going to support it, we should decide how β€˜deep’ we want to get into this, especially now that queryOptions is built in and somewhat simplifies the query keys management.

Thoughts? @TkDodo

@fbieler
Copy link
Contributor Author

fbieler commented Sep 26, 2025

I was under the impression that query key factories are already (partially) supported. Given your example, I understand that the support isn't perfect, and probably never will be. However, I would have thought that this PR touches query key factory support only peripherally.

@TkDodo
Copy link
Collaborator

TkDodo commented Sep 26, 2025

I think the example is somewhat flawed because why would you a pass a value (id) to a key factory if it has no influence on the key whatsoever? So yes, this should error, because for all we know, you call a function with id and it’s not part of the key, so it’s missing.

Copy link

changeset-bot bot commented Sep 26, 2025

πŸ¦‹ Changeset detected

Latest commit: e21267d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@tanstack/eslint-plugin-query Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

nx-cloud bot commented Sep 26, 2025

View your CI Pipeline Execution β†— for commit e21267d

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... βœ… Succeeded 45s View β†—
nx run-many --target=build --exclude=examples/*... βœ… Succeeded 5s View β†—

☁️ Nx Cloud last updated this comment at 2025-09-26 13:13:51 UTC

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
.changeset/cold-falcons-warn.md (1)

5-6: Tighten the changeset message and auto‑link the issue.

Make the summary more descriptive and add β€œFixes #9643” so GitHub auto‑closes the issue.

Apply this diff:

-fix: exhaustive-deps with variables and type assertions
+fix(exhaustive-deps): recursively dereference identifiers and TS assertions in query keys (variables, as-const), even when the referenced expression is not an array; avoid crashes on cycles
+
+Fixes #9643
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 877669f and 0740c7e.

πŸ“’ Files selected for processing (2)
  • .changeset/cold-falcons-warn.md (1 hunks)
  • packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
πŸ“š Learning: 2025-09-02T17:57:33.184Z
Learnt from: TkDodo
PR: TanStack/query#9612
File: packages/query-async-storage-persister/src/asyncThrottle.ts:0-0
Timestamp: 2025-09-02T17:57:33.184Z
Learning: When importing from tanstack/query-core in other TanStack Query packages like query-async-storage-persister, a workspace dependency "tanstack/query-core": "workspace:*" needs to be added to the package.json.

Applied to files:

  • .changeset/cold-falcons-warn.md
🧬 Code graph analysis (1)
packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (1)
packages/eslint-plugin-query/src/utils/ast-utils.ts (1)
  • ASTUtils (5-391)
πŸ”‡ Additional comments (2)
packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.rule.ts (2)

68-71: Good call: centralizing deref logic.

Using a helper for dereferencing reduces duplication and keeps the main rule readable.


171-204: Expand unwrapping to cover all type wrappers
Add TSTypeAssertion, TSNonNullExpression, ParenthesizedExpression, and TSSatisfiesExpression to the switch in dereferenceVariablesAndTypeAssertions, bump the iteration limit (MAX_STEPS = 1 << 10), and rename visitedNodes to visited:

 function dereferenceVariablesAndTypeAssertions(
   queryKeyNode: TSESTree.Node,
   context: Readonly<TSESLint.RuleContext<string, ReadonlyArray<unknown>>>,
 ) {
-  const visitedNodes = new Set<TSESTree.Node>()
+  const MAX_STEPS = 1 << 10
+  const visited = new Set<TSESTree.Node>()
 
-  for (let i = 0; i < 1 << 8; ++i) {
-    if (visitedNodes.has(queryKeyNode)) {
+  for (let i = 0; i < MAX_STEPS; ++i) {
+    if (visited.has(queryKeyNode)) return queryKeyNode
+    visited.add(queryKeyNode)
 
       switch (queryKeyNode.type) {
-      case AST_NODE_TYPES.TSAsExpression:
+      case AST_NODE_TYPES.TSAsExpression:
+      case AST_NODE_TYPES.TSTypeAssertion:
+      case AST_NODE_TYPES.TSNonNullExpression:
+      case AST_NODE_TYPES.ParenthesizedExpression:
+      case AST_NODE_TYPES.TSSatisfiesExpression:
         queryKeyNode = queryKeyNode.expression
         break
       …

@Newbie012
Copy link
Collaborator

My goal in the example was to show that, while we could validate that all required args were passed to the query key factory, it doesn’t really "cut it". I tried to keep the example minimal even though it’s not very practical. A better example might look like this:

const createKey = (id) => {
  const mock = id > 0;
  return ["foo", mock]; // user forgot to also pass the `id`
};

function useHook({ id }) {
  return useQuery({ queryKey: createKey({ id }), ... });
}

Ensuring that id exists in the returned array can get complicated in cases where a function has multiple return statements or aliases the original argument.


But maybe I'm overthinking it and we should support it as a "best-effort" solution.

LGTM @fbieler πŸ™‚

Copy link

pkg-pr-new bot commented Sep 26, 2025

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9687

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9687

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9687

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9687

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@9687

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@9687

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9687

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9687

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@9687

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@9687

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9687

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9687

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@9687

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9687

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9687

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@9687

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9687

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9687

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@9687

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9687

commit: e21267d

Copy link

codecov bot commented Sep 26, 2025

Codecov Report

❌ Patch coverage is 85.00000% with 3 lines in your changes missing coverage. Please review.
βœ… Project coverage is 83.03%. Comparing base (8e42926) to head (e21267d).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##             main    #9687       +/-   ##
===========================================
+ Coverage   46.38%   83.03%   +36.65%     
===========================================
  Files         214       19      -195     
  Lines        8488      560     -7928     
  Branches     1929      206     -1723     
===========================================
- Hits         3937      465     -3472     
+ Misses       4108       73     -4035     
+ Partials      443       22      -421     
Components Coverage Ξ”
@tanstack/angular-query-experimental βˆ… <ΓΈ> (βˆ…)
@tanstack/eslint-plugin-query 83.03% <85.00%> (-0.21%) ⬇️
@tanstack/query-async-storage-persister βˆ… <ΓΈ> (βˆ…)
@tanstack/query-broadcast-client-experimental βˆ… <ΓΈ> (βˆ…)
@tanstack/query-codemods βˆ… <ΓΈ> (βˆ…)
@tanstack/query-core βˆ… <ΓΈ> (βˆ…)
@tanstack/query-devtools βˆ… <ΓΈ> (βˆ…)
@tanstack/query-persist-client-core βˆ… <ΓΈ> (βˆ…)
@tanstack/query-sync-storage-persister βˆ… <ΓΈ> (βˆ…)
@tanstack/query-test-utils βˆ… <ΓΈ> (βˆ…)
@tanstack/react-query βˆ… <ΓΈ> (βˆ…)
@tanstack/react-query-devtools βˆ… <ΓΈ> (βˆ…)
@tanstack/react-query-next-experimental βˆ… <ΓΈ> (βˆ…)
@tanstack/react-query-persist-client βˆ… <ΓΈ> (βˆ…)
@tanstack/solid-query βˆ… <ΓΈ> (βˆ…)
@tanstack/solid-query-devtools βˆ… <ΓΈ> (βˆ…)
@tanstack/solid-query-persist-client βˆ… <ΓΈ> (βˆ…)
@tanstack/svelte-query βˆ… <ΓΈ> (βˆ…)
@tanstack/svelte-query-devtools βˆ… <ΓΈ> (βˆ…)
@tanstack/svelte-query-persist-client βˆ… <ΓΈ> (βˆ…)
@tanstack/vue-query βˆ… <ΓΈ> (βˆ…)
@tanstack/vue-query-devtools βˆ… <ΓΈ> (βˆ…)
πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • πŸ“¦ JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@TkDodo TkDodo merged commit 24a2266 into TanStack:main Sep 27, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

False positive in @tanstack/query/exhaustive-deps when combining query key variable with function invocation
4 participants