@blumintinc/eslint-plugin-blumint 1.12.5 → 1.13.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.
Files changed (142) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -20
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  86. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  87. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  88. package/lib/rules/no-unused-props.d.ts +3 -0
  89. package/lib/rules/no-unused-props.js +213 -78
  90. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  91. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  92. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  93. package/lib/rules/parallelize-async-operations.js +375 -0
  94. package/lib/rules/prefer-batch-operations.js +199 -33
  95. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  96. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  97. package/lib/rules/prefer-document-flattening.js +239 -0
  98. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  100. package/lib/rules/prefer-fragment-component.js +3 -3
  101. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  102. package/lib/rules/prefer-global-router-state-key.js +230 -31
  103. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  104. package/lib/rules/prefer-next-dynamic.js +366 -0
  105. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  107. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  109. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  110. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  111. package/lib/rules/prefer-settings-object.js +23 -15
  112. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  114. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  116. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  118. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  119. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  120. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  121. package/lib/rules/react-usememo-should-be-component.js +144 -78
  122. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  123. package/lib/rules/require-image-optimized.js +1 -1
  124. package/lib/rules/require-memo.js +11 -1
  125. package/lib/rules/require-usememo-object-literals.js +7 -1
  126. package/lib/rules/semantic-function-prefixes.js +38 -18
  127. package/lib/rules/sync-onwrite-name-func.js +5 -1
  128. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  129. package/lib/rules/test-file-location-enforcement.js +57 -0
  130. package/lib/rules/use-custom-link.js +6 -1
  131. package/lib/rules/use-custom-router.js +6 -1
  132. package/lib/rules/use-latest-callback.d.ts +3 -0
  133. package/lib/rules/use-latest-callback.js +270 -0
  134. package/lib/utils/ASTHelpers.js +10 -0
  135. package/lib/utils/createRule.js +1 -1
  136. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  137. package/lib/utils/ruleTester.d.ts +1 -0
  138. package/lib/utils/ruleTester.js +4 -1
  139. package/package.json +21 -14
  140. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
  141. package/lib/rules/no-restricted-imports-dynamic.d.ts +0 -25
  142. package/lib/rules/no-restricted-imports-dynamic.js +0 -213
@@ -10,48 +10,58 @@ function isHookCall(node) {
10
10
  return (callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name));
11
11
  }
