-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
exhaustive-deps.rule.ts
136 lines (119 loc) 路 3.83 KB
/
exhaustive-deps.rule.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import { AST_NODE_TYPES } from '@typescript-eslint/utils'
import { ASTUtils } from '../../utils/ast-utils'
import { createRule } from '../../utils/create-rule'
import { uniqueBy } from '../../utils/unique-by'
const QUERY_KEY = 'queryKey'
const QUERY_FN = 'queryFn'
export const name = 'exhaustive-deps'
export const rule = createRule({
name,
meta: {
type: 'problem',
docs: {
description: 'Prefer object syntax for useQuery',
recommended: 'error',
},
messages: {
missingDeps: `The following dependencies are missing in your queryKey: {{deps}}`,
fixTo: 'Fix to {{result}}',
},
hasSuggestions: true,
fixable: 'code',
schema: [],
},
defaultOptions: [],
create(context) {
return {
Property(node) {
if (
node.parent === undefined ||
!ASTUtils.isObjectExpression(node.parent) ||
!ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)
) {
return
}
const scopeManager = context.getSourceCode().scopeManager
const queryKey = ASTUtils.findPropertyWithIdentifierKey(
node.parent.properties,
QUERY_KEY,
)
const queryFn = ASTUtils.findPropertyWithIdentifierKey(
node.parent.properties,
QUERY_FN,
)
if (
scopeManager === null ||
queryKey === undefined ||
queryFn === undefined ||
queryFn.value.type !== AST_NODE_TYPES.ArrowFunctionExpression
) {
return
}
let queryKeyNode = queryKey.value
if (queryKeyNode.type === AST_NODE_TYPES.Identifier) {
const expression = ASTUtils.getReferencedExpressionByIdentifier({
context,
node: queryKeyNode,
})
if (expression?.type === AST_NODE_TYPES.ArrayExpression) {
queryKeyNode = expression
}
}
if (queryKeyNode.type !== AST_NODE_TYPES.ArrayExpression) {
// TODO support query key factory
return
}
const sourceCode = context.getSourceCode()
const queryKeyValue = queryKeyNode
const refs = ASTUtils.getExternalRefs({
scopeManager,
node: queryFn.value,
})
const existingKeys = ASTUtils.getNestedIdentifiers(queryKeyValue).map(
(identifier) => ASTUtils.mapKeyNodeToText(identifier, sourceCode),
)
const missingRefs = refs
.map((ref) => ({
ref: ref,
text: ASTUtils.mapKeyNodeToText(ref.identifier, sourceCode),
}))
.filter(({ ref, text }) => {
return (
!ref.isTypeReference &&
!ASTUtils.isAncestorIsCallee(ref.identifier) &&
!existingKeys.some((existingKey) => existingKey === text)
)
})
.map(({ ref, text }) => ({
identifier: ref.identifier,
text: text,
}))
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(', '),
},
suggest: [
{
messageId: 'fixTo',
data: { result: existingWithMissing },
fix(fixer) {
return fixer.replaceText(queryKeyValue, existingWithMissing)
},
},
],
})
}
},
}
},
})