@tanstack/eslint-plugin-query 5.94.5 → 5.95.1

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.
@@ -1,7 +1,6 @@
1
1
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils'
2
2
  import { ASTUtils } from '../../utils/ast-utils'
3
3
  import { getDocsUrl } from '../../utils/get-docs-url'
4
- import { uniqueBy } from '../../utils/unique-by'
5
4
  import { detectTanstackQueryImports } from '../../utils/detect-react-query-imports'
6
5
  import { ExhaustiveDepsUtils } from './exhaustive-deps.utils'
7
6
  import type { TSESLint, TSESTree } from '@typescript-eslint/utils'
@@ -14,6 +13,13 @@ export const name = 'exhaustive-deps'
14
13
 
15
14
  const createRule = ESLintUtils.RuleCreator<ExtraRuleDocs>(getDocsUrl)
16
15
 
16
+ type RuleOption = {
17
+ allowlist?: {
18
+ variables?: Array<string>
19
+ types?: Array<string>
20
+ }
21
+ }
22
+
17
23
  export const rule = createRule({
18
24
  name,
19
25
  meta: {
@@ -28,27 +34,36 @@ export const rule = createRule({
28
34
  },
29
35
  hasSuggestions: true,
30
36
  fixable: 'code',
31
- schema: [],
37
+ schema: [
38
+ {
39
+ type: 'object',
40
+ properties: {
41
+ allowlist: {
42
+ type: 'object',
43
+ properties: {
44
+ variables: { type: 'array', items: { type: 'string' } },
45
+ types: { type: 'array', items: { type: 'string' } },
46
+ },
47
+ additionalProperties: false,
48
+ },
49
+ },
50
+ additionalProperties: false,
51
+ },
52
+ ],
32
53
  },
33
54
  defaultOptions: [],
34
55
 
35
56
  create: detectTanstackQueryImports((context) => {
36
57
  return {
37
- Property: (node) => {
38
- if (
39
- !ASTUtils.isObjectExpression(node.parent) ||
40
- !ASTUtils.isIdentifierWithName(node.key, QUERY_KEY)
41
- ) {
42
- return
43
- }
44
-
58
+ ObjectExpression: (node: TSESTree.ObjectExpression) => {
45
59
  const scopeManager = context.sourceCode.scopeManager
60
+
46
61
  const queryKey = ASTUtils.findPropertyWithIdentifierKey(
47
- node.parent.properties,
62
+ node.properties,
48
63
  QUERY_KEY,
49
64
  )
50
65
  const queryFn = ASTUtils.findPropertyWithIdentifierKey(
51
- node.parent.properties,
66
+ node.properties,
52
67
  QUERY_FN,
53
68
  )
54
69
 
@@ -70,103 +85,134 @@ export const rule = createRule({
70
85
  context,
71
86
  )
72
87
 
73
- const externalRefs = ASTUtils.getExternalRefs({
74
- scopeManager,
75
- sourceCode: context.sourceCode,
76
- node: getQueryFnRelevantNode(queryFn),
77
- })
88
+ const queryFnNodes = ExhaustiveDepsUtils.getQueryFnNodes(queryFn)
78
89
 
79
- const relevantRefs = externalRefs.filter((reference) =>
80
- ExhaustiveDepsUtils.isRelevantReference({
81
- sourceCode: context.sourceCode,
82
- reference,
90
+ const externalRefs = queryFnNodes.flatMap((fnNode) =>
91
+ ASTUtils.getExternalRefs({
83
92
  scopeManager,
84
- node: getQueryFnRelevantNode(queryFn),
85
- filename: context.filename,
93
+ sourceCode: context.sourceCode,
94
+ node: fnNode,
86
95
  }),
87
96
  )
88
97
 
98
+ const relevantRefs = externalRefs.filter((reference) =>
99
+ queryFnNodes.some((fnNode) =>
100
+ ExhaustiveDepsUtils.isRelevantReference({
101
+ sourceCode: context.sourceCode,
102
+ reference,
103
+ scopeManager,
104
+ node: fnNode,
105
+ filename: context.filename,
106
+ }),
107
+ ),
108
+ )
109
+
110
+ const ruleOptions = context.options.at(0) as RuleOption | undefined
111
+ const allowlistedVariables = new Set(
112
+ ruleOptions?.allowlist?.variables ?? [],
113
+ )
114
+ const allowlistedTypes = new Set(ruleOptions?.allowlist?.types ?? [])
115
+
116
+ const requiredRefs = relevantRefs.flatMap((ref) => {
117
+ if (ref.identifier.type !== AST_NODE_TYPES.Identifier) return []
118
+
119
+ const refPath = ExhaustiveDepsUtils.computeRefPath({
120
+ identifier: ref.identifier,
121
+ sourceCode: context.sourceCode,
122
+ })
123
+
124
+ if (refPath === null) return []
125
+
126
+ return [
127
+ {
128
+ ...refPath,
129
+ allowlistedByType:
130
+ ExhaustiveDepsUtils.variableIsAllowlistedByType({
131
+ allowlistedTypes,
132
+ variable: ref.resolved ?? null,
133
+ }),
134
+ },
135
+ ]
136
+ })
137
+
138
+ if (requiredRefs.length === 0) return
139
+
89
140
  const queryKeyDeps = ExhaustiveDepsUtils.collectQueryKeyDeps({
90
141
  sourceCode: context.sourceCode,
91
- scopeManager,
142
+ scopeManager: scopeManager,
143
+ queryKeyNode: queryKeyNode,
144
+ })
145
+
146
+ const missingPaths = ExhaustiveDepsUtils.computeFilteredMissingPaths({
147
+ requiredRefs: requiredRefs,
148
+ allowlistedVariables: allowlistedVariables,
149
+ existingRootIdentifiers: queryKeyDeps.roots,
150
+ existingFullPaths: queryKeyDeps.paths,
151
+ })
152
+
153
+ if (missingPaths.length === 0) return
154
+
155
+ const missingAsText = missingPaths.join(', ')
156
+ const suggestions = buildSuggestions({
92
157
  queryKeyNode,
158
+ missingPaths,
159
+ missingAsText,
160
+ sourceCode: context.sourceCode,
93
161
  })
94
162
 
95
- const missingRefs = relevantRefs
96
- .map((ref) => ({
97
- ref: ref,
98
- text: ASTUtils.isAncestorIsCallee(ref.identifier)
99
- ? ref.identifier.name
100
- : ASTUtils.mapKeyNodeToBaseText(
101
- ref.identifier,
102
- context.sourceCode,
103
- ),
104
- }))
105
- .filter(({ ref, text }) => {
106
- return (
107
- !ref.isTypeReference &&
108
- !queryKeyDeps.has(text) &&
109
- !queryKeyDeps.has(text.split(/[?.]/)[0] ?? '')
110
- )
111
- })
112
- .map(({ ref, text }) => ({
113
- identifier: ref.identifier,
114
- text: text,
115
- }))
116
-
117
- const uniqueMissingRefs = uniqueBy(missingRefs, (x) => x.text)
118
-
119
- if (uniqueMissingRefs.length > 0) {
120
- const missingAsText = uniqueMissingRefs
121
- .map((ref) => ref.text)
122
- .join(', ')
123
-
124
- const queryKeyValue = context.sourceCode.getText(queryKeyNode)
125
-
126
- const existingWithMissing =
127
- queryKeyValue === '[]'
128
- ? `[${missingAsText}]`
129
- : queryKeyValue.replace(/\]$/, `, ${missingAsText}]`)
130
-
131
- const suggestions: TSESLint.ReportSuggestionArray<string> = []
132
-
133
- if (queryKeyNode.type === AST_NODE_TYPES.ArrayExpression) {
134
- suggestions.push({
135
- messageId: 'fixTo',
136
- data: { result: existingWithMissing },
137
- fix(fixer) {
138
- return fixer.replaceText(queryKeyNode, existingWithMissing)
139
- },
140
- })
141
- }
142
-
143
- context.report({
144
- node: node,
145
- messageId: 'missingDeps',
146
- data: {
147
- deps: uniqueMissingRefs.map((ref) => ref.text).join(', '),
148
- },
149
- suggest: suggestions,
150
- })
151
- }
163
+ context.report({
164
+ node,
165
+ messageId: 'missingDeps',
166
+ data: { deps: missingAsText },
167
+ suggest: suggestions,
168
+ })
152
169
  },
153
170
  }
154
171
  }),
155
172
  })
156
173
 
157
- function getQueryFnRelevantNode(queryFn: TSESTree.Property) {
158
- if (queryFn.value.type !== AST_NODE_TYPES.ConditionalExpression) {
159
- return queryFn.value
174
+ function buildSuggestions(params: {
175
+ queryKeyNode: TSESTree.Node
176
+ missingPaths: Array<string>
177
+ missingAsText: string
178
+ sourceCode: Readonly<TSESLint.SourceCode>
179
+ }): TSESLint.ReportSuggestionArray<string> {
180
+ const { queryKeyNode, missingPaths, missingAsText, sourceCode } = params
181
+
182
+ if (queryKeyNode.type !== AST_NODE_TYPES.ArrayExpression) {
183
+ return []
160
184
  }
161
185
 
162
- if (
163
- queryFn.value.consequent.type === AST_NODE_TYPES.Identifier &&
164
- queryFn.value.consequent.name === 'skipToken'
165
- ) {
166
- return queryFn.value.alternate
186
+ const closingBracket = sourceCode.getLastToken(queryKeyNode)
187
+ if (!closingBracket) return []
188
+
189
+ const existingElements = queryKeyNode.elements
190
+ .filter((el): el is NonNullable<typeof el> => el !== null)
191
+ .map((el) => sourceCode.getText(el))
192
+
193
+ const resultText = `[${[...existingElements, ...missingPaths].join(', ')}]`
194
+
195
+ if (queryKeyNode.elements.length === 0) {
196
+ return [
197
+ {
198
+ messageId: 'fixTo',
199
+ data: { result: resultText },
200
+ fix: (fixer) => fixer.replaceText(queryKeyNode, resultText),
201
+ },
202
+ ]
167
203
  }
168
204
 
169
- return queryFn.value.consequent
205
+ const tokenBefore = sourceCode.getTokenBefore(closingBracket)
206
+ const separator = tokenBefore?.value === ',' ? ' ' : ', '
207
+
208
+ return [
209
+ {
210
+ messageId: 'fixTo',
211
+ data: { result: resultText },
212
+ fix: (fixer) =>
213
+ fixer.insertTextBefore(closingBracket, `${separator}${missingAsText}`),
214
+ },
215
+ ]
170
216
  }
171
217
 
172
218
  function dereferenceVariablesAndTypeAssertions(
@@ -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
- isInstanceOfKind(node: TSESTree.Node) {
63
- return (
64
- node.type === AST_NODE_TYPES.BinaryExpression &&
65
- node.operator === 'instanceof'
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 deps = new Set<string>()
116
+ const roots = new Set<string>()
117
+ const paths = new Set<string>()
76
118
  const visitorKeys = sourceCode.visitorKeys
77
119
 
78
- function add(identifier: TSESTree.Identifier) {
79
- deps.add(ASTUtils.mapKeyNodeToBaseText(identifier, sourceCode))
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
- add(node)
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 === AST_NODE_TYPES.Identifier) {
121
- add(reference.identifier)
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
- deps.add(node.object.name)
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
- if (
142
- node.callee.type === AST_NODE_TYPES.MemberExpression ||
143
- node.callee.type === AST_NODE_TYPES.ChainExpression ||
144
- node.callee.type === AST_NODE_TYPES.TSNonNullExpression
145
- ) {
146
- visit(node.callee)
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 deps
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
  }
@@ -42,10 +42,9 @@ export const ASTUtils = {
42
42
  properties: Array<TSESTree.ObjectLiteralElement>,
43
43
  key: string,
44
44
  ): TSESTree.Property | undefined {
45
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
46
- return properties.find((x) =>
45
+ return properties.find((x): x is TSESTree.Property =>
47
46
  ASTUtils.isPropertyWithIdentifierKey(x, key),
48
- ) as TSESTree.Property | undefined
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>,