@blumintinc/eslint-plugin-blumint 1.12.6 → 1.14.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 (258) hide show
  1. package/README.md +171 -99
  2. package/lib/index.js +300 -17
  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/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +1 -1
  7. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  8. package/lib/rules/dynamic-https-errors.js +48 -11
  9. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  10. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +52 -96
  11. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  12. package/lib/rules/enforce-assert-throws.js +493 -51
  13. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  14. package/lib/rules/enforce-boolean-naming-prefixes.js +820 -39
  15. package/lib/rules/enforce-callable-types.js +4 -4
  16. package/lib/rules/enforce-callback-memo.js +143 -18
  17. package/lib/rules/enforce-centralized-mock-firestore.js +5 -2
  18. package/lib/rules/enforce-console-error.d.ts +3 -0
  19. package/lib/rules/enforce-console-error.js +278 -0
  20. package/lib/rules/enforce-css-media-queries.js +24 -46
  21. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  22. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  23. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  24. package/lib/rules/enforce-dynamic-imports.js +1 -1
  25. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  26. package/lib/rules/enforce-early-destructuring.js +980 -0
  27. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  28. package/lib/rules/enforce-empty-object-check.js +502 -0
  29. package/lib/rules/enforce-exported-function-types.js +4 -4
  30. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  31. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +262 -0
  32. package/lib/rules/enforce-firestore-doc-ref-generic.js +350 -3
  33. package/lib/rules/enforce-firestore-facade.js +349 -157
  34. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  35. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  36. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  37. package/lib/rules/enforce-global-constants.d.ts +4 -1
  38. package/lib/rules/enforce-global-constants.js +240 -12
  39. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  40. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  41. package/lib/rules/enforce-memoize-async.js +92 -27
  42. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  43. package/lib/rules/enforce-memoize-getters.js +147 -0
  44. package/lib/rules/enforce-microdiff.js +3 -2
  45. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  46. package/lib/rules/enforce-positive-naming.js +9 -7
  47. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  48. package/lib/rules/enforce-props-argument-name.js +165 -151
  49. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  50. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  51. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  52. package/lib/rules/enforce-querykey-ts.js +274 -0
  53. package/lib/rules/enforce-safe-stringify.js +1 -1
  54. package/lib/rules/enforce-serializable-params.js +3 -3
  55. package/lib/rules/enforce-singular-type-names.js +10 -6
  56. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  57. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  58. package/lib/rules/enforce-storage-context.d.ts +9 -0
  59. package/lib/rules/enforce-storage-context.js +646 -0
  60. package/lib/rules/enforce-timestamp-now.js +23 -7
  61. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  62. package/lib/rules/enforce-transform-memoization.js +416 -0
  63. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  64. package/lib/rules/enforce-typescript-markdown-code-blocks.js +100 -0
  65. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  66. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  67. package/lib/rules/enforce-verb-noun-naming.js +90 -14
  68. package/lib/rules/ensure-pointer-events-none.js +28 -2
  69. package/lib/rules/export-if-in-doubt.js +27 -4
  70. package/lib/rules/extract-global-constants.js +92 -53
  71. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  72. package/lib/rules/fast-deep-equal-over-microdiff.js +351 -83
  73. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  74. package/lib/rules/firestore-transaction-reads-before-writes.js +277 -0
  75. package/lib/rules/flatten-push-calls.d.ts +2 -0
  76. package/lib/rules/flatten-push-calls.js +428 -0
  77. package/lib/rules/generic-starts-with-t.js +8 -2
  78. package/lib/rules/global-const-style.js +50 -19
  79. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  80. package/lib/rules/jsdoc-above-field.js +208 -0
  81. package/lib/rules/key-only-outermost-element.js +21 -3
  82. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  83. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  84. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  85. package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
  86. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  87. package/lib/rules/memo-nested-react-components.js +410 -0
  88. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  89. package/lib/rules/memoize-root-level-hocs.js +242 -0
  90. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  91. package/lib/rules/no-always-true-false-conditions.js +68 -8
  92. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  93. package/lib/rules/no-array-length-in-deps.js +314 -0
  94. package/lib/rules/no-async-array-filter.js +5 -2
  95. package/lib/rules/no-async-foreach.js +210 -12
  96. package/lib/rules/no-circular-references.js +18 -17
  97. package/lib/rules/no-class-instance-destructuring.js +55 -12
  98. package/lib/rules/no-complex-cloud-params.js +22 -5
  99. package/lib/rules/no-compositing-layer-props.js +6 -4
  100. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  101. package/lib/rules/no-console-error.d.ts +9 -0
  102. package/lib/rules/no-console-error.js +525 -0
  103. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  104. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  105. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  106. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  107. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  108. package/lib/rules/no-entire-object-hook-deps.js +320 -68
  109. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  110. package/lib/rules/no-excessive-parent-chain.js +273 -0
  111. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  112. package/lib/rules/no-explicit-return-type.js +215 -26
  113. package/lib/rules/no-filter-without-return.js +5 -1
  114. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  115. package/lib/rules/no-firestore-jest-mock.js +126 -8
  116. package/lib/rules/no-firestore-object-arrays.js +398 -28
  117. package/lib/rules/no-handler-suffix.d.ts +12 -0
  118. package/lib/rules/no-handler-suffix.js +305 -0
  119. package/lib/rules/no-hungarian.js +14 -14
  120. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  121. package/lib/rules/no-inline-component-prop.js +456 -0
  122. package/lib/rules/no-jsx-in-hooks.js +6 -1
  123. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  124. package/lib/rules/no-margin-properties.d.ts +7 -1
  125. package/lib/rules/no-margin-properties.js +22 -9
  126. package/lib/rules/no-memoize-on-static.js +9 -1
  127. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  128. package/lib/rules/no-misleading-boolean-prefixes.js +331 -0
  129. package/lib/rules/no-misused-switch-case.js +22 -1
  130. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  131. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  132. package/lib/rules/no-mock-firebase-admin.js +5 -2
  133. package/lib/rules/no-object-values-on-strings.js +10 -2
  134. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  135. package/lib/rules/no-overridable-method-calls-in-constructor.js +272 -0
  136. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  137. package/lib/rules/no-passthrough-getters.js +206 -0
  138. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  139. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  140. package/lib/rules/no-redundant-param-types.js +16 -6
  141. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  142. package/lib/rules/no-redundant-this-params.js +459 -0
  143. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  144. package/lib/rules/no-redundant-usecallback-wrapper.js +328 -0
  145. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  146. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  147. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  148. package/lib/rules/no-restricted-properties-fix.js +173 -0
  149. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  150. package/lib/rules/no-separate-loading-state.js +133 -0
  151. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  152. package/lib/rules/no-stale-state-across-await.js +185 -0
  153. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  154. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  155. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  156. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  157. package/lib/rules/no-type-assertion-returns.js +49 -49
  158. package/lib/rules/no-undefined-null-passthrough.js +217 -40
  159. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  160. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  161. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  162. package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
  163. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  164. package/lib/rules/no-unnecessary-verb-suffix.js +2 -3
  165. package/lib/rules/no-unpinned-dependencies.js +36 -5
  166. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  167. package/lib/rules/no-unused-props.d.ts +5 -2
  168. package/lib/rules/no-unused-props.js +451 -112
  169. package/lib/rules/no-unused-usestate.js +6 -2
  170. package/lib/rules/no-useless-fragment.js +28 -2
  171. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  172. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  173. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  174. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  175. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  176. package/lib/rules/omit-index-html.d.ts +2 -1
  177. package/lib/rules/omit-index-html.js +62 -7
  178. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  179. package/lib/rules/optimize-object-boolean-conditions.js +236 -0
  180. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  181. package/lib/rules/parallelize-async-operations.js +394 -0
  182. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  183. package/lib/rules/prefer-batch-operations.js +228 -35
  184. package/lib/rules/prefer-block-comments-for-declarations.js +22 -7
  185. package/lib/rules/prefer-clone-deep.js +3 -3
  186. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  187. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  188. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  189. package/lib/rules/prefer-docsetter-setall.js +243 -0
  190. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  191. package/lib/rules/prefer-document-flattening.js +239 -0
  192. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  193. package/lib/rules/prefer-field-paths-in-transforms.js +251 -0
  194. package/lib/rules/prefer-fragment-component.js +4 -4
  195. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  196. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  197. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  198. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  199. package/lib/rules/prefer-global-router-state-key.js +305 -31
  200. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  201. package/lib/rules/prefer-memoized-props.js +445 -0
  202. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  203. package/lib/rules/prefer-next-dynamic.js +357 -0
  204. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  205. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +435 -0
  206. package/lib/rules/prefer-nullish-coalescing-override.d.ts +7 -0
  207. package/lib/rules/prefer-nullish-coalescing-override.js +188 -0
  208. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  209. package/lib/rules/prefer-params-over-parent-id.js +277 -0
  210. package/lib/rules/prefer-settings-object.js +23 -15
  211. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  212. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  213. package/lib/rules/prefer-type-over-interface.js +7 -2
  214. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  215. package/lib/rules/prefer-url-tostring-over-tojson.js +169 -0
  216. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  217. package/lib/rules/prefer-use-deep-compare-memo.js +431 -0
  218. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +25 -2
  219. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  220. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  221. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  222. package/lib/rules/prevent-children-clobber.js +536 -0
  223. package/lib/rules/react-memoize-literals.d.ts +4 -0
  224. package/lib/rules/react-memoize-literals.js +495 -0
  225. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  226. package/lib/rules/react-usememo-should-be-component.js +144 -78
  227. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  228. package/lib/rules/require-hooks-default-params.js +17 -16
  229. package/lib/rules/require-https-error-cause.d.ts +4 -0
  230. package/lib/rules/require-https-error-cause.js +136 -0
  231. package/lib/rules/require-https-error.js +43 -21
  232. package/lib/rules/require-image-optimized.js +2 -2
  233. package/lib/rules/require-memo.js +12 -2
  234. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  235. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  236. package/lib/rules/require-usememo-object-literals.js +9 -4
  237. package/lib/rules/semantic-function-prefixes.js +38 -18
  238. package/lib/rules/sync-onwrite-name-func.js +5 -1
  239. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  240. package/lib/rules/test-file-location-enforcement.js +57 -0
  241. package/lib/rules/use-custom-link.js +6 -1
  242. package/lib/rules/use-custom-router.js +6 -1
  243. package/lib/rules/use-latest-callback.d.ts +3 -0
  244. package/lib/rules/use-latest-callback.js +271 -0
  245. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  246. package/lib/rules/vertically-group-related-functions.js +480 -0
  247. package/lib/utils/ASTHelpers.d.ts +13 -1
  248. package/lib/utils/ASTHelpers.js +34 -0
  249. package/lib/utils/createRule.js +1 -1
  250. package/lib/utils/getMethodName.d.ts +27 -0
  251. package/lib/utils/getMethodName.js +35 -0
  252. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  253. package/lib/utils/ruleTester.d.ts +1 -0
  254. package/lib/utils/ruleTester.js +4 -1
  255. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  256. package/lib/utils/tsTypeClassifier.js +149 -0
  257. package/package.json +23 -14
  258. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -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;