12
12
  function isArrayOrPrimitive(checker, esTreeNode, nodeMap) {
13
- const tsNode = nodeMap.get(esTreeNode);
14
- if (!tsNode)
13
+ try {
14
+ const tsNode = nodeMap.get(esTreeNode);
15
+ if (!tsNode)
16
+ return false;
17
+ const type = checker.getTypeAtLocation(tsNode);
18
+ // Check if it's a primitive type
19
+ if (type.flags &
20
+ (typescript_1.TypeFlags.String |
21
+ typescript_1.TypeFlags.StringLike |
22
+ typescript_1.TypeFlags.StringLiteral |
23
+ typescript_1.TypeFlags.Number |
24
+ typescript_1.TypeFlags.Boolean |
25
+ typescript_1.TypeFlags.Null |
26
+ typescript_1.TypeFlags.Undefined |
27
+ typescript_1.TypeFlags.Void |
28
+ typescript_1.TypeFlags.Never |
29
+ typescript_1.TypeFlags.BigInt |
30
+ typescript_1.TypeFlags.ESSymbol)) {
31
+ return true;
32
+ }
33
+ // Check if it's an array type
34
+ const typeNode = checker.typeToTypeNode(type, undefined, undefined);
35
+ if (type.symbol?.name === 'Array' ||
36
+ type.symbol?.escapedName === 'Array' ||
37
+ (typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
38
+ return true;
39
+ }
40
+ // Check if it's a string type with methods (like String object)
41
+ if (type.symbol?.name === 'String' ||
42
+ type.symbol?.escapedName === 'String') {
43
+ return true;
44
+ }
45
+ // Be more conservative - if we can't determine the type clearly, assume it's an object
46
+ // This prevents false positives where complex objects are incorrectly identified as primitives
47
+ if (type.flags & (typescript_1.TypeFlags.Any | typescript_1.TypeFlags.Unknown)) {
48
+ return false; // Treat Any/Unknown as potential objects
49
+ }
50
+ // If it's not a primitive or array, and has properties, it's an object
15
51
  return false;
16
- const type = checker.getTypeAtLocation(tsNode);
17
- // Check if it's a primitive type
18
- if (type.flags &
19
- (typescript_1.TypeFlags.String |
20
- typescript_1.TypeFlags.StringLike |
21
- typescript_1.TypeFlags.StringLiteral |
22
- typescript_1.TypeFlags.Number |
23
- typescript_1.TypeFlags.Boolean |
24
- typescript_1.TypeFlags.Null |
25
- typescript_1.TypeFlags.Undefined |
26
- typescript_1.TypeFlags.Void |
27
- typescript_1.TypeFlags.Never |
28
- typescript_1.TypeFlags.Any |
29
- typescript_1.TypeFlags.Unknown |
30
- typescript_1.TypeFlags.BigInt |
31
- typescript_1.TypeFlags.ESSymbol)) {
32
- return true;
33
- }
34
- // Check if it's an array type
35
- const typeNode = checker.typeToTypeNode(type, undefined, undefined);
36
- if (type.symbol?.name === 'Array' ||
37
- type.symbol?.escapedName === 'Array' ||
38
- (typeNode && ((0, typescript_1.isArrayTypeNode)(typeNode) || (0, typescript_1.isTupleTypeNode)(typeNode)))) {
39
- return true;
40
52
  }
41
- // Check if it's a string type with methods (like String object)
42
- if (type.symbol?.name === 'String' || type.symbol?.escapedName === 'String') {
43
- return true;
53
+ catch (error) {
54
+ // If there's any error in type checking, assume it's an object to be safe
55
+ return false;
44
56
  }
45
- // If it's not a primitive or array, and has properties, it's an object
46
- return false;
47
57
  }
48
58
  function getObjectUsagesInHook(hookBody, objectName,
49
59
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
60
  context) {
51
- const usages = new Set();
61
+ const usages = new Map(); // Track usage and its position
52
62
  const visited = new Set();
53
63
  let needsEntireObject = false;
54
- // Built-in array methods that should not be considered as object properties
64
+ // Built-in array methods that indicate usage of the entire array
55
65
  const ARRAY_METHODS = new Set([
56
66
  'map',
57
67
  'filter',
@@ -76,6 +86,33 @@ context) {
76
86
  'flat',
77
87
  'flatMap',
78
88
  ]);
89
+ // Built-in string methods that indicate usage of the entire string
90
+ const STRING_METHODS = new Set([
91
+ 'charAt',
92
+ 'charCodeAt',
93
+ 'concat',
94
+ 'indexOf',
95
+ 'lastIndexOf',
96
+ 'localeCompare',
97
+ 'match',
98
+ 'replace',
99
+ 'search',
100
+ 'slice',
101
+ 'split',
102
+ 'substr',
103
+ 'substring',
104
+ 'toLowerCase',
105
+ 'toUpperCase',
106
+ 'trim',
107
+ 'trimStart',
108
+ 'trimEnd',
109
+ 'padStart',
110
+ 'padEnd',
111
+ 'repeat',
112
+ 'startsWith',
113
+ 'endsWith',
114
+ 'includes',
115
+ ]);
79
116
  function buildAccessPath(node) {
80
117
  const parts = [];
81
118
  let current = node;
@@ -85,6 +122,20 @@ context) {
85
122
  const memberExpr = current;
86
123
  // Handle computed properties (like array indices)
87
124
  if (memberExpr.computed) {
125
+ // For computed properties with variables (like user[key]), we need the entire object
126
+ if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Identifier) {
127
+ // Check if this is accessing our target object
128
+ let current = memberExpr.object;
129
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
130
+ current = current.object;
131
+ }
132
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
133
+ current.name === objectName) {
134
+ // This is a computed property access on our target object, so we need the entire object
135
+ needsEntireObject = true;
136
+ }
137
+ return null;
138
+ }
88
139
  // For computed properties with literals
89
140
  if (memberExpr.property.type === utils_1.AST_NODE_TYPES.Literal) {
90
141
  const literalProp = memberExpr.property;
@@ -118,9 +169,38 @@ context) {
118
169
  if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
119
170
  return null;
120
171
  }
121
- // Skip array methods
172
+ // Check for array/string methods - these indicate usage of the entire array/string
122
173
  if (memberExpr.property.name &&
123
- ARRAY_METHODS.has(memberExpr.property.name)) {
174
+ (ARRAY_METHODS.has(memberExpr.property.name) ||
175
+ STRING_METHODS.has(memberExpr.property.name))) {
176
+ // Check if this is accessing our target object or a property of it
177
+ let current = memberExpr.object;
178
+ let pathParts = [];
179
+ let hasOptionalChaining = false;
180
+ // Build the path to the array/string being accessed
181
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
182
+ const currentMember = current;
183
+ if (currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
184
+ pathParts.unshift(currentMember.property.name);
185
+ }
186
+ if (currentMember.optional) {
187
+ hasOptionalChaining = true;
188
+ }
189
+ current = currentMember.object;
190
+ }
191
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
192
+ current.name === objectName) {
193
+ if (pathParts.length === 0) {
194
+ // Direct method call on the object (e.g., userData.map(...))
195
+ needsEntireObject = true;
196
+ }
197
+ else {
198
+ // Method call on a property (e.g., userData.items.map(...) or userData?.items?.map(...))
199
+ let path = objectName + (hasOptionalChaining ? '?' : '');
200
+ path += '.' + pathParts.join('.');
201
+ usages.set(path, memberExpr.range?.[0] || 0);
202
+ }
203
+ }
124
204
  return null;
125
205
  }
126
206
  parts.unshift(memberExpr.property.name);
@@ -153,6 +233,11 @@ context) {
153
233
  return;
154
234
  visited.add(node);
155
235
  if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
236
+ // Check if the object is being called as a function
237
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
238
+ node.callee.name === objectName) {
239
+ needsEntireObject = true;
240
+ }
156
241
  // Check if the object is directly passed as an argument
