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 e870bbd640..78492306bd 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 @@ -18,7 +18,9 @@ export const rule = createRule({ }, messages: { missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`, + fixTo: 'Fix to {{result}}', }, + hasSuggestions: true, fixable: 'code', schema: [], }, @@ -89,25 +91,29 @@ export const rule = createRule({ const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text) if (uniqueMissingRefs.length > 0) { + const missingAsText = uniqueMissingRefs + .map((ref) => ASTUtils.mapKeyNodeToText(ref.identifier, sourceCode)) + .join(', ') + + const existingWithMissing = sourceCode + .getText(queryKeyValue) + .replace(/\]$/, `, ${missingAsText}]`) + context.report({ node: node, messageId: 'missingDeps', data: { deps: uniqueMissingRefs.map((ref) => ref.text).join(', '), }, - fix(fixer) { - const missingAsText = uniqueMissingRefs - .map((ref) => - ASTUtils.mapKeyNodeToText(ref.identifier, sourceCode), - ) - .join(', ') - - const existingWithMissing = sourceCode - .getText(queryKeyValue) - .replace(/\]$/, `, ${missingAsText}]`) - - return fixer.replaceText(queryKeyValue, existingWithMissing) - }, + suggest: [ + { + messageId: 'fixTo', + data: { result: existingWithMissing }, + fix(fixer) { + return fixer.replaceText(queryKeyValue, existingWithMissing) + }, + }, + ], }) } }, 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 137a46caef..9f199fe3e7 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 @@ -1,4 +1,5 @@ import { ESLintUtils } from '@typescript-eslint/utils' +import { normalizeIndent } from '../../utils/test-utils' import { rule } from './exhaustive-deps.rule' const ruleTester = new ESLintUtils.RuleTester({ @@ -60,73 +61,128 @@ ruleTester.run('exhaustive-deps', rule, { invalid: [ { name: 'should fail when deps are missing in query factory', - code: ` + code: normalizeIndent` const todoQueries = { list: () => ({ queryKey: ['entity'], queryFn: fetchEntities }), detail: (id) => ({ queryKey: ['entity'], queryFn: () => fetchEntity(id) }) } `, - output: ` - const todoQueries = { - list: () => ({ queryKey: ['entity'], queryFn: fetchEntities }), - detail: (id) => ({ queryKey: ['entity', id], queryFn: () => fetchEntity(id) }) - } - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'id' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: "['entity', id]" }, + output: normalizeIndent` + const todoQueries = { + list: () => ({ queryKey: ['entity'], queryFn: fetchEntities }), + detail: (id) => ({ queryKey: ['entity', id], queryFn: () => fetchEntity(id) }) + } + `, + }, + ], + }, + ], }, { name: 'should fail when no deps are passed (react)', - code: ` + code: normalizeIndent` const id = 1; useQuery({ queryKey: ["entity"], queryFn: () => api.getEntity(id) }); `, - output: ` - const id = 1; - useQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'id' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '["entity", id]' }, + output: normalizeIndent` + const id = 1; + useQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); + `, + }, + ], + }, + ], }, { name: 'should fail when no deps are passed (solid)', - code: ` + code: normalizeIndent` const id = 1; createQuery({ queryKey: ["entity"], queryFn: () => api.getEntity(id) }); `, - output: ` - const id = 1; - createQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'id' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '["entity", id]' }, + output: normalizeIndent` + const id = 1; + createQuery({ queryKey: ["entity", id], queryFn: () => api.getEntity(id) }); + `, + }, + ], + }, + ], }, { name: 'should fail when deps are passed incorrectly', - code: ` + code: normalizeIndent` const id = 1; useQuery({ queryKey: ["entity/\${id}"], queryFn: () => api.getEntity(id) }); `, - output: ` - const id = 1; - useQuery({ queryKey: ["entity/\${id}", id], queryFn: () => api.getEntity(id) }); - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'id' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '["entity/${id}", id]' }, + output: normalizeIndent` + const id = 1; + useQuery({ queryKey: ["entity/\${id}", id], queryFn: () => api.getEntity(id) }); + `, + }, + ], + }, + ], }, { name: 'should pass missing dep while key has a template literal', - code: ` + code: normalizeIndent` const a = 1; const b = 2; useQuery({ queryKey: [\`entity/\${a}\`], queryFn: () => api.getEntity(a, b) }); `, - output: ` - const a = 1; - const b = 2; - useQuery({ queryKey: [\`entity/\${a}\`, b], queryFn: () => api.getEntity(a, b) }); - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'b' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'b' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '[`entity/${a}`, b]' }, + output: normalizeIndent` + const a = 1; + const b = 2; + useQuery({ queryKey: [\`entity/\${a}\`, b], queryFn: () => api.getEntity(a, b) }); + `, + }, + ], + }, + ], }, { name: 'should fail when dep exists inside setter and missing in queryKey', - code: ` + code: normalizeIndent` const [id] = React.useState(1); useQuery({ queryKey: ["entity"], @@ -136,21 +192,32 @@ ruleTester.run('exhaustive-deps', rule, { } }); `, - output: ` - const [id] = React.useState(1); - useQuery({ - queryKey: ["entity", id], - queryFn: () => { - const { data } = axios.get(\`.../\${id}\`); - return data; - } - }); - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'id' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'id' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '["entity", id]' }, + output: normalizeIndent` + const [id] = React.useState(1); + useQuery({ + queryKey: ["entity", id], + queryFn: () => { + const { data } = axios.get(\`.../\${id}\`); + return data; + } + }); + `, + }, + ], + }, + ], }, { name: 'should fail when dep does not exist while having a complex queryKey', - code: ` + code: normalizeIndent` const todoQueries = { key: (a, b, c, d, e) => ({ queryKey: ["entity", a, [b], { c }, 1, true], @@ -158,19 +225,30 @@ ruleTester.run('exhaustive-deps', rule, { }) } `, - output: ` - const todoQueries = { - key: (a, b, c, d, e) => ({ - queryKey: ["entity", a, [b], { c }, 1, true, d, e], - queryFn: () => api.getEntity(a, b, c, d, e) - }) - } - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'd, e' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'd, e' }, + suggestions: [ + { + messageId: 'fixTo', + data: { result: '["entity", a, [b], { c }, 1, true, d, e]' }, + output: normalizeIndent` + const todoQueries = { + key: (a, b, c, d, e) => ({ + queryKey: ["entity", a, [b], { c }, 1, true, d, e], + queryFn: () => api.getEntity(a, b, c, d, e) + }) + } + `, + }, + ], + }, + ], }, { name: 'should fail when dep does not exist while having a complex queryKey #2', - code: ` + code: normalizeIndent` const todoQueries = { key: (dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) => ({ queryKey: ['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7]], @@ -178,29 +256,56 @@ ruleTester.run('exhaustive-deps', rule, { }), }; `, - output: ` - const todoQueries = { - key: (dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) => ({ - queryKey: ['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7], dep8], - queryFn: () => api.getEntity(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8), - }), - }; - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'dep8' } }], + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'dep8' }, + suggestions: [ + { + messageId: 'fixTo', + data: { + result: + "['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7], dep8]", + }, + output: normalizeIndent` + const todoQueries = { + key: (dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8) => ({ + queryKey: ['foo', {dep1, dep2: dep2, bar: dep3, baz: [dep4, dep5]}, [dep6, dep7], dep8], + queryFn: () => api.getEntity(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8), + }), + }; + `, + }, + ], + }, + ], }, { name: 'should fail when two deps that depend on each other are missing', - code: ` - function Component({ map, key }) { - useQuery({ queryKey: ["key"], queryFn: () => api.get(map[key]) }); - } - `, - output: ` - function Component({ map, key }) { - useQuery({ queryKey: ["key", map[key]], queryFn: () => api.get(map[key]) }); - } - `, - errors: [{ messageId: 'missingDeps', data: { deps: 'map[key]' } }], + code: normalizeIndent` + function Component({ map, key }) { + useQuery({ queryKey: ["key"], queryFn: () => api.get(map[key]) }); + } + `, + errors: [ + { + messageId: 'missingDeps', + data: { deps: 'map[key]' }, + suggestions: [ + { + messageId: 'fixTo', + data: { + result: '["key", map[key]]', + }, + output: normalizeIndent` + function Component({ map, key }) { + useQuery({ queryKey: ["key", map[key]], queryFn: () => api.get(map[key]) }); + } + `, + }, + ], + }, + ], }, ], })