@@ -102,9 +153,7 @@ context) {
102
153
  else {
103
154
  // For other computed properties, use the exact expression
104
155
  try {
105
- const propertyText = context
106
- .getSourceCode()
107
- .getText(memberExpr.property);
156
+ const propertyText = context.sourceCode.getText(memberExpr.property);
108
157
  parts.unshift(`[${propertyText}]`);
109
158
  }
110
159
  catch (e) {
@@ -118,9 +167,38 @@ context) {
118
167
  if (memberExpr.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
119
168
  return null;
120
169
  }
121
- // Skip array methods
170
+ // Check for array/string methods - these indicate usage of the entire array/string
122
171
  if (memberExpr.property.name &&
123
- ARRAY_METHODS.has(memberExpr.property.name)) {
172
+ (ARRAY_METHODS.has(memberExpr.property.name) ||
173
+ STRING_METHODS.has(memberExpr.property.name))) {
174
+ // Check if this is accessing our target object or a property of it
175
+ let current = memberExpr.object;
176
+ let pathParts = [];
177
+ let hasOptionalChaining = false;
178
+ // Build the path to the array/string being accessed
179
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
180
+ const currentMember = current;
181
+ if (currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
182
+ pathParts.unshift(currentMember.property.name);
183
+ }
184
+ if (currentMember.optional) {
185
+ hasOptionalChaining = true;
186
+ }
187
+ current = currentMember.object;
188
+ }
189
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
190
+ current.name === objectName) {
191
+ if (pathParts.length === 0) {
192
+ // Direct method call on the object (e.g., userData.map(...))
193
+ needsEntireObject = true;
194
+ }
195
+ else {
196
+ // Method call on a property (e.g., userData.items.map(...) or userData?.items?.map(...))
197
+ let path = objectName + (hasOptionalChaining ? '?' : '');
198
+ path += '.' + pathParts.join('.');
199
+ usages.set(path, memberExpr.range?.[0] || 0);
200
+ }
201
+ }
124
202
  return null;
125
203
  }
126
204
  parts.unshift(memberExpr.property.name);
@@ -153,6 +231,11 @@ context) {
153
231
  return;
154
232
  visited.add(node);
155
233
  if (node.type === utils_1.AST_NODE_TYPES.CallExpression) {
234
+ // Check if the object is being called as a function
235
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
236
+ node.callee.name === objectName) {
237
+ needsEntireObject = true;
238
+ }
156
239
  // Check if the object is directly passed as an argument
157
240
  node.arguments.forEach((arg) => {
158
241
  if (arg.type === utils_1.AST_NODE_TYPES.Identifier && arg.name === objectName) {
@@ -183,11 +266,81 @@ context) {
183
266
  }
184
267
  else if (node.type === utils_1.AST_NODE_TYPES.MemberExpression) {
185
268
  // Check if this is accessing a property of our target object
186
- const path = buildAccessPath(node);
187
- if (path) {
188
- usages.add(path);
269
+ const memberExpr = node;
270
+ // Only process if this is the outermost member expression in a chain
271
+ // (i.e., its parent is not also a member expression)
272
+ const parent = memberExpr.parent;
273
+ if (parent && parent.type === utils_1.AST_NODE_TYPES.MemberExpression) {
274
+ // This is an intermediate member expression, skip it
275
+ return;
276
+ }
277
+ // Check if this member expression involves our target object
278
+ let current = memberExpr;
279
+ let foundTargetObject = false;
280
+ let hasDynamicComputed = false;
281
+ // Walk up the member expression chain to see if it involves our target object
282
+ while (current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
283
+ const currentMember = current;
284
+ // Check if this level uses dynamic computed property access
285
+ if (currentMember.computed &&
286
+ currentMember.property.type === utils_1.AST_NODE_TYPES.Identifier) {
287
+ hasDynamicComputed = true;
288
+ }
289
+ current = currentMember.object;
290
+ }
291
+ // Check if we reached our target object
292
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier &&
293
+ current.name === objectName) {
294
+ foundTargetObject = true;
295
+ }
296
+ if (foundTargetObject) {
297
+ if (hasDynamicComputed) {
298
+ // Dynamic computed property access means we need the entire object
299
+ needsEntireObject = true;
300
+ return;
301
+ }
302
+ else {
303
+ // Static property access - add to usages
304
+ const path = buildAccessPath(memberExpr);
305
+ if (path) {
306
+ usages.set(path, memberExpr.range?.[0] || 0);
307
+ }
308
+ }
189
309
  }
190
310
  }
311
+ else if (node.type === utils_1.AST_NODE_TYPES.ChainExpression) {
312
+ // Handle optional chaining expressions
313
+ if (node.expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
314
+ const path = buildAccessPath(node.expression);
315
+ if (path) {
316
+ usages.set(path, node.range?.[0] || 0);
317
+ }
318
+ }
319
+ }
320
+ else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression ||
321
+ node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
322
+ // Handle binary expressions like `userId || userData?.id`
323
+ visit(node.left);
324
+ visit(node.right);
325
+ }
326
+ else if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression) {
327
+ // Handle ternary expressions
328
+ visit(node.test);
329
+ visit(node.consequent);
330
+ visit(node.alternate);
331
+ }
332
+ else if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
333
+ // Handle variable declarations
334
+ node.declarations.forEach((declaration) => {
335
+ if (declaration.init) {
336
+ visit(declaration.init);
337
+ }
338
+ });
339
+ }
340
+ else if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression) {
341
+ // Handle assignments
342
+ visit(node.right);
343
+ }
191
344
  // Visit all child nodes