157
242
  node.arguments.forEach((arg) => {
158
243
  if (arg.type === utils_1.AST_NODE_TYPES.Identifier && arg.name === objectName) {
@@ -183,11 +268,81 @@ context) {
183
268
  }
184
269
  else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
185
270
  // Check if this is accessing a property of our target object
186
- const path = buildAccessPath(node);
187
- if (path) {
188
- usages.add(path);
271
+ const memberExpr = node;
272
+ // Only process if this is the outermost member expression in a chain
273
+ // (i.e., its parent is not also a member expression)
274
+ const parent = memberExpr.parent;
275
+ if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
276
+ // This is an intermediate member expression, skip it
277
+ return;
278
+ }
279
+ // Check if this member expression involves our target object
280
+ let current = memberExpr;
281
+ let foundTargetObject = false;
282
+ let hasDynamicComputed = false;
283
+ // Walk up the member expression chain to see if it involves our target object
284
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
285
+ const currentMember = current;
286
+ // Check if this level uses dynamic computed property access
287
+ if (currentMember.computed &&
288
+ currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
289
+ hasDynamicComputed = true;
290
+ }
291
+ current = currentMember.object;
292
+ }
293
+ // Check if we reached our target object
294
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
295
+ current.name === objectName) {
296
+ foundTargetObject = true;
297
+ }
298
+ if (foundTargetObject) {
299
+ if (hasDynamicComputed) {
300
+ // Dynamic computed property access means we need the entire object
301
+ needsEntireObject = true;
302
+ return;
303
+ }
304
+ else {
305
+ // Static property access - add to usages
306
+ const path = buildAccessPath(memberExpr);
307
+ if (path) {
308
+ usages.set(path, memberExpr.range?.[0] || 0);
309
+ }
310
+ }
189
311
  }
190
312
  }
313
+ else if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
314
+ // Handle optional chaining expressions
315
+ if (node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
316
+ const path = buildAccessPath(node.expression);
317
+ if (path) {
318
+ usages.set(path, node.range?.[0] || 0);
319
+ }
320
+ }
321
+ }
322
+ else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
323
+ node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
324
+ // Handle binary expressions like `userId || userData?.id`
325
+ visit(node.left);
326
+ visit(node.right);
327
+ }
328
+ else if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
329
+ // Handle ternary expressions
330
+ visit(node.test);
331
+ visit(node.consequent);
332
+ visit(node.alternate);
333
+ }
334
+ else if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
335
+ // Handle variable declarations
336
+ node.declarations.forEach((declaration) => {
337
+ if (declaration.init) {
338
+ visit(declaration.init);
339
+ }
340
+ });
341
+ }
342
+ else if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
343
+ // Handle assignments
344
+ visit(node.right);
345
+ }
191
346
  // Visit all child nodes
192
347
  for (const key in node) {
193
348
  if (key === 'parent')
@@ -209,24 +364,81 @@ context) {
209
364
  }
210
365
  }
211
366
  visit(hookBody);
