@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.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 (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. package/package.json +3 -1
@@ -0,0 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type MessageIds = 'componentLiteral' | 'nestedHookLiteral' | 'hookReturnLiteral' | 'memoizeLiteralSuggestion';
3
+ export declare const reactMemoizeLiterals: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
4
+ export {};
@@ -0,0 +1,614 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reactMemoizeLiterals = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
7
+ const LITERAL_DESCRIPTOR_BY_TYPE = {
8
+ [utils_1.AST_NODE_TYPES.ObjectExpression]: {
9
+ literalType: 'object literal',
10
+ memoHook: 'useMemo',
11
+ },
12
+ [utils_1.AST_NODE_TYPES.ArrayExpression]: {
13
+ literalType: 'array literal',
14
+ memoHook: 'useMemo',
15
+ },
16
+ [utils_1.AST_NODE_TYPES.ArrowFunctionExpression]: {
17
+ literalType: 'inline function',
18
+ memoHook: 'useCallback',
19
+ },
20
+ [utils_1.AST_NODE_TYPES.FunctionExpression]: {
21
+ literalType: 'inline function',
22
+ memoHook: 'useCallback',
23
+ },
24
+ [utils_1.AST_NODE_TYPES.FunctionDeclaration]: {
25
+ literalType: 'inline function',
26
+ memoHook: 'useCallback',
27
+ },
28
+ };
29
+ const SAFE_HOOK_ARGUMENTS = new Set([
30
+ 'useMemo',
31
+ 'useCallback',
32
+ 'useEffect',
33
+ 'useLayoutEffect',
34
+ 'useInsertionEffect',
35
+ 'useImperativeHandle',
36
+ 'useState',
37
+ 'useReducer',
38
+ 'useRef',
39
+ 'useSyncExternalStore',
40
+ 'useDeferredValue',
41
+ 'useTransition',
42
+ 'useId',
43
+ 'useLatestCallback',
44
+ 'useDeepCompareMemo',
45
+ 'useDeepCompareCallback',
46
+ 'useDeepCompareEffect',
47
+ 'useProgressionCallback',
48
+ ]);
49
+ const MEMOIZATION_DEPS_TODO_PLACEHOLDER = '__TODO_MEMOIZATION_DEPENDENCIES__';
50
+ const TODO_DEPS_COMMENT = `/* ${MEMOIZATION_DEPS_TODO_PLACEHOLDER} */`;
51
+ const PARENTHESIZED_EXPRESSION_TYPE = utils_1.AST_NODE_TYPES.ParenthesizedExpression ??
52
+ 'ParenthesizedExpression';
53
+ /**
54
+ * Detects React hook-style identifiers prefixed with "use".
55
+ * @param name Candidate identifier name.
56
+ * @returns True when the name follows the hook naming convention.
57
+ */
58
+ function isHookName(name) {
59
+ return !!name && /^use[A-Z]/.test(name);
60
+ }
61
+ /**
62
+ * Detects PascalCase identifiers commonly used for React components.
63
+ * @param name Candidate identifier name.
64
+ * @returns True when the name begins with an uppercase character.
65
+ */
66
+ function isComponentName(name) {
67
+ return !!name && /^[A-Z]/.test(name);
68
+ }
69
+ /**
70
+ * Type guard for parenthesized expressions to unwrap safely.
71
+ * @param node Node to evaluate.
72
+ * @returns True when the node is a parenthesized expression wrapper.
73
+ */
74
+ function isParenthesizedExpression(node) {
75
+ return node.type === PARENTHESIZED_EXPRESSION_TYPE;
76
+ }
77
+ /**
78
+ * Extracts an identifier name from variable, assignment, or property nodes.
79
+ * @param node AST node that may carry an identifier.
80
+ * @returns Identifier name when present, otherwise null.
81
+ */
82
+ function getNameFromNode(node) {
83
+ if (!node)
84
+ return null;
85
+ if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
86
+ node.id.type === utils_1.AST_NODE_TYPES.Identifier) {
87
+ return node.id.name;
88
+ }
89
+ if (node.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
90
+ node.left.type === utils_1.AST_NODE_TYPES.Identifier) {
91
+ return node.left.name;
92
+ }
93
+ if (node.type === utils_1.AST_NODE_TYPES.Property &&
94
+ node.key.type === utils_1.AST_NODE_TYPES.Identifier) {
95
+ return node.key.name;
96
+ }
97
+ return null;
98
+ }
99
+ /**
100
+ * Determines whether a node should be traversed through when resolving names.
101
+ * @param node Node to evaluate for transparency.
102
+ * @returns True when the node does not introduce a binding boundary.
103
+ */
104
+ function isTransparentNode(node) {
105
+ return (node.type === utils_1.AST_NODE_TYPES.CallExpression ||
106
+ node.type === utils_1.AST_NODE_TYPES.MemberExpression ||
107
+ isExpressionWrapper(node));
108
+ }
109
+ /**
110
+ * Walks ancestors through transparent nodes to find the nearest identifier name.
111
+ * @param startNode Node where the search begins.
112
+ * @returns Resolved identifier name or null if none is located.
113
+ */
114
+ function findNameInAncestors(startNode) {
115
+ let current = startNode;
116
+ while (current) {
117
+ const name = getNameFromNode(current);
118
+ if (name) {
119
+ return name;
120
+ }
121
+ if (!isTransparentNode(current)) {
122
+ break;
123
+ }
124
+ current = current.parent;
125
+ }
126
+ return null;
127
+ }
128
+ /**
129
+ * Resolves a function's display name from its declaration or surrounding
130
+ * assignment/property wrappers so HOC-wrapped and asserted components still map
131
+ * to their intended identifiers.
132
+ */
133
+ function getFunctionName(fn) {
134
+ if (fn.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
135
+ return fn.id?.name ?? null;
136
+ }
137
+ if (fn.type === utils_1.AST_NODE_TYPES.FunctionExpression && fn.id?.name) {
138
+ return fn.id.name;
139
+ }
140
+ const parent = fn.parent;
141
+ if (!parent) {
142
+ return null;
143
+ }
144
+ const immediateName = getNameFromNode(parent);
145
+ if (immediateName) {
146
+ return immediateName;
147
+ }
148
+ return findNameInAncestors(parent);
149
+ }
150
+ /**
151
+ * Checks whether a function should be treated as a React component or hook
152
+ * based on naming conventions. Enables the rule to limit reports to user-facing
153
+ * components and hooks rather than arbitrary functions.
154
+ */
155
+ function isComponentOrHookFunction(fn) {
156
+ const name = getFunctionName(fn);
157
+ return isComponentName(name) || isHookName(name);
158
+ }
159
+ /**
160
+ * Checks whether a function node represents a hook by name.
161
+ * @param fn Function node to inspect.
162
+ * @returns True when the function follows hook naming.
163
+ */
164
+ function isHookFunction(fn) {
165
+ return isHookName(getFunctionName(fn));
166
+ }
167
+ /**
168
+ * Narrows nodes to the supported function-like shapes.
169
+ * @param node Node to test.
170
+ * @returns True when the node is any function declaration or expression.
171
+ */
172
+ function isFunctionNode(node) {
173
+ return (node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
174
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
175
+ node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression);
176
+ }
177
+ /**
178
+ * Extracts a hook name from simple identifier or member-expression callees.
179
+ * @param callee Callee expression from a call.
180
+ * @returns Hook name when available, otherwise null.
181
+ */
182
+ function getHookNameFromCallee(callee) {
183
+ const resolvedCallee = unwrapNestedExpressions(callee);
184
+ if (resolvedCallee.type === utils_1.AST_NODE_TYPES.Identifier) {
185
+ return resolvedCallee.name;
186
+ }
187
+ if (resolvedCallee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
188
+ !resolvedCallee.computed &&
189
+ resolvedCallee.property.type ===
190
+ utils_1.AST_NODE_TYPES.Identifier) {
191
+ const property = resolvedCallee.property;
192
+ return property.name;
193
+ }
194
+ return null;
195
+ }
196
+ /**
197
+ * Detects hook call expressions and returns their hook name.
198
+ * @param node Call expression to inspect.
199
+ * @returns Hook name when the call targets a hook, otherwise null.
200
+ */
201
+ function isHookCall(node) {
202
+ const hookName = getHookNameFromCallee(node.callee);
203
+ return hookName && isHookName(hookName) ? hookName : null;
204
+ }
205
+ /**
206
+ * Maps literal node types to memoization metadata used for reporting.
207
+ * @param node Candidate literal node.
208
+ * @returns Descriptor with literal type and memo hook, or null.
209
+ */
210
+ function getLiteralDescriptor(node) {
211
+ const descriptor = LITERAL_DESCRIPTOR_BY_TYPE[node.type] ?? null;
212
+ return descriptor;
213
+ }
214
+ /**
215
+ * Finds the nearest ancestor function considered a React component or hook.
216
+ * @param node Starting node for the search.
217
+ * @returns Owning component/hook function or null.
218
+ */
219
+ function findEnclosingComponentOrHook(node) {
220
+ let current = node.parent;
221
+ while (current) {
222
+ if (isFunctionNode(current) && isComponentOrHookFunction(current)) {
223
+ return current;
224
+ }
225
+ current = current.parent;
226
+ }
227
+ return null;
228
+ }
229
+ /**
230
+ * Checks whether a node is inside an allowed hook callback that should be skipped.
231
+ * @param node Starting node for traversal.
232
+ * @returns True when enclosed by a callback to an allowed hook.
233
+ */
234
+ function isInsideAllowedHookCallback(node) {
235
+ let current = node;
236
+ while (current) {
237
+ if (isFunctionNode(current)) {
238
+ if (current.async && current !== node) {
239
+ return true;
240
+ }
241
+ if (current.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
242
+ current.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
243
+ let parent = current.parent;
244
+ // Skip through TypeScript type assertions and parentheses to find
245
+ // the actual CallExpression that invokes the hook.
246
+ while (parent && isExpressionWrapper(parent)) {
247
+ parent = parent.parent;
248
+ }
249
+ if (parent?.type === utils_1.AST_NODE_TYPES.CallExpression) {
250
+ const hookName = getHookNameFromCallee(parent.callee);
251
+ const matchesCallback = parent.arguments.some((arg) => unwrapNestedExpressions(arg) === current);
252
+ if (hookName &&
253
+ SAFE_HOOK_ARGUMENTS.has(hookName) &&
254
+ matchesCallback) {
255
+ return true;
256
+ }
257
+ }
258
+ }
259
+ }
260
+ current = current.parent;
261
+ }
262
+ return false;
263
+ }
264
+ /**
265
+ * Type guard for nodes that wrap an underlying expression (e.g. assertions,
266
+ * parentheses, optional chaining wrappers).
267
+ */
268
+ function isExpressionWrapper(node) {
269
+ return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
270
+ node.type === utils_1.AST_NODE_TYPES.TSTypeAssertion ||
271
+ node.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
272
+ node.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
273
+ node.type === utils_1.AST_NODE_TYPES.ChainExpression ||
274
+ isParenthesizedExpression(node));
275
+ }
276
+ /**
277
+ * Unwraps TypeScript assertions, optional chaining, and parentheses to reach
278
+ * the underlying expression node for stable identity comparisons.
279
+ */
280
+ function unwrapNestedExpressions(node) {
281
+ let current = node;
282
+ while (isExpressionWrapper(current)) {
283
+ current = current.expression;
284
+ }
285
+ return current;
286
+ }
287
+ /**
288
+ * Finds the nearest enclosing hook call for a literal and whether the literal
289
+ * is passed directly as an argument (versus nested inside another expression).
290
+ */
291
+ function findEnclosingHookCall(node) {
292
+ const unwrappedTarget = unwrapNestedExpressions(node);
293
+ let current = node.parent;
294
+ while (current) {
295
+ if (current.type === utils_1.AST_NODE_TYPES.CallExpression) {
296
+ const hookName = isHookCall(current);
297
+ if (hookName) {
298
+ const isDirectArgument = current.arguments.some((arg) => {
299
+ const unwrappedArg = unwrapNestedExpressions(arg);
300
+ return unwrappedArg === unwrappedTarget;
301
+ });
302
+ return { hookName, isDirectArgument };
303
+ }
304
+ }
305
+ current = current.parent;
306
+ }
307
+ return null;
308
+ }
309
+ /**
310
+ * Finds the nearest owning function for the provided node.
311
+ * @param node Node to start from.
312
+ * @returns Enclosing function declaration/expression or null.
313
+ */
314
+ function findOwningFunction(node) {
315
+ let current = node.parent;
316
+ while (current) {
317
+ if (isFunctionNode(current)) {
318
+ return current;
319
+ }
320
+ current = current.parent;
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Locates the closest return statement ancestor, skipping assertion wrappers.
326
+ * @param node Node to start from.
327
+ * @returns Enclosing return statement or null.
328
+ */
329
+ function findEnclosingReturnStatement(node) {
330
+ let current = node.parent;
331
+ while (current) {
332
+ if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
333
+ return current;
334
+ }
335
+ if (isExpressionWrapper(current)) {
336
+ current = current.parent;
337
+ continue;
338
+ }
339
+ break;
340
+ }
341
+ return null;
342
+ }
343
+ /**
344
+ * Determines whether a node is returned from the provided hook function.
345
+ * @param node Expression candidate for return.
346
+ * @param owner Hook function candidate.
347
+ * @returns True when the expression is the hook's return value.
348
+ */
349
+ function isReturnValueFromHook(node, owner) {
350
+ if (!isHookFunction(owner))
351
+ return false;
352
+ if (owner.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
353
+ owner.body.type !== utils_1.AST_NODE_TYPES.BlockStatement &&
354
+ unwrapNestedExpressions(owner.body) === unwrapNestedExpressions(node)) {
355
+ return true;
356
+ }
357
+ if (node.parent?.type !== utils_1.AST_NODE_TYPES.ReturnStatement ||
358
+ node.parent.argument !== node) {
359
+ const returnStatement = findEnclosingReturnStatement(node);
360
+ if (!returnStatement?.argument) {
361
+ return false;
362
+ }
363
+ if (unwrapNestedExpressions(returnStatement.argument) !==
364
+ unwrapNestedExpressions(node)) {
365
+ return false;
366
+ }
367
+ const owningFunction = findOwningFunction(returnStatement);
368
+ return owningFunction === owner;
369
+ }
370
+ const owningFunction = findOwningFunction(node.parent);
371
+ return owningFunction === owner;
372
+ }
373
+ /**
374
+ * Builds memoization suggestions with dependency placeholders for developers.
375
+ * @param node Literal node to wrap.
376
+ * @param descriptor Literal metadata including memo hook.
377
+ * @param sourceCode Source code utility for text extraction.
378
+ * @returns Suggestion array encouraging memoization with explicit deps TODO.
379
+ */
380
+ function buildMemoSuggestions(node, descriptor, sourceCode) {
381
+ const initializerText = sourceCode.getText(node);
382
+ const wrappedInitializer = descriptor.literalType === 'object literal'
383
+ ? `(${initializerText})`
384
+ : initializerText;
385
+ if (descriptor.literalType === 'inline function') {
386
+ return [
387
+ {
388
+ messageId: 'memoizeLiteralSuggestion',
389
+ data: {
390
+ literalType: descriptor.literalType,
391
+ memoHook: descriptor.memoHook,
392
+ },
393
+ fix(fixer) {
394
+ return fixer.replaceText(node, `${descriptor.memoHook}(${initializerText}, [${TODO_DEPS_COMMENT}])`);
395
+ },
396
+ },
397
+ ];
398
+ }
399
+ return [
400
+ {
401
+ messageId: 'memoizeLiteralSuggestion',
402
+ data: {
403
+ literalType: descriptor.literalType,
404
+ memoHook: descriptor.memoHook,
405
+ },
406
+ fix(fixer) {
407
+ return fixer.replaceText(node, `${descriptor.memoHook}(() => ${wrappedInitializer}, [${TODO_DEPS_COMMENT}])`);
408
+ },
409
+ },
410
+ ];
411
+ }
412
+ /**
413
+ * Formats a readable label for diagnostics based on the owning function.
414
+ * @param fn Owning component or hook function.
415
+ * @returns Human-friendly label for error messages.
416
+ */
417
+ function formatContextLabel(fn) {
418
+ const name = getFunctionName(fn);
419
+ if (!name)
420
+ return 'this component or hook';
421
+ if (isHookName(name))
422
+ return `hook "${name}"`;
423
+ return `component "${name}"`;
424
+ }
425
+ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
426
+ name: 'react-memoize-literals',
427
+ meta: {
428
+ type: 'suggestion',
429
+ docs: {
430
+ description: 'Detect object, array, and function literals created in React components or hooks that create new references every render. Prefer memoized values (useMemo/useCallback) or module-level constants to keep referential stability.',
431
+ recommended: 'error',
432
+ },
433
+ hasSuggestions: true,
434
+ schema: [],
435
+ messages: {
436
+ componentLiteral: 'New {{literalType}} inside {{context}} is created on every render → Breaks referential stability for hooks/props and can re-run effects or re-render children → Hoist it to a module-level constant or wrap it in {{memoHook}} with the right dependencies.',
437
+ nestedHookLiteral: 'Nested {{literalType}} inside {{hookName}} arguments is recreated on every render → Dependency/reference comparisons change each time and defeat memoization/caching → Extract it into a memoized value (useMemo/useCallback) or hoist it to a module constant before passing it to {{hookName}}.',
438
+ hookReturnLiteral: '{{hookName}} returns a {{literalType}} literal on each render → Callers receive a fresh reference and may re-render or re-run effects → Memoize the returned value with useMemo/useCallback or return pre-memoized pieces so callers see stable references.',
439
+ memoizeLiteralSuggestion: 'This {{literalType}} is created inline → It produces a new reference each render → Wrap it in {{memoHook}} and include every closed-over value in the dependency array.',
440
+ },
441
+ },
442
+ defaultOptions: [],
443
+ create(context) {
444
+ const sourceCode = context.getSourceCode();
445
+ /**
446
+ * Checks whether a VariableDeclarator's sole usage is being thrown
447
+ * in the same function scope.
448
+ */
449
+ function isVariableAlwaysThrown(declarator) {
450
+ const variables = ASTHelpers_1.ASTHelpers.getDeclaredVariables(context, declarator);
451
+ if (variables.length === 0) {
452
+ return false;
453
+ }
454
+ const variable = variables[0];
455
+ const usages = variable.references.filter((ref) => !ref.init);
456
+ // Variables with no usages (dead code) don't bypass memoization checks
457
+ // because we can't prove the literal is thrown.
458
+ if (usages.length === 0) {
459
+ return false;
460
+ }
461
+ const owningFunction = findOwningFunction(declarator);
462
+ return usages.every((ref) => {
463
+ let refParent = ref.identifier
464
+ .parent;
465
+ while (refParent) {
466
+ if (isFunctionNode(refParent)) {
467
+ // Stop at function boundaries: throws inside nested functions don't
468
+ // abort the outer render cycle, so they don't make the literal terminal
469
+ // from the perspective of the declaring function.
470
+ return false;
471
+ }
472
+ if (refParent.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
473
+ // Found a throw in the same lexical scope as the usage.
474
+ // Verify the throw is in the same function where the variable was declared
475
+ // to ensure the throw terminates the render before memoization matters.
476
+ let throwCheckParent = refParent.parent;
477
+ while (throwCheckParent) {
478
+ if (isFunctionNode(throwCheckParent)) {
479
+ return throwCheckParent === owningFunction;
480
+ }
481
+ throwCheckParent =
482
+ throwCheckParent.parent;
483
+ }
484
+ return owningFunction === null;
485
+ }
486
+ if (refParent === declarator) {
487
+ break;
488
+ }
489
+ refParent = refParent.parent;
490
+ }
491
+ return false;
492
+ });
493
+ }
494
+ /**
495
+ * Checks whether a literal is destined to be thrown, making referential
496
+ * stability irrelevant.
497
+ */
498
+ function isTerminalUsage(node) {
499
+ let current = node.parent;
500
+ // Tracks whether the literal passed through a node that represents non-container usage.
501
+ // Once true, any throw we encounter won't count as "terminal" because the literal
502
+ // is already used in an expression that might need memoization.
503
+ let hasPassedForbiddenNode = false;
504
+ while (current) {
505
+ if (current.type === utils_1.AST_NODE_TYPES.ThrowStatement) {
506
+ return !hasPassedForbiddenNode;
507
+ }
508
+ if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
509
+ current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
510
+ current.init) {
511
+ if (!hasPassedForbiddenNode && isVariableAlwaysThrown(current)) {
512
+ return true;
513
+ }
514
+ }
515
+ // Stop at function boundaries as throws across functions are not "terminal"
516
+ // in the same render cycle sense for the current component/hook.
517
+ if (isFunctionNode(current)) {
518
+ break;
519
+ }
520
+ // We distinguish between "containers" (nodes that build up an error value
521
+ // or wrap an expression) and "usages" (nodes where the value is consumed
522
+ // in a way that might benefit from memoization).
523
+ //
524
+ // Containers (NewExpression for errors, wrappers, or object/array builders)
525
+ // are allowed because they are part of the path to a 'throw'.
526
+ // Other nodes (like function calls or being a prop) mark the literal as
527
+ // having a non-terminal usage.
528
+ //
529
+ // Note: AST_NODE_TYPES.CallExpression is intentionally excluded from allowed
530
+ // containers. Passing a literal into a CallExpression allows the callee to
531
+ // observe or store the reference, making its identity relevant even if
532
+ // the result is eventually thrown (e.g., const err = fn({ ... }); throw err;).
533
+ if (current.type !== utils_1.AST_NODE_TYPES.ObjectExpression &&
534
+ current.type !== utils_1.AST_NODE_TYPES.Property &&
535
+ current.type !== utils_1.AST_NODE_TYPES.ArrayExpression &&
536
+ current.type !== utils_1.AST_NODE_TYPES.NewExpression &&
537
+ current.type !== utils_1.AST_NODE_TYPES.ConditionalExpression &&
538
+ current.type !== utils_1.AST_NODE_TYPES.LogicalExpression &&
539
+ !isExpressionWrapper(current)) {
540
+ hasPassedForbiddenNode = true;
541
+ }
542
+ current = current.parent;
543
+ }
544
+ return false;
545
+ }
546
+ function reportLiteral(node) {
547
+ const descriptor = getLiteralDescriptor(node);
548
+ if (!descriptor)
549
+ return;
550
+ const owner = findEnclosingComponentOrHook(node);
551
+ if (!owner)
552
+ return;
553
+ if (isInsideAllowedHookCallback(node)) {
554
+ return;
555
+ }
556
+ if (isTerminalUsage(node)) {
557
+ return;
558
+ }
559
+ const hookCall = findEnclosingHookCall(node);
560
+ if (hookCall) {
561
+ if (hookCall.isDirectArgument) {
562
+ // Top-level literal passed directly to a hook argument is allowed.
563
+ return;
564
+ }
565
+ context.report({
566
+ node,
567
+ messageId: 'nestedHookLiteral',
568
+ data: {
569
+ literalType: descriptor.literalType,
570
+ hookName: hookCall.hookName,
571
+ },
572
+ });
573
+ return;
574
+ }
575
+ if (isReturnValueFromHook(node, owner)) {
576
+ const hookName = getFunctionName(owner) ?? 'this hook';
577
+ context.report({
578
+ node,
579
+ messageId: 'hookReturnLiteral',
580
+ data: {
581
+ literalType: descriptor.literalType,
582
+ hookName,
583
+ },
584
+ });
585
+ return;
586
+ }
587
+ const contextLabel = formatContextLabel(owner);
588
+ // Only emit auto-fix suggestions for simple variable initializers; other
589
+ // contexts (returns, JSX props, nested expressions) risk unsafe rewrites.
590
+ const suggestions = node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
591
+ node.parent.init === node
592
+ ? buildMemoSuggestions(node, descriptor, sourceCode)
593
+ : undefined;
594
+ context.report({
595
+ node,
596
+ messageId: 'componentLiteral',
597
+ data: {
598
+ literalType: descriptor.literalType,
599
+ context: contextLabel,
600
+ memoHook: descriptor.memoHook,
601
+ },
602
+ suggest: suggestions,
603
+ });
604
+ }
605
+ return {
606
+ ObjectExpression: reportLiteral,
607
+ ArrayExpression: reportLiteral,
608
+ ArrowFunctionExpression: reportLiteral,
609
+ FunctionExpression: reportLiteral,
610
+ FunctionDeclaration: reportLiteral,
611
+ };
612
+ },
613
+ });
614
+ //# sourceMappingURL=react-memoize-literals.js.map
@@ -47,7 +47,7 @@ const isUsedAsComponentProp = (context, variableName, node) => {
47
47
  break;
48
48
  }
49
49
  }
50
- const sourceCode = context.getSourceCode();
50
+ const sourceCode = context.sourceCode;
51
51
  const scopeManager = sourceCode.scopeManager;
52
52
  const scope = scopeManager &&
53
53
  ((fn && scopeManager.acquire(fn)) || scopeManager.acquire(node));
@@ -75,7 +75,7 @@ const isUsedAsComponentProp = (context, variableName, node) => {
75
75
  const isUsedAsJsxChild = (context, variableName, node) => {
76
76
  // Traverse the nearest function (or program) to see if the identifier appears
77
77
  // as a JSX child expression.
78
- const sourceCode = context.getSourceCode();
78
+ const sourceCode = context.sourceCode;
79
79
  let container = sourceCode.ast;
80
80
  let current = node;
81
81
  while (current?.parent) {