192
345
  for (const key in node) {
193
346
  if (key === 'parent')
@@ -209,45 +362,105 @@ context) {
209
362
  }
210
363
  }
211
364
  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);
365
+ // Process paths and determine which ones to include
366
+ const paths = Array.from(usages.keys());
367
+ const finalPaths = new Set();
368
+ paths.forEach((path) => {
369
+ // Always include the main path
370
+ finalPaths.add(path);
371
+ // For optional chaining, include the FIRST optional chaining point as intermediate
372
+ if (path.includes('?.')) {
373
+ // Find the first optional chaining point
374
+ // For userData?.profile.settings.theme.primary, we want to include userData?.profile
375
+ const firstOptionalIndex = path.indexOf('?.');
376
+ if (firstOptionalIndex !== -1) {
377
+ // Find the end of the first property after the optional chaining
378
+ const afterOptional = path.substring(firstOptionalIndex + 2);
379
+ const nextDotIndex = afterOptional.indexOf('.');
380
+ if (nextDotIndex !== -1) {
381
+ // There are more properties after the first optional property
382
+ const firstOptionalPath = path.substring(0, firstOptionalIndex + 2 + nextDotIndex);
383
+ finalPaths.add(firstOptionalPath);
384
+ }
385
+ }
386
+ }
387
+ // For array access, include the array property itself
388
+ if (path.includes('[') && path.includes(']')) {
389
+ const bracketIndex = path.indexOf('[');
390
+ if (bracketIndex > 0) {
391
+ const arrayPath = path.substring(0, bracketIndex);
392
+ finalPaths.add(arrayPath);
393
+ }
394
+ }
395
+ });
396
+ // Convert to array for sorting
397
+ const pathsArray = Array.from(finalPaths);
218
398
  // 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 &&