212
- // If the entire object is needed, return an empty set to indicate valid usage
213
- if (needsEntireObject) {
214
- return new Set();
215
- }
216
- // Filter out intermediate paths
217
- const paths = Array.from(usages);
367
+ // Process paths and determine which ones to include
368
+ const paths = Array.from(usages.keys());
369
+ const finalPaths = new Set();
370
+ paths.forEach((path) => {
371
+ // Always include the main path
372
+ finalPaths.add(path);
373
+ // For optional chaining, include the FIRST optional chaining point as intermediate
374
+ if (path.includes('?.')) {
375
+ // Find the first optional chaining point
376
+ // For userData?.profile.settings.theme.primary, we want to include userData?.profile
377
+ const firstOptionalIndex = path.indexOf('?.');
378
+ if (firstOptionalIndex !== -1) {
379
+ // Find the end of the first property after the optional chaining
380
+ const afterOptional = path.substring(firstOptionalIndex + 2);
381
+ const nextDotIndex = afterOptional.indexOf('.');
382
+ if (nextDotIndex !== -1) {
383
+ // There are more properties after the first optional property
384
+ const firstOptionalPath = path.substring(0, firstOptionalIndex + 2 + nextDotIndex);
385
+ finalPaths.add(firstOptionalPath);
386
+ }
387
+ }
388
+ }
389
+ // For array access, include the array property itself
390
+ if (path.includes('[') && path.includes(']')) {
391
+ const bracketIndex = path.indexOf('[');
392
+ if (bracketIndex > 0) {
393
+ const arrayPath = path.substring(0, bracketIndex);
394
+ finalPaths.add(arrayPath);
395
+ }
396
+ }
397
+ });
398
+ // Convert to array for sorting
399
+ const pathsArray = Array.from(finalPaths);
218
400
  // Filter out array paths when we're already accessing specific indices
