Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
},
Expand Down Expand Up @@ -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)
},
},
],
})
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -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"],
Expand All @@ -136,71 +192,120 @@ 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],
queryFn: () => api.getEntity(a, b, c, d, e)
})
}
`,
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]],
queryFn: () => api.getEntity(dep1, dep2, dep3, dep4, dep5, dep6, dep7, dep8),
}),
};
`,
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]) });
}
`,
},
],
},
],
},
],
})