399
+ // Exception: keep array paths with optional chaining as they represent different dependencies
400
+ const filteredPaths = pathsArray.filter((path) => {
401
+ // Skip array paths if we're accessing specific indices, unless it's optional chaining
402
+ const isArrayWithSpecificIndices = pathsArray.some((otherPath) => otherPath !== path &&
225
403
  (otherPath.startsWith(path + '[') ||
226
404
  otherPath.match(new RegExp(`^${path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[\\d+\\]`))));
227
- return !isIntermediatePath && !isArrayWithSpecificIndices;
405
+ // Keep array paths with optional chaining even if specific indices are accessed
406
+ if (isArrayWithSpecificIndices && path.includes('?.')) {
407
+ return true;
408
+ }
409
+ return !isArrayWithSpecificIndices;
228
410
  });
229
- return new Set(filteredPaths);
411
+ // Sort paths: longer/more specific paths first, then by optional chaining preference
412
+ const sortedPaths = filteredPaths.sort((a, b) => {
413
+ const posA = usages.get(a) || 0;
414
+ const posB = usages.get(b) || 0;
415
+ // For paths with the same base, put longer ones first
416
+ const aDepth = a.split('.').length + (a.includes('[') ? 1 : 0);
417
+ const bDepth = b.split('.').length + (b.includes('[') ? 1 : 0);
418
+ if (aDepth !== bDepth) {
419
+ return bDepth - aDepth; // Longer paths first
420
+ }
421
+ // If same depth, prefer optional chaining paths first
422
+ const aHasOptional = a.includes('?');
423
+ const bHasOptional = b.includes('?');
424
+ if (aHasOptional && !bHasOptional) {
425
+ return -1; // a comes first
426
+ }
427
+ if (!aHasOptional && bHasOptional) {
428
+ return 1; // b comes first
429
+ }
430
+ // If same depth and same optional chaining status, sort by source position
431
+ return posA - posB;
432
+ });
433
+ const filteredUsages = new Set(sortedPaths);
434
+ const notUsed = !needsEntireObject && filteredUsages.size === 0;
435
+ return {
436
+ usages: filteredUsages,
437
+ needsEntireObject,
438
+ notUsed,
439
+ };
230
440
  }
