@tanstack/eslint-plugin-query 5.0.0-alpha.6 → 5.0.0-alpha.68

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.
Files changed (45) hide show
  1. package/build/lib/configs/index.d.ts +1 -0
  2. package/build/lib/configs/index.d.ts.map +1 -0
  3. package/build/lib/configs/index.test.d.ts +1 -0
  4. package/build/lib/configs/index.test.d.ts.map +1 -0
  5. package/build/lib/index.cjs +465 -0
  6. package/build/lib/index.cjs.map +1 -0
  7. package/build/lib/index.d.ts +1 -0
  8. package/build/lib/index.d.ts.map +1 -0
  9. package/build/lib/index.js +437 -6
  10. package/build/lib/index.js.map +1 -0
  11. package/build/lib/rules/exhaustive-deps/exhaustive-deps.rule.d.ts +1 -0
  12. package/build/lib/rules/exhaustive-deps/exhaustive-deps.rule.d.ts.map +1 -0
  13. package/build/lib/rules/exhaustive-deps/exhaustive-deps.test.d.ts +1 -0
  14. package/build/lib/rules/exhaustive-deps/exhaustive-deps.test.d.ts.map +1 -0
  15. package/build/lib/rules/exhaustive-deps/exhaustive-deps.utils.d.ts +10 -0
  16. package/build/lib/rules/exhaustive-deps/exhaustive-deps.utils.d.ts.map +1 -0
  17. package/build/lib/rules/index.d.ts +1 -0
  18. package/build/lib/rules/index.d.ts.map +1 -0
  19. package/build/lib/utils/ast-utils.d.ts +9 -0
  20. package/build/lib/utils/ast-utils.d.ts.map +1 -0
  21. package/build/lib/utils/create-rule.d.ts +2 -1
  22. package/build/lib/utils/create-rule.d.ts.map +1 -0
  23. package/build/lib/utils/detect-react-query-imports.d.ts +6 -5
  24. package/build/lib/utils/detect-react-query-imports.d.ts.map +1 -0
  25. package/build/lib/utils/object-utils.d.ts +1 -0
  26. package/build/lib/utils/object-utils.d.ts.map +1 -0
  27. package/build/lib/utils/test-utils.d.ts +1 -0
  28. package/build/lib/utils/test-utils.d.ts.map +1 -0
  29. package/build/lib/utils/unique-by.d.ts +1 -0
  30. package/build/lib/utils/unique-by.d.ts.map +1 -0
  31. package/package.json +29 -22
  32. package/src/configs/index.test.ts +18 -0
  33. package/src/configs/index.ts +22 -0
  34. package/src/index.ts +2 -0
  35. package/src/rules/exhaustive-deps/exhaustive-deps.rule.ts +154 -0
  36. package/src/rules/exhaustive-deps/exhaustive-deps.test.ts +721 -0
  37. package/src/rules/exhaustive-deps/exhaustive-deps.utils.ts +41 -0
  38. package/src/rules/index.ts +5 -0
  39. package/src/utils/ast-utils.ts +318 -0
  40. package/src/utils/create-rule.ts +20 -0
  41. package/src/utils/detect-react-query-imports.ts +75 -0
  42. package/src/utils/object-utils.ts +5 -0
  43. package/src/utils/test-utils.ts +5 -0
  44. package/src/utils/unique-by.ts +3 -0
  45. package/build/.tsbuildinfo +0 -1
