@tanstack/eslint-plugin-query 5.94.4 → 5.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/legacy/_tsup-dts-rollup.d.cts +329 -0
- package/build/legacy/_tsup-dts-rollup.d.ts +329 -0
- package/build/legacy/chunk-T44AUBX4.js +1344 -0
- package/build/legacy/chunk-T44AUBX4.js.map +1 -0
- package/build/legacy/index.cjs +1413 -0
- package/build/legacy/index.cjs.map +1 -0
- package/build/legacy/index.d.cts +3 -0
- package/build/legacy/index.d.ts +3 -0
- package/build/legacy/index.js +50 -0
- package/build/legacy/index.js.map +1 -0
- package/build/legacy/rules.cjs +1370 -0
- package/build/legacy/rules.cjs.map +1 -0
- package/build/legacy/rules.d.cts +1 -0
- package/build/legacy/rules.d.ts +1 -0
- package/build/legacy/rules.js +7 -0
- package/build/legacy/rules.js.map +1 -0
- package/build/legacy/types.cjs +19 -0
- package/build/legacy/types.cjs.map +1 -0
- package/build/legacy/types.d.cts +1 -0
- package/build/legacy/types.d.ts +1 -0
- package/build/legacy/types.js +1 -0
- package/build/legacy/types.js.map +1 -0
- package/build/modern/_tsup-dts-rollup.d.cts +329 -0
- package/build/modern/_tsup-dts-rollup.d.ts +329 -0
- package/build/modern/chunk-XH4ABCYA.js +1335 -0
- package/build/modern/chunk-XH4ABCYA.js.map +1 -0
- package/build/modern/index.cjs +1405 -0
- package/build/modern/index.cjs.map +1 -0
- package/build/modern/index.d.cts +3 -0
- package/build/modern/index.d.ts +3 -0
- package/build/modern/index.js +50 -0
- package/build/modern/index.js.map +1 -0
- package/build/modern/rules.cjs +1362 -0
- package/build/modern/rules.cjs.map +1 -0
- package/build/modern/rules.d.cts +1 -0
- package/build/modern/rules.d.ts +1 -0
- package/build/modern/rules.js +7 -0
- package/build/modern/rules.js.map +1 -0
- package/build/modern/types.cjs +20 -0
- package/build/modern/types.cjs.map +1 -0
- package/build/modern/types.d.cts +1 -0
- package/build/modern/types.d.ts +1 -0
- package/build/modern/types.js +1 -0
- package/build/modern/types.js.map +1 -0
- package/package.json +1 -1
- package/src/rules/exhaustive-deps/exhaustive-deps.rule.ts +136 -90
- package/src/rules/exhaustive-deps/exhaustive-deps.utils.ts +248 -22
- package/src/utils/ast-utils.ts +2 -25
|
@@ -14,7 +14,7 @@ export const ExhaustiveDepsUtils = {
|
|
|
14
14
|
const component = ASTUtils.getFunctionAncestor(sourceCode, node)
|
|
15
15
|
const queryFnScope = scopeManager.acquire(node)
|
|
16
16
|
|
|
17
|
-
if (queryFnScope === null) {
|
|
17
|
+
if (queryFnScope === null || reference.isValueReference === false) {
|
|
18
18
|
return false
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -59,24 +59,88 @@ export const ExhaustiveDepsUtils = {
|
|
|
59
59
|
!ExhaustiveDepsUtils.isInstanceOfKind(reference.identifier.parent)
|
|
60
60
|
)
|
|
61
61
|
},
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Given required refs and existing queryKey entries, compute missing dependency paths
|
|
65
|
+
* respecting allowlisted variables and types.
|
|
66
|
+
*/
|
|
67
|
+
computeFilteredMissingPaths(params: {
|
|
68
|
+
requiredRefs: Array<{
|
|
69
|
+
path: string
|
|
70
|
+
root: string
|
|
71
|
+
allowlistedByType: boolean
|
|
72
|
+
}>
|
|
73
|
+
allowlistedVariables: Set<string>
|
|
74
|
+
existingRootIdentifiers: Set<string>
|
|
75
|
+
existingFullPaths: Set<string>
|
|
76
|
+
}): Array<string> {
|
|
77
|
+
const {
|
|
78
|
+
requiredRefs,
|
|
79
|
+
allowlistedVariables,
|
|
80
|
+
existingRootIdentifiers,
|
|
81
|
+
existingFullPaths,
|
|
82
|
+
} = params
|
|
83
|
+
|
|
84
|
+
const missingPaths = new Set<string>()
|
|
85
|
+
|
|
86
|
+
for (const { root, path, allowlistedByType } of requiredRefs) {
|
|
87
|
+
// If root itself is present in the key, it covers all members
|
|
88
|
+
if (existingRootIdentifiers.has(root)) continue
|
|
89
|
+
if (allowlistedVariables.has(root)) continue
|
|
90
|
+
if (existingFullPaths.has(path)) continue
|
|
91
|
+
if (allowlistedByType) continue
|
|
92
|
+
|
|
93
|
+
missingPaths.add(path)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Collapse descendants: if a root is already missing, drop deeper paths
|
|
97
|
+
for (const path of missingPaths) {
|
|
98
|
+
const root = path.split('.')[0]
|
|
99
|
+
if (root !== path && root !== undefined && missingPaths.has(root)) {
|
|
100
|
+
missingPaths.delete(path)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Array.from(missingPaths)
|
|
67
105
|
},
|
|
68
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Extract existing queryKey deps as root identifiers and full member paths.
|
|
109
|
+
*/
|
|
69
110
|
collectQueryKeyDeps(params: {
|
|
70
111
|
sourceCode: Readonly<TSESLint.SourceCode>
|
|
71
112
|
scopeManager: TSESLint.Scope.ScopeManager
|
|
72
113
|
queryKeyNode: TSESTree.Node
|
|
73
|
-
}): Set<string> {
|
|
114
|
+
}): { roots: Set<string>; paths: Set<string> } {
|
|
74
115
|
const { sourceCode, scopeManager, queryKeyNode } = params
|
|
75
|
-
const
|
|
116
|
+
const roots = new Set<string>()
|
|
117
|
+
const paths = new Set<string>()
|
|
76
118
|
const visitorKeys = sourceCode.visitorKeys
|
|
77
119
|
|
|
78
|
-
function
|
|
79
|
-
|
|
120
|
+
function addRoot(name: string) {
|
|
121
|
+
const cleaned = ExhaustiveDepsUtils.normalizeChain(name)
|
|
122
|
+
roots.add(cleaned)
|
|
123
|
+
paths.add(cleaned)
|
|
124
|
+
}
|
|
125
|
+
function addFull(text: string) {
|
|
126
|
+
const cleaned = ExhaustiveDepsUtils.normalizeChain(text)
|
|
127
|
+
paths.add(cleaned)
|
|
128
|
+
}
|
|
129
|
+
function addRefPath(
|
|
130
|
+
refPath: {
|
|
131
|
+
path: string
|
|
132
|
+
root: string
|
|
133
|
+
coversRootMembers: boolean
|
|
134
|
+
} | null,
|
|
135
|
+
) {
|
|
136
|
+
if (!refPath) return
|
|
137
|
+
|
|
138
|
+
if (refPath.coversRootMembers) {
|
|
139
|
+
addRoot(refPath.root)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
addFull(refPath.path)
|
|
80
144
|
}
|
|
81
145
|
|
|
82
146
|
function visitChildren(node: TSESTree.Node): void {
|
|
@@ -106,9 +170,15 @@ export const ExhaustiveDepsUtils = {
|
|
|
106
170
|
if (!node) return
|
|
107
171
|
|
|
108
172
|
switch (node.type) {
|
|
109
|
-
case AST_NODE_TYPES.Identifier:
|
|
110
|
-
|
|
173
|
+
case AST_NODE_TYPES.Identifier: {
|
|
174
|
+
addRefPath(
|
|
175
|
+
ExhaustiveDepsUtils.computeRefPath({
|
|
176
|
+
identifier: node,
|
|
177
|
+
sourceCode: sourceCode,
|
|
178
|
+
}),
|
|
179
|
+
)
|
|
111
180
|
return
|
|
181
|
+
}
|
|
112
182
|
case AST_NODE_TYPES.ArrowFunctionExpression:
|
|
113
183
|
case AST_NODE_TYPES.FunctionExpression:
|
|
114
184
|
for (const reference of ExhaustiveDepsUtils.collectExternalRefsInFunction(
|
|
@@ -117,9 +187,16 @@ export const ExhaustiveDepsUtils = {
|
|
|
117
187
|
scopeManager: scopeManager,
|
|
118
188
|
},
|
|
119
189
|
)) {
|
|
120
|
-
if (reference.identifier.type
|
|
121
|
-
|
|
190
|
+
if (reference.identifier.type !== AST_NODE_TYPES.Identifier) {
|
|
191
|
+
continue
|
|
122
192
|
}
|
|
193
|
+
|
|
194
|
+
addRefPath(
|
|
195
|
+
ExhaustiveDepsUtils.computeRefPath({
|
|
196
|
+
identifier: reference.identifier,
|
|
197
|
+
sourceCode: sourceCode,
|
|
198
|
+
}),
|
|
199
|
+
)
|
|
123
200
|
}
|
|
124
201
|
return
|
|
125
202
|
case AST_NODE_TYPES.Property:
|
|
@@ -131,19 +208,20 @@ export const ExhaustiveDepsUtils = {
|
|
|
131
208
|
node.parent.callee === node &&
|
|
132
209
|
node.object.type === AST_NODE_TYPES.Identifier
|
|
133
210
|
) {
|
|
134
|
-
|
|
211
|
+
addRoot(node.object.name)
|
|
135
212
|
} else {
|
|
136
213
|
visit(node.object)
|
|
137
214
|
}
|
|
138
215
|
return
|
|
139
216
|
case AST_NODE_TYPES.CallExpression:
|
|
140
217
|
node.arguments.forEach((argument) => visit(argument))
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
218
|
+
switch (node.callee.type) {
|
|
219
|
+
case AST_NODE_TYPES.Identifier:
|
|
220
|
+
case AST_NODE_TYPES.MemberExpression:
|
|
221
|
+
case AST_NODE_TYPES.ChainExpression:
|
|
222
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
223
|
+
visit(node.callee)
|
|
224
|
+
break
|
|
147
225
|
}
|
|
148
226
|
return
|
|
149
227
|
}
|
|
@@ -153,7 +231,7 @@ export const ExhaustiveDepsUtils = {
|
|
|
153
231
|
|
|
154
232
|
visit(queryKeyNode)
|
|
155
233
|
|
|
156
|
-
return
|
|
234
|
+
return { roots, paths }
|
|
157
235
|
},
|
|
158
236
|
|
|
159
237
|
isNode(value: unknown): value is TSESTree.Node {
|
|
@@ -165,6 +243,97 @@ export const ExhaustiveDepsUtils = {
|
|
|
165
243
|
)
|
|
166
244
|
},
|
|
167
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Checks whether the resolved variable is allowlisted by its type annotation
|
|
248
|
+
*/
|
|
249
|
+
variableIsAllowlistedByType(params: {
|
|
250
|
+
allowlistedTypes: Set<string>
|
|
251
|
+
variable: TSESLint.Scope.Variable | null
|
|
252
|
+
}): boolean {
|
|
253
|
+
const { allowlistedTypes, variable } = params
|
|
254
|
+
if (allowlistedTypes.size === 0) return false
|
|
255
|
+
if (!variable) return false
|
|
256
|
+
|
|
257
|
+
for (const id of variable.identifiers) {
|
|
258
|
+
if (id.typeAnnotation) {
|
|
259
|
+
const typeIdentifiers = new Set<string>()
|
|
260
|
+
ExhaustiveDepsUtils.collectTypeIdentifiers(
|
|
261
|
+
id.typeAnnotation.typeAnnotation,
|
|
262
|
+
typeIdentifiers,
|
|
263
|
+
)
|
|
264
|
+
for (const typeIdentifier of typeIdentifiers) {
|
|
265
|
+
if (allowlistedTypes.has(typeIdentifier)) return true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return false
|
|
271
|
+
},
|
|
272
|
+
isInstanceOfKind(node: TSESTree.Node) {
|
|
273
|
+
return (
|
|
274
|
+
node.type === AST_NODE_TYPES.BinaryExpression &&
|
|
275
|
+
node.operator === 'instanceof'
|
|
276
|
+
)
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Normalizes a chain by removing optional chaining operators
|
|
281
|
+
*
|
|
282
|
+
* Example: `a?.b.c!` -> `a.b.c`
|
|
283
|
+
*/
|
|
284
|
+
normalizeChain(text: string): string {
|
|
285
|
+
return text.replace(/(?:\?(\.)|!)/g, '$1')
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Computes the reference path for an identifier
|
|
290
|
+
*
|
|
291
|
+
* Example: `a.b.c!` -> `{ path: 'a.b.c', root: 'a' }`
|
|
292
|
+
*/
|
|
293
|
+
computeRefPath(params: {
|
|
294
|
+
identifier: TSESTree.Identifier
|
|
295
|
+
sourceCode: Readonly<TSESLint.SourceCode>
|
|
296
|
+
}): { path: string; root: string; coversRootMembers: boolean } | null {
|
|
297
|
+
const { identifier, sourceCode } = params
|
|
298
|
+
|
|
299
|
+
const fullChainNode = ASTUtils.traverseUpOnly(identifier, [
|
|
300
|
+
AST_NODE_TYPES.MemberExpression,
|
|
301
|
+
AST_NODE_TYPES.TSNonNullExpression,
|
|
302
|
+
AST_NODE_TYPES.Identifier,
|
|
303
|
+
])
|
|
304
|
+
|
|
305
|
+
const fullText = ExhaustiveDepsUtils.normalizeChain(
|
|
306
|
+
sourceCode.getText(fullChainNode),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
const parent = fullChainNode.parent
|
|
310
|
+
let dependencyPath = fullText
|
|
311
|
+
let coversRootMembers = fullText === identifier.name
|
|
312
|
+
|
|
313
|
+
if (
|
|
314
|
+
parent &&
|
|
315
|
+
parent.type === AST_NODE_TYPES.CallExpression &&
|
|
316
|
+
parent.callee === fullChainNode
|
|
317
|
+
) {
|
|
318
|
+
const segments = fullText.split('.')
|
|
319
|
+
if (segments.length > 1) {
|
|
320
|
+
dependencyPath = segments.slice(0, -1).join('.')
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
coversRootMembers = false
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
dependencyPath =
|
|
327
|
+
dependencyPath.split('.')[0] === '' ? identifier.name : dependencyPath
|
|
328
|
+
const root = dependencyPath.split('.')[0]
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
path: dependencyPath,
|
|
332
|
+
root: root ?? identifier.name,
|
|
333
|
+
coversRootMembers: coversRootMembers && dependencyPath === root,
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
|
|
168
337
|
collectExternalRefsInFunction(params: {
|
|
169
338
|
functionNode: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression
|
|
170
339
|
scopeManager: TSESLint.Scope.ScopeManager
|
|
@@ -210,4 +379,61 @@ export const ExhaustiveDepsUtils = {
|
|
|
210
379
|
|
|
211
380
|
return externalRefs
|
|
212
381
|
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Recursively collects type identifiers from a type annotation
|
|
385
|
+
*/
|
|
386
|
+
collectTypeIdentifiers(typeNode: TSESTree.TypeNode, out: Set<string>): void {
|
|
387
|
+
switch (typeNode.type) {
|
|
388
|
+
case AST_NODE_TYPES.TSTypeReference: {
|
|
389
|
+
if (typeNode.typeName.type === AST_NODE_TYPES.Identifier) {
|
|
390
|
+
out.add(typeNode.typeName.name)
|
|
391
|
+
}
|
|
392
|
+
break
|
|
393
|
+
}
|
|
394
|
+
case AST_NODE_TYPES.TSUnionType:
|
|
395
|
+
case AST_NODE_TYPES.TSIntersectionType: {
|
|
396
|
+
typeNode.types.forEach((t) =>
|
|
397
|
+
ExhaustiveDepsUtils.collectTypeIdentifiers(t, out),
|
|
398
|
+
)
|
|
399
|
+
break
|
|
400
|
+
}
|
|
401
|
+
case AST_NODE_TYPES.TSArrayType: {
|
|
402
|
+
ExhaustiveDepsUtils.collectTypeIdentifiers(typeNode.elementType, out)
|
|
403
|
+
break
|
|
404
|
+
}
|
|
405
|
+
case AST_NODE_TYPES.TSTupleType: {
|
|
406
|
+
typeNode.elementTypes.forEach((et) =>
|
|
407
|
+
ExhaustiveDepsUtils.collectTypeIdentifiers(et, out),
|
|
408
|
+
)
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Gets the function expression nodes from a queryFn property, handling conditional expressions.
|
|
416
|
+
* When neither branch is skipToken, returns both branches so all deps are scanned.
|
|
417
|
+
*/
|
|
418
|
+
getQueryFnNodes(queryFn: TSESTree.Property): Array<TSESTree.Node> {
|
|
419
|
+
if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {
|
|
420
|
+
return [queryFn.value]
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (
|
|
424
|
+
queryFn.value.consequent.type === AST_NODE_TYPES.Identifier &&
|
|
425
|
+
queryFn.value.consequent.name === 'skipToken'
|
|
426
|
+
) {
|
|
427
|
+
return [queryFn.value.alternate]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (
|
|
431
|
+
queryFn.value.alternate.type === AST_NODE_TYPES.Identifier &&
|
|
432
|
+
queryFn.value.alternate.name === 'skipToken'
|
|
433
|
+
) {
|
|
434
|
+
return [queryFn.value.consequent]
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return [queryFn.value.consequent, queryFn.value.alternate]
|
|
438
|
+
},
|
|
213
439
|
}
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -42,10 +42,9 @@ export const ASTUtils = {
|
|
|
42
42
|
properties: Array<TSESTree.ObjectLiteralElement>,
|
|
43
43
|
key: string,
|
|
44
44
|
): TSESTree.Property | undefined {
|
|
45
|
-
|
|
46
|
-
return properties.find((x) =>
|
|
45
|
+
return properties.find((x): x is TSESTree.Property =>
|
|
47
46
|
ASTUtils.isPropertyWithIdentifierKey(x, key),
|
|
48
|
-
)
|
|
47
|
+
)
|
|
49
48
|
},
|
|
50
49
|
getNestedIdentifiers(node: TSESTree.Node): Array<TSESTree.Identifier> {
|
|
51
50
|
const identifiers: Array<TSESTree.Identifier> = []
|
|
@@ -132,28 +131,6 @@ export const ASTUtils = {
|
|
|
132
131
|
|
|
133
132
|
return identifiers
|
|
134
133
|
},
|
|
135
|
-
isAncestorIsCallee(identifier: TSESTree.Node) {
|
|
136
|
-
let previousNode = identifier
|
|
137
|
-
let currentNode = identifier.parent
|
|
138
|
-
|
|
139
|
-
while (currentNode !== undefined) {
|
|
140
|
-
if (
|
|
141
|
-
currentNode.type === AST_NODE_TYPES.CallExpression &&
|
|
142
|
-
currentNode.callee === previousNode
|
|
143
|
-
) {
|
|
144
|
-
return true
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (currentNode.type !== AST_NODE_TYPES.MemberExpression) {
|
|
148
|
-
return false
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
previousNode = currentNode
|
|
152
|
-
currentNode = currentNode.parent
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return false
|
|
156
|
-
},
|
|
157
134
|
traverseUpOnly(
|
|
158
135
|
identifier: TSESTree.Node,
|
|
159
136
|
allowedNodeTypes: Array<AST_NODE_TYPES>,
|