diff --git a/.changeset/slimy-taxes-make.md b/.changeset/slimy-taxes-make.md new file mode 100644 index 0000000000..1dc1cf8302 --- /dev/null +++ b/.changeset/slimy-taxes-make.md @@ -0,0 +1,5 @@ +--- +'@tanstack/eslint-plugin-query': patch +--- + +exhaustive-deps rule fixed for vue files 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 f27b058ec5..78a0a2fd6f 100644 --- a/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts +++ b/packages/eslint-plugin-query/src/__tests__/exhaustive-deps.test.ts @@ -548,6 +548,45 @@ ruleTester.run('exhaustive-deps', rule, { } `, }, + { + name: 'should pass in Vue file when deps are correctly included (script setup)', + filename: 'Component.vue', + code: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const id = 1 + useQuery({ + queryKey: ['entity', id], + queryFn: () => fetchEntity(id), + }) + `, + }, + { + name: 'should not require imports in queryKey for Vue files', + filename: 'Component.vue', + code: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + import { fetchTodos } from './api' + + useQuery({ + queryKey: ['todos'], + queryFn: () => fetchTodos(), + }) + `, + }, + { + name: 'should not require global fetch in queryKey for Vue files', + filename: 'Component.vue', + code: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const id = 1 + useQuery({ + queryKey: ['entity', id], + queryFn: () => fetch('/api/entity/' + id), + }) + `, + }, ], invalid: [ { @@ -975,5 +1014,75 @@ ruleTester.run('exhaustive-deps', rule, { }, ], }, + { + name: 'should fail in Vue file when deps are missing (script setup)', + filename: 'Component.vue', + code: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const id = 1 + useQuery({ + queryKey: ['entity'], + queryFn: () => fetchEntity(id), + }) + `, + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: "['entity', id]" }, + output: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const id = 1 + useQuery({ + queryKey: ['entity', id], + queryFn: () => fetchEntity(id), + }) + `, + }, + ], + }, + ], + }, + { + name: 'should fail in Vue file when multiple deps are missing', + filename: 'Component.vue', + code: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const userId = 1 + const orgId = 2 + useQuery({ + queryKey: ['users'], + queryFn: () => fetchUser(userId, orgId), + }) + `, + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'userId, orgId' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: "['users', userId, orgId]" }, + output: normalizeIndent` + import { useQuery } from '@tanstack/vue-query' + + const userId = 1 + const orgId = 2 + useQuery({ + queryKey: ['users', userId, orgId], + queryFn: () => fetchUser(userId, orgId), + }) + `, + }, + ], + }, + ], + }, ], }) 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 15c0918e97..c3a12e5e18 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 @@ -82,6 +82,7 @@ export const rule = createRule({ reference, scopeManager, node: getQueryFnRelevantNode(queryFn), + filename: context.filename, }), ) diff --git a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts index 5762f506d1..c0533e3910 100644 --- a/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts +++ b/packages/eslint-plugin-query/src/rules/exhaustive-deps/exhaustive-deps.utils.ts @@ -8,22 +8,35 @@ export const ExhaustiveDepsUtils = { reference: TSESLint.Scope.Reference scopeManager: TSESLint.Scope.ScopeManager node: TSESTree.Node + filename: string }) { - const { sourceCode, reference, scopeManager, node } = params + const { sourceCode, reference, scopeManager, node, filename } = params const component = ASTUtils.getFunctionAncestor(sourceCode, node) - if (component === undefined) { - return false - } + if (component !== undefined) { + if ( + !ASTUtils.isDeclaredInNode({ + scopeManager, + reference, + functionNode: component, + }) + ) { + return false + } + } else { + const isVueFile = filename.endsWith('.vue') + + if (!isVueFile) { + return false + } + + const definition = reference.resolved?.defs[0] + const isGlobalVariable = definition === undefined + const isImport = definition?.type === 'ImportBinding' - if ( - !ASTUtils.isDeclaredInNode({ - scopeManager, - reference, - functionNode: component, - }) - ) { - return false + if (isGlobalVariable || isImport) { + return false + } } return (