@@ -0,0 +1,41 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils'
2
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils'
3
+ import { ASTUtils } from '../../utils/ast-utils'
4
+
5
+ export const ExhaustiveDepsUtils = {
6
+ isRelevantReference(params: {
7
+ context: Readonly<TSESLint.RuleContext<string, readonly unknown[]>>
8
+ reference: TSESLint.Scope.Reference
9
+ scopeManager: TSESLint.Scope.ScopeManager
10
+ }) {
11
+ const { reference, scopeManager, context } = params
12
+ const component = ASTUtils.getFunctionAncestor(context)
13
+
14
+ if (
15
+ component !== undefined &&
16
+ !ASTUtils.isDeclaredInNode({
17
+ scopeManager,
18
+ reference,
19
+ functionNode: component,
20
+ })
21
+ ) {
22
+ return false
23
+ }
24
+
25
+ return (
26
+ reference.identifier.name !== 'undefined' &&
27
+ reference.identifier.parent?.type !== AST_NODE_TYPES.NewExpression &&
28
+ !ExhaustiveDepsUtils.isQueryClientReference(reference)
29
+ )
30
+ },
31
+ isQueryClientReference(reference: TSESLint.Scope.Reference) {
32
+ const declarator = reference.resolved?.defs[0]?.node
33
+
34
+ return (
35
+ declarator?.type === AST_NODE_TYPES.VariableDeclarator &&
36
+ declarator.init?.type === AST_NODE_TYPES.CallExpression &&
37
+ declarator.init.callee.type === AST_NODE_TYPES.Identifier &&
38
+ declarator.init.callee.name === 'useQueryClient'
39
+ )
40
+ },
41
+ }
@@ -0,0 +1,5 @@
1
+ import * as exhaustiveDeps from './exhaustive-deps/exhaustive-deps.rule'
2
+
3
+ export const rules = {
4
+ [exhaustiveDeps.name]: exhaustiveDeps.rule,
5
+ }
@@ -0,0 +1,318 @@
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
2
+ import type TSESLintScopeManager from '@typescript-eslint/scope-manager'
3
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils'
4
+ import type { RuleContext } from '@typescript-eslint/utils/dist/ts-eslint'
5
+ import { uniqueBy } from './unique-by'
6
+
7
+ export const ASTUtils = {
8
+ isNodeOfOneOf<T extends AST_NODE_TYPES>(
9
+ node: TSESTree.Node,
10
+ types: readonly T[],
11
+ ): node is TSESTree.Node & { type: T } {
12
+ return types.includes(node.type as T)
13
+ },
14
+ isIdentifier(node: TSESTree.Node): node is TSESTree.Identifier {
15
+ return node.type === AST_NODE_TYPES.Identifier
16
+ },
17
+ isIdentifierWithName(
18
+ node: TSESTree.Node,
19
+ name: string,
20
+ ): node is TSESTree.Identifier {
21
+ return ASTUtils.isIdentifier(node) && node.name === name
22
+ },
23
+ isIdentifierWithOneOfNames<T extends string[]>(
24
+ node: TSESTree.Node,
25
+ name: T,
26
+ ): node is TSESTree.Identifier & { name: T[number] } {
27
+ return ASTUtils.isIdentifier(node) && name.includes(node.name)
28
+ },
29
+ isProperty(node: TSESTree.Node): node is TSESTree.Property {
30
+ return node.type === AST_NODE_TYPES.Property
31
+ },
32
+ isObjectExpression(node: TSESTree.Node): node is TSESTree.ObjectExpression {
33
+ return node.type === AST_NODE_TYPES.ObjectExpression
34
+ },
35
+ isPropertyWithIdentifierKey(
36
+ node: TSESTree.Node,
37
+ key: string,
38
+ ): node is TSESTree.Property {
39
+ return (
40
+ ASTUtils.isProperty(node) && ASTUtils.isIdentifierWithName(node.key, key)
41
+ )
42
+ },
43
+ findPropertyWithIdentifierKey(
44
+ properties: TSESTree.ObjectLiteralElement[],
45
+ key: string,
46
+ ): TSESTree.Property | undefined {
47
+ return properties.find((x) =>
48
+ ASTUtils.isPropertyWithIdentifierKey(x, key),
49
+ ) as TSESTree.Property | undefined
50
+ },
51
+ getNestedIdentifiers(node: TSESTree.Node): TSESTree.Identifier[] {
52
+ const identifiers: TSESTree.Identifier[] = []
53
+
54
+ if (ASTUtils.isIdentifier(node)) {
55
+ identifiers.push(node)
56
+ }
57
+
58
+ if ('arguments' in node) {
59
+ node.arguments.forEach((x) => {
60
+ identifiers.push(...ASTUtils.getNestedIdentifiers(x))
61
+ })
62
+ }
63
+
64
+ if ('elements' in node) {
65
+ node.elements.forEach((x) => {
66
+ if (x !== null) {
67
+ identifiers.push(...ASTUtils.getNestedIdentifiers(x))
68
+ }
69
+ })
70
+ }
71
+
72
+ if ('properties' in node) {
73
+ node.properties.forEach((x) => {
74
+ identifiers.push(...ASTUtils.getNestedIdentifiers(x))
75
+ })
76
+ }
77
+
78
+ if ('expressions' in node) {
79
+ node.expressions.forEach((x) => {
80
+ identifiers.push(...ASTUtils.getNestedIdentifiers(x))
81
+ })
82
+ }
83
+
84
+ if (node.type === AST_NODE_TYPES.Property) {
85
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.value))
86
+ }
87
+
88
+ if (node.type === AST_NODE_TYPES.SpreadElement) {
89
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument))
90
+ }
91
+
92
+ if (node.type === AST_NODE_TYPES.MemberExpression) {
93
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.object))
94
+ }
95
+
96
+ if (node.type === AST_NODE_TYPES.UnaryExpression) {
97
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.argument))
98
+ }
99
+
100
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
101
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression))
102
+ }
103
+
104
+ if (node.type === AST_NODE_TYPES.TSNonNullExpression) {
105
+ identifiers.push(...ASTUtils.getNestedIdentifiers(node.expression))
106
+ }
107
+
108
+ return identifiers
109
+ },
110
+ isAncestorIsCallee(identifier: TSESTree.Node) {
111
+ let previousNode = identifier
112
+ let currentNode = identifier.parent
113
+
114
+ while (currentNode !== undefined) {
115
+ if (
116
+ currentNode.type === AST_NODE_TYPES.CallExpression &&
117
+ currentNode.callee === previousNode
118
+ ) {
119
+ return true
120
+ }
121
+
122
+ if (currentNode.type !== AST_NODE_TYPES.MemberExpression) {
123
+ return false
124
+ }
125
+
126
+ previousNode = currentNode
127
+ currentNode = currentNode.parent
128
+ }
129
+
130
+ return false
131
+ },
132
+ traverseUpOnly(
133
+ identifier: TSESTree.Node,
134
+ allowedNodeTypes: AST_NODE_TYPES[],
135
+ ): TSESTree.Node {
136
+ const parent = identifier.parent
137
+
138
+ if (parent !== undefined && allowedNodeTypes.includes(parent.type)) {
139
+ return ASTUtils.traverseUpOnly(parent, allowedNodeTypes)
140
+ }
141
+
142
+ return identifier
143
+ },
144
+ isDeclaredInNode(params: {
145
+ functionNode: TSESTree.Node
146
+ reference: TSESLintScopeManager.Reference
147
+ scopeManager: TSESLint.Scope.ScopeManager
148
+ }) {
149
+ const { functionNode, reference, scopeManager } = params
150
+ const scope = scopeManager.acquire(functionNode)
151
+
152
+ if (scope === null) {
153
+ return false
154
+ }
155
+
156
+ return scope.set.has(reference.identifier.name)
157
+ },
158
+ getExternalRefs(params: {
159
+ scopeManager: TSESLint.Scope.ScopeManager
160
+ sourceCode: Readonly<TSESLint.SourceCode>
161
+ node: TSESTree.Node
162
+ }): TSESLint.Scope.Reference[] {
163
+ const { scopeManager, sourceCode, node } = params
164
+ const scope = scopeManager.acquire(node)
165
+
166
+ if (scope === null) {
167
+ return []
168
+ }
169
+
170
+ const references = scope.references
171
+ .filter((x) => x.isRead() && !scope.set.has(x.identifier.name))
172
+ .map((x) => {
173
+ const referenceNode = ASTUtils.traverseUpOnly(x.identifier, [
174
+ AST_NODE_TYPES.MemberExpression,
175
+ AST_NODE_TYPES.Identifier,
176
+ ])
177
+
178
+ return {
179
+ variable: x,
180
+ node: referenceNode,
181
+ text: sourceCode.getText(referenceNode),
182
+ }
183
+ })
184
+
185
+ const localRefIds = new Set(
186
+ [...scope.set.values()].map((x) => sourceCode.getText(x.identifiers[0])),
187
+ )
188
+
189
+ const externalRefs = references.filter(
190
+ (x) => x.variable.resolved === null || !localRefIds.has(x.text),
191
+ )
192
+
193
+ return uniqueBy(externalRefs, (x) => x.text).map((x) => x.variable)
194
+ },
195
+ mapKeyNodeToText(
196
+ node: TSESTree.Node,
197
+ sourceCode: Readonly<TSESLint.SourceCode>,
198
+ ) {
199
+ return sourceCode.getText(
200
+ ASTUtils.traverseUpOnly(node, [
201
+ AST_NODE_TYPES.MemberExpression,
202
+ AST_NODE_TYPES.Identifier,
203
+ ]),
204
+ )
205
+ },
206
+ isValidReactComponentOrHookName(identifier: TSESTree.Identifier | null) {
207
+ return identifier !== null && /^(use|[A-Z])/.test(identifier.name)
208
+ },
209
+ getFunctionAncestor(
210
+ context: Readonly<RuleContext<string, readonly unknown[]>>,
211
+ ) {
212
+ return context.getAncestors().find((x) => {
213
+ if (x.type === AST_NODE_TYPES.FunctionDeclaration) {
214
+ return true
215
+ }
216
+
217
+ return (
218
+ x.parent?.type === AST_NODE_TYPES.VariableDeclarator &&
219
+ x.parent.id.type === AST_NODE_TYPES.Identifier &&
220
+ ASTUtils.isNodeOfOneOf(x, [
221
+ AST_NODE_TYPES.FunctionDeclaration,
222
+ AST_NODE_TYPES.FunctionExpression,
223
+ AST_NODE_TYPES.ArrowFunctionExpression,
224
+ ])
225
+ )
226
+ })
227
+ },
228
+ getReferencedExpressionByIdentifier(params: {
229
+ node: TSESTree.Node
230
+ context: Readonly<RuleContext<string, readonly unknown[]>>
231
+ }) {
232
+ const { node, context } = params
233
+
234
+ const resolvedNode = context
235
+ .getScope()
236
+ .references.find((ref) => ref.identifier === node)?.resolved
237
+ ?.defs[0]?.node
238
+
239
+ if (resolvedNode?.type !== AST_NODE_TYPES.VariableDeclarator) {
240
+ return null
241
+ }
242
+
243
+ return resolvedNode.init
244
+ },
245
+ getNestedReturnStatements(node: TSESTree.Node): TSESTree.ReturnStatement[] {
246
+ const returnStatements: TSESTree.ReturnStatement[] = []
247
+
248
+ if (node.type === AST_NODE_TYPES.ReturnStatement) {
249
+ returnStatements.push(node)
250
+ }
251
+
252
+ if ('body' in node && node.body !== undefined && node.body !== null) {
253
+ Array.isArray(node.body)
254
+ ? node.body.forEach((x) => {
255
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(x))
256
+ })
257
+ : returnStatements.push(
258
+ ...ASTUtils.getNestedReturnStatements(node.body),
259
+ )
260
+ }
261
+
262
+ if ('consequent' in node) {
263
+ Array.isArray(node.consequent)
264
+ ? node.consequent.forEach((x) => {
265
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(x))
266
+ })
267
+ : returnStatements.push(
268
+ ...ASTUtils.getNestedReturnStatements(node.consequent),
269
+ )
270
+ }
271
+
272
+ if ('alternate' in node && node.alternate !== null) {
273
+ Array.isArray(node.alternate)
274
+ ? node.alternate.forEach((x) => {
275
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(x))
276
+ })
277
+ : returnStatements.push(
278
+ ...ASTUtils.getNestedReturnStatements(node.alternate),
279
+ )
280
+ }
281
+
282
+ if ('cases' in node) {
283
+ node.cases.forEach((x) => {
284
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(x))
285
+ })
286
+ }
287
+
288
+ if ('block' in node) {
289
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(node.block))
290
+ }
291
+
292
+ if ('handler' in node && node.handler !== null) {
293
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(node.handler))
294
+ }
295
+
296
+ if ('finalizer' in node && node.finalizer !== null) {
297
+ returnStatements.push(
298
+ ...ASTUtils.getNestedReturnStatements(node.finalizer),
299
+ )
300
+ }
301
+
302
+ if (
303
+ 'expression' in node &&
304
+ node.expression !== true &&
305
+ node.expression !== false
306
+ ) {
307
+ returnStatements.push(
308
+ ...ASTUtils.getNestedReturnStatements(node.expression),
309
+ )
310
+ }
311
+
312
+ if ('test' in node && node.test !== null) {
313
+ returnStatements.push(...ASTUtils.getNestedReturnStatements(node.test))
314
+ }
315
+
316
+ return returnStatements
317
+ },
318
+ }
@@ -0,0 +1,20 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils'
2
+ import type { EnhancedCreate } from './detect-react-query-imports'
3
+ import { detectTanstackQueryImports } from './detect-react-query-imports'
4
+
5
+ const getDocsUrl = (ruleName: string): string =>
6
+ `https://tanstack.com/query/v4/docs/eslint/${ruleName}`
7
+
8
+ type EslintRule = Omit<
9
+ Parameters<ReturnType<typeof ESLintUtils.RuleCreator>>[0],
10
+ 'create'
11
+ > & {
12
+ create: EnhancedCreate
13
+ }
14
+
15
+ export function createRule({ create, ...rest }: EslintRule) {
16
+ return ESLintUtils.RuleCreator(getDocsUrl)({
17
+ ...rest,
18
+ create: detectTanstackQueryImports(create),
19
+ })
20
+ }
@@ -0,0 +1,75 @@
1
+ import type { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils'
2
+
3
+ type Create = Parameters<
4
+ ReturnType<typeof ESLintUtils.RuleCreator>
5
+ >[0]['create']
6
+
7
+ type Context = Parameters<Create>[0]
8
+ type Options = Parameters<Create>[1]
9
+ type Helpers = {
10
+ isTanstackQueryImport: (node: TSESTree.Identifier) => boolean
11
+ }
12
+
13
+ export type EnhancedCreate = (
14
+ context: Context,
15
+ options: Options,
16
+ helpers: Helpers,
17
+ ) => ReturnType<Create>
18
+
19
+ export function detectTanstackQueryImports(create: EnhancedCreate): Create {
20
+ return (context, optionsWithDefault) => {
21
+ const tanstackQueryImportSpecifiers: TSESTree.ImportClause[] = []
22
+
23
+ const helpers: Helpers = {
24
+ isTanstackQueryImport(node) {
25
+ return !!tanstackQueryImportSpecifiers.find((specifier) => {
26
+ if (specifier.type === 'ImportSpecifier') {
27
+ return node.name === specifier.local.name
28
+ }
29
+
30
+ return false
31
+ })
32
+ },
33
+ }
34
+
35
+ const detectionInstructions: TSESLint.RuleListener = {
36
+ ImportDeclaration(node) {
37
+ if (
38
+ node.specifiers.length > 0 &&
39
+ node.importKind === 'value' &&
40
+ node.source.value.startsWith('@tanstack/') &&
41
+ node.source.value.endsWith('-query')
42
+ ) {
43
+ tanstackQueryImportSpecifiers.push(...node.specifiers)
44
+ }
45
+ },
46
+ }
47
+
48
+ // Call original rule definition
49
+ const ruleInstructions = create(context, optionsWithDefault, helpers)
50
+ const enhancedRuleInstructions: TSESLint.RuleListener = {}
51
+
52
+ const allKeys = new Set(
53
+ Object.keys(detectionInstructions).concat(Object.keys(ruleInstructions)),
54
+ )
55
+
56
+ // Iterate over ALL instructions keys so we can override original rule instructions
57
+ // to prevent their execution if conditions to report errors are not met.
58
+ allKeys.forEach((instruction) => {
59
+ enhancedRuleInstructions[instruction] = (node) => {
60
+ if (instruction in detectionInstructions) {
61
+ detectionInstructions[instruction]?.(node)
62
+ }
63
+
64
+ // TODO: canReportErrors()
65
+ if (ruleInstructions[instruction]) {
66
+ return ruleInstructions[instruction]?.(node)
67
+ }
68
+
69
+ return undefined
70
+ }
71
+ })
72
+
73
+ return enhancedRuleInstructions
74
+ }
75
+ }
@@ -0,0 +1,5 @@
1
+ export function objectKeys<T extends Record<string, unknown>>(
2
+ obj: T,
3
+ ): Array<keyof T> {
4
+ return Object.keys(obj) as Array<keyof T>
5
+ }
@@ -0,0 +1,5 @@
1
+ export function normalizeIndent(template: TemplateStringsArray) {
2
+ const codeLines = template[0]?.split('\n') ?? ['']
3
+ const leftPadding = codeLines[1]?.match(/\s+/)?.[0] ?? ''
4
+ return codeLines.map((line) => line.slice(leftPadding.length)).join('\n')
5
+ }
@@ -0,0 +1,3 @@
1
+ export function uniqueBy<T>(arr: T[], fn: (x: T) => unknown): T[] {
2
+ return arr.filter((x, i, a) => a.findIndex((y) => fn(x) === fn(y)) === i)
3
+ }