219
- // For example, don't include 'obj.arr' if we have 'obj.arr[0]'
220
- const filteredPaths = paths.filter((path) => {
221
- // Skip intermediate paths (if path is prefix of another path)
222
- const isIntermediatePath = paths.some((otherPath) => otherPath !== path && otherPath.startsWith(path + '.'));
223
- // Skip array paths if we're accessing specific indices
224
- const isArrayWithSpecificIndices = paths.some((otherPath) => otherPath !== path &&
401
+ // Exception: keep array paths with optional chaining as they represent different dependencies
402
+ const filteredPaths = pathsArray.filter((path) => {
403
+ // Skip array paths if we're accessing specific indices, unless it's optional chaining
404
+ const isArrayWithSpecificIndices = pathsArray.some((otherPath) => otherPath !== path &&
225
405
  (otherPath.startsWith(path + '[') ||
226
406
  otherPath.match(new RegExp(`^${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[\\d+\\]`))));
227
- return !isIntermediatePath && !isArrayWithSpecificIndices;
407
+ // Keep array paths with optional chaining even if specific indices are accessed
408
+ if (isArrayWithSpecificIndices && path.includes('?.')) {
409
+ return true;
410
+ }
411
+ return !isArrayWithSpecificIndices;
228
412
  });
229
- return new Set(filteredPaths);
413
+ // Sort paths: longer/more specific paths first, then by optional chaining preference
414
+ const sortedPaths = filteredPaths.sort((a, b) => {
415
+ const posA = usages.get(a) || 0;
416
+ const posB = usages.get(b) || 0;
417
+ // For paths with the same base, put longer ones first
418
+ const aDepth = a.split('.').length + (a.includes('[') ? 1 : 0);
419
+ const bDepth = b.split('.').length + (b.includes('[') ? 1 : 0);
420
+ if (aDepth !== bDepth) {
421
+ return bDepth - aDepth; // Longer paths first
422
+ }
423
+ // If same depth, prefer optional chaining paths first
424
+ const aHasOptional = a.includes('?');
425
+ const bHasOptional = b.includes('?');
426
+ if (aHasOptional && !bHasOptional) {
427
+ return -1; // a comes first
428
+ }
429
+ if (!aHasOptional && bHasOptional) {
430
+ return 1; // b comes first
431
+ }
432
+ // If same depth and same optional chaining status, sort by source position
433
+ return posA - posB;
434
+ });
435
+ const filteredUsages = new Set(sortedPaths);
436
+ const notUsed = !needsEntireObject && filteredUsages.size === 0;
437
+ return {
438
+ usages: filteredUsages,
439
+ needsEntireObject,
440
+ notUsed,
441
+ };
230
442
  }
231
443
  exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
232
444
  name: 'no-entire-object-hook-deps',
@@ -241,13 +453,16 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
241
453
  schema: [],
242
454
  messages: {
243
455
  avoidEntireObject: 'Avoid using entire object "{{objectName}}" in dependency array. Use specific fields: {{fields}}',
456
+ removeUnusedDependency: 'Remove unused dependency "{{objectName}}" from dependency array (not used in effect)',
244
457
  },
245
458
  },
246
459
  defaultOptions: [],
247
460
  create(context) {
248
461
  // For testing purposes, we'll make the rule work without TypeScript services
249
462
  const parserServices = context.parserServices;
250
- const hasFullTypeChecking = parserServices?.program && parserServices?.esTreeNodeToTSNodeMap;
463
+ const hasFullTypeChecking = parserServices?.program &&
464
+ parserServices?.esTreeNodeToTSNodeMap &&
465
+ typeof parserServices.program.getTypeChecker === 'function';
251
466
  // Skip type checking if we don't have TypeScript services
252
467
  if (hasFullTypeChecking) {
253
468
  // This is just to make the rule work in tests without TypeScript services
@@ -278,7 +493,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
278
493
  if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
279
494
  const objectName = element.name;
280
495
  // Skip type checking if we don't have TypeScript services
281
- if (hasFullTypeChecking) {
496
+ if (hasFullTypeChecking && parserServices) {
282
497
  const checker = parserServices.program.getTypeChecker();
283
498
  const nodeMap = parserServices.esTreeNodeToTSNodeMap;
284
499
  // Skip if the dependency is an array or primitive type
@@ -286,11 +501,54 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
286
501
  return;
287
502
  }
288
503
  }
289
- const usages = getObjectUsagesInHook(callbackArg.body, objectName, context);
504
+ // For testing without TypeScript services, we'll assume all identifiers are objects
505
+ const result = getObjectUsagesInHook(callbackArg.body, objectName, context);
506
+ // If the object is not used at all, suggest removing it
507
+ if (result.notUsed) {
508
+ context.report({
509
+ node: element,
510
+ messageId: 'removeUnusedDependency',
511
+ data: {
512
+ objectName,
513
+ },
514
+ fix(fixer) {
515
+ // Remove the element and handle commas properly
516
+ const elementIndex = depsArg.elements.indexOf(element);
517
+ if (elementIndex === -1)
518
+ return null;
519
+ // If this is the only element, just remove it
520
+ if (depsArg.elements.length === 1) {
521
+ return fixer.remove(element);
522
+ }
523
+ // If this is the last element, remove the preceding comma
524
+ if (elementIndex === depsArg.elements.length - 1) {
525
+ const prevElement = depsArg.elements[elementIndex - 1];
526
+ if (prevElement) {
527
+ const range = [
528
+ prevElement.range[1],
529
+ element.range[1],
530
+ ];
531
+ return fixer.removeRange(range);
532
+ }
533
+ }
534
+ // Otherwise, remove the element and the following comma
535
+ const nextElement = depsArg.elements[elementIndex + 1];
536
+ if (nextElement) {
537
+ const range = [
538
+ element.range[0],
539
+ nextElement.range[0],
540
+ ];
541
+ return fixer.removeRange(range);
542
+ }
543
+ // Fallback to just removing the element
544
+ return fixer.remove(element);
545
+ },
546
+ });
547
+ }
290
548
  // If we found specific field usages and the entire object is in deps
291
- // Skip reporting if usages is empty (indicates spread operator usage)
292
- if (usages.size > 0) {
293
- const fields = Array.from(usages).join(', ');
549
+ // Skip reporting if needsEntireObject is true (indicates spread operator usage)
550
+ else if (result.usages.size > 0 && !result.needsEntireObject) {
551
+ const fields = Array.from(result.usages).join(', ');
294
552
  context.report({
295
553
  node: element,
296
554
  messageId: 'avoidEntireObject',
@@ -299,11 +557,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
299
557
  fields,
300
558
  },
301
559
  fix(fixer) {
302
- // Only provide fix if we have specific fields to suggest
303
- if (usages.size > 0) {
304
- return fixer.replaceText(element, Array.from(usages).join(', '));
305
- }
306
- return null;
560
+ return fixer.replaceText(element, Array.from(result.usages).join(', '));
307
561
  },
308
562
  });
309
563
  }
@@ -0,0 +1,5 @@
1
+ type Options = [{
2
+ max?: number;
3
+ }];
4
+ export declare const noExcessiveParentChain: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"excessiveParentChain", Options, import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
5
+ export {};