231
441
  exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
232
442
  name: 'no-entire-object-hook-deps',
233
443
  meta: {
234
444
  type: 'suggestion',
235
445
  docs: {
236
- description: 'Avoid using entire objects in React hook dependency arrays when only specific fields are used, as this can cause unnecessary re-renders. When a hook only uses obj.name but obj is in the deps array, any change to obj.age will trigger the hook. Use individual fields (obj.name) instead of the entire object. Requires TypeScript and `parserOptions.project` to be configured.',
446
+ description: 'Avoid using entire objects in React hook dependency arrays.',
237
447
  recommended: 'error',
238
448
  requiresTypeChecking: true,
239
449
  },
240
450
  fixable: 'code',
241
451
  schema: [],
242
452
  messages: {
243
- avoidEntireObject: 'Avoid using entire object "{{objectName}}" in dependency array. Use specific fields: {{fields}}',
453
+ avoidEntireObject: 'What\'s wrong: Dependency array includes entire object "{{objectName}}". Why it matters: Any change to its other properties reruns the hook even though the hook reads only {{fields}}, creating extra renders and stale memoized values. How to fix: Depend on those fields instead.',
454
+ removeUnusedDependency: 'What\'s wrong: Dependency "{{objectName}}" is listed in the array but never read inside the hook body. Why it matters: The hook reruns when "{{objectName}}" changes without affecting the result and can hide the real missing dependency. How to fix: Remove it or add the specific value that actually drives the hook.',
244
455
  },
245
456
  },
246
457
  defaultOptions: [],
247
458
  create(context) {
248
459
  // For testing purposes, we'll make the rule work without TypeScript services
249
460
  const parserServices = context.parserServices;
250
- const hasFullTypeChecking = parserServices?.program && parserServices?.esTreeNodeToTSNodeMap;
461
+ const hasFullTypeChecking = parserServices?.program &&
462
+ parserServices?.esTreeNodeToTSNodeMap &&
463
+ typeof parserServices.program.getTypeChecker === 'function';
251
464
  // Skip type checking if we don't have TypeScript services
252
465
  if (hasFullTypeChecking) {
253
466
  // This is just to make the rule work in tests without TypeScript services
@@ -278,7 +491,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
278
491
  if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
279
492
  const objectName = element.name;
280
493
  // Skip type checking if we don't have TypeScript services
281
- if (hasFullTypeChecking) {
494
+ if (hasFullTypeChecking && parserServices) {
282
495
  const checker = parserServices.program.getTypeChecker();
283
496
  const nodeMap = parserServices.esTreeNodeToTSNodeMap;
284
497
  // Skip if the dependency is an array or primitive type
@@ -286,11 +499,54 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
286
499
  return;
287
500
  }
288
501
  }
289
- const usages = getObjectUsagesInHook(callbackArg.body, objectName, context);
502
+ // For testing without TypeScript services, we'll assume all identifiers are objects
503
+ const result = getObjectUsagesInHook(callbackArg.body, objectName, context);
504
+ // If the object is not used at all, suggest removing it
505
+ if (result.notUsed) {
506
+ context.report({
507
+ node: element,
508
+ messageId: 'removeUnusedDependency',
509
+ data: {
510
+ objectName,
511
+ },
512
+ fix(fixer) {
513
+ // Remove the element and handle commas properly
514
+ const elementIndex = depsArg.elements.indexOf(element);
515
+ if (elementIndex === -1)
516
+ return null;
517
+ // If this is the only element, just remove it
518
+ if (depsArg.elements.length === 1) {
519
+ return fixer.remove(element);
520
+ }
521
+ // If this is the last element, remove the preceding comma
522
+ if (elementIndex === depsArg.elements.length - 1) {
523
+ const prevElement = depsArg.elements[elementIndex - 1];
524
+ if (prevElement) {
525
+ const range = [
526
+ prevElement.range[1],
527
+ element.range[1],
528
+ ];
529
+ return fixer.removeRange(range);
530
+ }
531
+ }
532
+ // Otherwise, remove the element and the following comma
533
+ const nextElement = depsArg.elements[elementIndex + 1];
534
+ if (nextElement) {
535
+ const range = [
536
+ element.range[0],
537
+ nextElement.range[0],
538
+ ];
539
+ return fixer.removeRange(range);
540
+ }
541
+ // Fallback to just removing the element
542
+ return fixer.remove(element);
543
+ },
544
+ });
545
+ }
290
546
  // 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(', ');
547
+ // Skip reporting if needsEntireObject is true (indicates spread operator usage)
548
+ else if (result.usages.size > 0 && !result.needsEntireObject) {
549
+ const fields = Array.from(result.usages).join(', ');
294
550
  context.report({
295
551
  node: element,
296
552
  messageId: 'avoidEntireObject',
@@ -299,11 +555,7 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
299
555
  fields,
300
556
  },
301
557
  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;
558
+ return fixer.replaceText(element, Array.from(result.usages).join(', '));
307
559
  },
308
560
  });
309
561
  }
@@ -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 {};