@blumintinc/eslint-plugin-blumint 1.13.0 → 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 (186) hide show
  1. package/README.md +170 -139
  2. package/lib/index.js +190 -1
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  5. package/lib/rules/consistent-callback-naming.js +1 -1
  6. package/lib/rules/dynamic-https-errors.js +3 -4
  7. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  8. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -23
  9. package/lib/rules/enforce-callback-memo.js +1 -1
  10. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  11. package/lib/rules/enforce-console-error.js +35 -7
  12. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  13. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  14. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  15. package/lib/rules/enforce-early-destructuring.js +980 -0
  16. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  17. package/lib/rules/enforce-empty-object-check.js +502 -0
  18. package/lib/rules/enforce-exported-function-types.js +1 -1
  19. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  20. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  21. package/lib/rules/enforce-firestore-facade.js +4 -2
  22. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  23. package/lib/rules/enforce-global-constants.js +1 -1
  24. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  25. package/lib/rules/enforce-memoize-async.js +2 -6
  26. package/lib/rules/enforce-memoize-getters.js +39 -15
  27. package/lib/rules/enforce-microdiff.js +1 -1
  28. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  29. package/lib/rules/enforce-positive-naming.js +4 -0
  30. package/lib/rules/enforce-props-argument-name.js +2 -2
  31. package/lib/rules/enforce-safe-stringify.js +1 -1
  32. package/lib/rules/enforce-serializable-params.js +3 -3
  33. package/lib/rules/enforce-singular-type-names.js +1 -1
  34. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  35. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  36. package/lib/rules/enforce-storage-context.d.ts +9 -0
  37. package/lib/rules/enforce-storage-context.js +646 -0
  38. package/lib/rules/enforce-timestamp-now.js +23 -7
  39. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  40. package/lib/rules/enforce-transform-memoization.js +416 -0
  41. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  42. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  43. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  44. package/lib/rules/enforce-verb-noun-naming.js +5 -4
  45. package/lib/rules/ensure-pointer-events-none.js +28 -2
  46. package/lib/rules/export-if-in-doubt.js +27 -4
  47. package/lib/rules/extract-global-constants.js +40 -13
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  49. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  50. package/lib/rules/flatten-push-calls.d.ts +2 -0
  51. package/lib/rules/flatten-push-calls.js +428 -0
  52. package/lib/rules/global-const-style.js +50 -19
  53. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  54. package/lib/rules/jsdoc-above-field.js +208 -0
  55. package/lib/rules/key-only-outermost-element.js +21 -3
  56. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  57. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  58. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  59. package/lib/rules/memo-compare-deeply-complex-props.js +788 -0
  60. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  61. package/lib/rules/memo-nested-react-components.js +410 -0
  62. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  63. package/lib/rules/no-always-true-false-conditions.js +68 -8
  64. package/lib/rules/no-array-length-in-deps.js +9 -3
  65. package/lib/rules/no-async-foreach.js +210 -12
  66. package/lib/rules/no-circular-references.js +18 -17
  67. package/lib/rules/no-class-instance-destructuring.js +55 -12
  68. package/lib/rules/no-complex-cloud-params.js +7 -3
  69. package/lib/rules/no-compositing-layer-props.js +2 -2
  70. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  71. package/lib/rules/no-console-error.d.ts +9 -0
  72. package/lib/rules/no-console-error.js +525 -0
  73. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  74. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  75. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  76. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  77. package/lib/rules/no-entire-object-hook-deps.js +4 -6
  78. package/lib/rules/no-excessive-parent-chain.js +1 -1
  79. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  80. package/lib/rules/no-explicit-return-type.js +215 -26
  81. package/lib/rules/no-filter-without-return.js +5 -1
  82. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  83. package/lib/rules/no-firestore-jest-mock.js +126 -8
  84. package/lib/rules/no-firestore-object-arrays.js +67 -12
  85. package/lib/rules/no-handler-suffix.d.ts +12 -0
  86. package/lib/rules/no-handler-suffix.js +305 -0
  87. package/lib/rules/no-hungarian.js +1 -1
  88. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  89. package/lib/rules/no-inline-component-prop.js +456 -0
  90. package/lib/rules/no-jsx-in-hooks.js +6 -1
  91. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  92. package/lib/rules/no-margin-properties.js +6 -6
  93. package/lib/rules/no-memoize-on-static.js +9 -1
  94. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  95. package/lib/rules/no-misused-switch-case.js +22 -1
  96. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  97. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  98. package/lib/rules/no-mock-firebase-admin.js +5 -2
  99. package/lib/rules/no-object-values-on-strings.js +10 -2
  100. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  101. package/lib/rules/no-passthrough-getters.js +38 -2
  102. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  103. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  104. package/lib/rules/no-redundant-param-types.js +16 -6
  105. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  106. package/lib/rules/no-redundant-this-params.js +459 -0
  107. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  108. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  109. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  110. package/lib/rules/no-restricted-properties-fix.js +11 -10
  111. package/lib/rules/no-separate-loading-state.js +7 -16
  112. package/lib/rules/no-stale-state-across-await.js +1 -1
  113. package/lib/rules/no-type-assertion-returns.js +43 -49
  114. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  115. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  116. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  117. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  118. package/lib/rules/no-unnecessary-destructuring-rename.js +347 -0
  119. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  120. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  121. package/lib/rules/no-unpinned-dependencies.js +36 -5
  122. package/lib/rules/no-unused-props.d.ts +2 -2
  123. package/lib/rules/no-unused-props.js +295 -91
  124. package/lib/rules/no-unused-usestate.js +6 -2
  125. package/lib/rules/no-useless-fragment.js +28 -2
  126. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  127. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  128. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  129. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  130. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  131. package/lib/rules/omit-index-html.d.ts +2 -1
  132. package/lib/rules/omit-index-html.js +62 -7
  133. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  134. package/lib/rules/parallelize-async-operations.js +24 -5
  135. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  136. package/lib/rules/prefer-batch-operations.js +32 -5
  137. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  138. package/lib/rules/prefer-clone-deep.js +3 -3
  139. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  140. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  141. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  142. package/lib/rules/prefer-docsetter-setall.js +243 -0
  143. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  144. package/lib/rules/prefer-fragment-component.js +1 -1
  145. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  146. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  147. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  148. package/lib/rules/prefer-global-router-state-key.js +88 -13
  149. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  150. package/lib/rules/prefer-memoized-props.js +445 -0
  151. package/lib/rules/prefer-next-dynamic.js +60 -69
  152. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +9 -4
  153. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  154. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  155. package/lib/rules/prefer-params-over-parent-id.js +220 -171
  156. package/lib/rules/prefer-settings-object.js +2 -2
  157. package/lib/rules/prefer-type-over-interface.js +7 -2
  158. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  159. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  160. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  161. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  162. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  163. package/lib/rules/prevent-children-clobber.js +536 -0
  164. package/lib/rules/react-memoize-literals.d.ts +4 -0
  165. package/lib/rules/react-memoize-literals.js +495 -0
  166. package/lib/rules/react-usememo-should-be-component.js +2 -2
  167. package/lib/rules/require-hooks-default-params.js +17 -16
  168. package/lib/rules/require-https-error-cause.d.ts +4 -0
  169. package/lib/rules/require-https-error-cause.js +136 -0
  170. package/lib/rules/require-https-error.js +43 -21
  171. package/lib/rules/require-image-optimized.js +1 -1
  172. package/lib/rules/require-memo.js +1 -1
  173. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  174. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  175. package/lib/rules/require-usememo-object-literals.js +2 -3
  176. package/lib/rules/test-file-location-enforcement.js +1 -1
  177. package/lib/rules/use-latest-callback.js +6 -5
  178. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  179. package/lib/rules/vertically-group-related-functions.js +480 -0
  180. package/lib/utils/ASTHelpers.d.ts +13 -1
  181. package/lib/utils/ASTHelpers.js +24 -0
  182. package/lib/utils/getMethodName.d.ts +27 -0
  183. package/lib/utils/getMethodName.js +35 -0
  184. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  185. package/lib/utils/tsTypeClassifier.js +149 -0
  186. package/package.json +3 -1
@@ -0,0 +1,8 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type Options = [
3
+ {
4
+ ignorePatterns?: string[];
5
+ }
6
+ ];
7
+ export declare const memoNestedReactComponents: TSESLint.RuleModule<"memoizeNestedComponent", Options, TSESLint.RuleListener>;
8
+ export {};
@@ -0,0 +1,410 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoNestedReactComponents = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const minimatch_1 = require("minimatch");
6
+ const createRule_1 = require("../utils/createRule");
7
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
8
+ const CALLBACK_HOOKS = new Set(['useCallback', 'useDeepCompareCallback']);
9
+ const HOOK_REPLACEMENT = {
10
+ useCallback: 'useMemo',
11
+ useDeepCompareCallback: 'useDeepCompareMemo',
12
+ };
13
+ const collectReactImports = (sourceCode) => {
14
+ const reactImports = { namespace: null, named: {} };
15
+ for (const statement of sourceCode.ast.body) {
16
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration)
17
+ continue;
18
+ if (statement.source.value !== 'react')
19
+ continue;
20
+ for (const specifier of statement.specifiers) {
21
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
22
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier) {
23
+ const importedName = specifier.imported.name;
24
+ if (importedName === 'createElement' ||
25
+ importedName === 'memo' ||
26
+ importedName === 'forwardRef') {
27
+ reactImports.named[importedName] = specifier.local.name;
28
+ }
29
+ }
30
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
31
+ specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
32
+ reactImports.namespace = specifier.local.name;
33
+ }
34
+ }
35
+ }
36
+ return reactImports;
37
+ };
38
+ const calleeMatchesReactMember = (callee, reactImports, member) => {
39
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
40
+ return reactImports.named[member] === callee.name;
41
+ }
42
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
43
+ !callee.computed &&
44
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
45
+ callee.property.name === member &&
46
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
47
+ reactImports.namespace === callee.object.name) {
48
+ return true;
49
+ }
50
+ return false;
51
+ };
52
+ const isFunctionExpression = (node) => {
53
+ return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
54
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression);
55
+ };
56
+ const unwrapExpression = (expression) => {
57
+ if (expression.type === utils_1.AST_NODE_TYPES.TSAsExpression) {
58
+ return unwrapExpression(expression.expression);
59
+ }
60
+ if (expression.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression) {
61
+ return unwrapExpression(expression.expression);
62
+ }
63
+ if (expression.type === utils_1.AST_NODE_TYPES.TSNonNullExpression) {
64
+ return unwrapExpression(expression.expression);
65
+ }
66
+ /**
67
+ * ParenthesizedExpression appears in the parsed AST even though the type
68
+ * is missing from AST_NODE_TYPES. This assertion safely unwraps to keep
69
+ * traversal consistent.
70
+ */
71
+ if (expression.type ===
72
+ 'ParenthesizedExpression') {
73
+ return unwrapExpression(expression.expression);
74
+ }
75
+ return expression;
76
+ };
77
+ const isReactCreateElementCall = (node, reactImports) => {
78
+ return calleeMatchesReactMember(node.callee, reactImports, 'createElement');
79
+ };
80
+ const isHookCall = (callee) => {
81
+ if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
82
+ if (CALLBACK_HOOKS.has(callee.name)) {
83
+ return { name: callee.name };
84
+ }
85
+ return null;
86
+ }
87
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
88
+ !callee.computed &&
89
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
90
+ CALLBACK_HOOKS.has(callee.property.name)) {
91
+ return { name: callee.property.name };
92
+ }
93
+ return null;
94
+ };
95
+ const isForwardRefCall = (node, reactImports) => {
96
+ return calleeMatchesReactMember(node.callee, reactImports, 'forwardRef');
97
+ };
98
+ const isMemoCall = (node, reactImports) => {
99
+ return calleeMatchesReactMember(node.callee, reactImports, 'memo');
100
+ };
101
+ const filterPresentResults = (results) => {
102
+ return results.filter((result) => Boolean(result));
103
+ };
104
+ const areAllComponentsCallbacks = (components) => {
105
+ return components.every((result) => result.componentIsCallback);
106
+ };
107
+ const createMergedResult = (components) => {
108
+ return {
109
+ found: true,
110
+ // Auto-fix wraps the callback in memo() and swaps the hook. That is only valid when
111
+ // every branch is itself a component callback (returns JSX/createElement directly).
112
+ // If any branch yields a component factory, the transformation changes behavior.
113
+ componentIsCallback: areAllComponentsCallbacks(components),
114
+ };
115
+ };
116
+ const mergeComponentResults = (...results) => {
117
+ const present = filterPresentResults(results);
118
+ if (!present.length) {
119
+ return null;
120
+ }
121
+ return createMergedResult(present);
122
+ };
123
+ const expressionCreatesComponent = (expression, reactImports) => {
124
+ if (!expression) {
125
+ return null;
126
+ }
127
+ const unwrapped = unwrapExpression(expression);
128
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
129
+ unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment) {
130
+ return { found: true, componentIsCallback: true };
131
+ }
132
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression &&
133
+ isReactCreateElementCall(unwrapped, reactImports)) {
134
+ return { found: true, componentIsCallback: true };
135
+ }
136
+ switch (unwrapped.type) {
137
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
138
+ case utils_1.AST_NODE_TYPES.FunctionExpression: {
139
+ const inner = functionCreatesComponent(unwrapped, reactImports);
140
+ return inner ? { found: true, componentIsCallback: false } : null;
141
+ }
142
+ case utils_1.AST_NODE_TYPES.CallExpression: {
143
+ if (isForwardRefCall(unwrapped, reactImports) ||
144
+ isMemoCall(unwrapped, reactImports)) {
145
+ const firstArg = unwrapped.arguments[0];
146
+ if (firstArg && isFunctionExpression(firstArg)) {
147
+ const inner = functionCreatesComponent(firstArg, reactImports);
148
+ if (inner) {
149
+ return { found: true, componentIsCallback: false };
150
+ }
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ case utils_1.AST_NODE_TYPES.ConditionalExpression: {
156
+ const cons = expressionCreatesComponent(unwrapped.consequent, reactImports);
157
+ const alt = expressionCreatesComponent(unwrapped.alternate, reactImports);
158
+ return mergeComponentResults(cons, alt);
159
+ }
160
+ case utils_1.AST_NODE_TYPES.LogicalExpression: {
161
+ const left = expressionCreatesComponent(unwrapped.left, reactImports);
162
+ const right = expressionCreatesComponent(unwrapped.right, reactImports);
163
+ return mergeComponentResults(left, right);
164
+ }
165
+ case utils_1.AST_NODE_TYPES.SequenceExpression: {
166
+ const last = unwrapped.expressions[unwrapped.expressions.length - 1];
167
+ return expressionCreatesComponent(last || null, reactImports);
168
+ }
169
+ case utils_1.AST_NODE_TYPES.ArrayExpression: {
170
+ const matches = unwrapped.elements
171
+ .map((element) => element && element.type !== utils_1.AST_NODE_TYPES.SpreadElement
172
+ ? expressionCreatesComponent(element, reactImports)
173
+ : null)
174
+ .filter(Boolean);
175
+ return mergeComponentResults(...matches);
176
+ }
177
+ default:
178
+ return null;
179
+ }
180
+ };
181
+ const statementCreatesComponent = (node, reactImports) => {
182
+ switch (node.type) {
183
+ case utils_1.AST_NODE_TYPES.ReturnStatement:
184
+ return expressionCreatesComponent(node.argument, reactImports);
185
+ case utils_1.AST_NODE_TYPES.IfStatement: {
186
+ const cons = node.consequent.type === utils_1.AST_NODE_TYPES.BlockStatement
187
+ ? blockCreatesComponent(node.consequent, reactImports)
188
+ : statementCreatesComponent(node.consequent, reactImports);
189
+ const alt = node.alternate
190
+ ? node.alternate.type === utils_1.AST_NODE_TYPES.BlockStatement
191
+ ? blockCreatesComponent(node.alternate, reactImports)
192
+ : statementCreatesComponent(node.alternate, reactImports)
193
+ : null;
194
+ return mergeComponentResults(cons, alt);
195
+ }
196
+ case utils_1.AST_NODE_TYPES.SwitchStatement: {
197
+ const matches = [];
198
+ for (const switchCase of node.cases) {
199
+ for (const consequent of switchCase.consequent) {
200
+ matches.push(statementCreatesComponent(consequent, reactImports));
201
+ }
202
+ }
203
+ return mergeComponentResults(...matches);
204
+ }
205
+ case utils_1.AST_NODE_TYPES.BlockStatement:
206
+ return blockCreatesComponent(node, reactImports);
207
+ case utils_1.AST_NODE_TYPES.ForStatement:
208
+ case utils_1.AST_NODE_TYPES.ForInStatement:
209
+ case utils_1.AST_NODE_TYPES.ForOfStatement:
210
+ case utils_1.AST_NODE_TYPES.WhileStatement:
211
+ case utils_1.AST_NODE_TYPES.DoWhileStatement: {
212
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
213
+ return blockCreatesComponent(node.body, reactImports);
214
+ }
215
+ return statementCreatesComponent(node.body, reactImports);
216
+ }
217
+ case utils_1.AST_NODE_TYPES.TryStatement: {
218
+ const blockMatch = blockCreatesComponent(node.block, reactImports);
219
+ const handlerMatch = node.handler?.body
220
+ ? blockCreatesComponent(node.handler.body, reactImports)
221
+ : null;
222
+ const finalizerMatch = node.finalizer
223
+ ? blockCreatesComponent(node.finalizer, reactImports)
224
+ : null;
225
+ return mergeComponentResults(blockMatch, handlerMatch, finalizerMatch);
226
+ }
227
+ default:
228
+ return null;
229
+ }
230
+ };
231
+ const blockCreatesComponent = (block, reactImports) => {
232
+ const matches = [];
233
+ for (const statement of block.body) {
234
+ matches.push(statementCreatesComponent(statement, reactImports));
235
+ }
236
+ return mergeComponentResults(...matches);
237
+ };
238
+ const functionCreatesComponent = (node, reactImports) => {
239
+ if (node.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
240
+ return blockCreatesComponent(node.body, reactImports);
241
+ }
242
+ return expressionCreatesComponent(node.body, reactImports);
243
+ };
244
+ const shouldIgnoreFile = (filename, patterns) => {
245
+ return patterns.some((pattern) => (0, minimatch_1.minimatch)(filename, pattern));
246
+ };
247
+ const getVariableName = (node) => {
248
+ if (node.parent &&
249
+ node.parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
250
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
251
+ return node.parent.id.name;
252
+ }
253
+ return null;
254
+ };
255
+ const findMemoReference = (reactImports) => {
256
+ if (reactImports.namespace) {
257
+ return `${reactImports.namespace}.memo`;
258
+ }
259
+ return reactImports.named.memo ?? null;
260
+ };
261
+ const hasIdentifierInScope = (identifierName, scope) => {
262
+ return !!ASTHelpers_1.ASTHelpers.findVariableInScope(scope, identifierName);
263
+ };
264
+ exports.memoNestedReactComponents = (0, createRule_1.createRule)({
265
+ name: 'memo-nested-react-components',
266
+ meta: {
267
+ type: 'suggestion',
268
+ docs: {
269
+ description: 'Disallow React components defined in useCallback/useDeepCompareCallback',
270
+ recommended: 'error',
271
+ },
272
+ fixable: 'code',
273
+ schema: [
274
+ {
275
+ type: 'object',
276
+ properties: {
277
+ ignorePatterns: {
278
+ type: 'array',
279
+ items: { type: 'string' },
280
+ default: [],
281
+ },
282
+ },
283
+ additionalProperties: false,
284
+ },
285
+ ],
286
+ messages: {
287
+ memoizeNestedComponent: `What's wrong: React component "{{componentName}}" is created inside {{hookName}}.
288
+ Why it matters: Components defined inside callbacks get new identities when the callback changes, causing React to remount them and drop their state and effects.
289
+ How to fix: Create the component via {{replacementHook}} and wrap it in memo() so its identity stays stable across renders.`,
290
+ },
291
+ },
292
+ defaultOptions: [{}],
293
+ create(context, [options]) {
294
+ const ignorePatterns = options?.ignorePatterns ?? [];
295
+ const filename = context.filename ?? context.getFilename();
296
+ if (ignorePatterns.length && shouldIgnoreFile(filename, ignorePatterns)) {
297
+ return {};
298
+ }
299
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
300
+ const reactImports = collectReactImports(sourceCode);
301
+ const memoReference = findMemoReference(reactImports);
302
+ return {
303
+ CallExpression(node) {
304
+ const hook = isHookCall(node.callee);
305
+ if (!hook)
306
+ return;
307
+ const callback = node.arguments[0];
308
+ if (!callback || callback.type === utils_1.AST_NODE_TYPES.SpreadElement) {
309
+ return;
310
+ }
311
+ const componentMatch = isFunctionExpression(callback)
312
+ ? functionCreatesComponent(callback, reactImports)
313
+ : expressionCreatesComponent(callback, reactImports);
314
+ if (!componentMatch) {
315
+ return;
316
+ }
317
+ const componentName = getVariableName(node) ??
318
+ (isFunctionExpression(callback) &&
319
+ callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
320
+ ? callback.id.name
321
+ : 'component');
322
+ const replacementHook = HOOK_REPLACEMENT[hook.name];
323
+ context.report({
324
+ node,
325
+ messageId: 'memoizeNestedComponent',
326
+ data: {
327
+ componentName,
328
+ hookName: `${hook.name}()`,
329
+ replacementHook: `${replacementHook}()`,
330
+ },
331
+ fix: (fixer) => {
332
+ if (!memoReference) {
333
+ return null;
334
+ }
335
+ const memoNamespace = memoReference.endsWith('.memo')
336
+ ? memoReference.slice(0, -'.memo'.length)
337
+ : null;
338
+ // Prefer SourceCode#getScope when available; fall back to RuleContext#getScope for ESLint compatibility.
339
+ const sourceCodeWithScope = sourceCode;
340
+ const scope = typeof sourceCodeWithScope.getScope === 'function'
341
+ ? sourceCodeWithScope.getScope(node)
342
+ : context.getScope();
343
+ const replacementIdentifierAvailable = hook.name === 'useCallback'
344
+ ? hasIdentifierInScope(HOOK_REPLACEMENT.useCallback, scope)
345
+ : hasIdentifierInScope(HOOK_REPLACEMENT.useDeepCompareCallback, scope);
346
+ if (!replacementIdentifierAvailable) {
347
+ // When the standalone replacement hook (useMemo/useDeepCompareMemo) is not imported,
348
+ // we can only fix member expressions that use the React namespace.
349
+ if (node.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression ||
350
+ node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier) {
351
+ return null;
352
+ }
353
+ // Ensure memo's namespace matches the hook's namespace (e.g., React.useCallback → React.useMemo + React.memo).
354
+ if (memoReference &&
355
+ memoReference.includes('.') &&
356
+ memoReference !== `${node.callee.object.name}.memo`) {
357
+ return null;
358
+ }
359
+ }
360
+ // Component factories (functions returning functions that return JSX) require manual refactoring.
361
+ if (!componentMatch.componentIsCallback) {
362
+ return null;
363
+ }
364
+ // Ensure callback is a function expression before attempting to wrap it.
365
+ if (!isFunctionExpression(callback)) {
366
+ return null;
367
+ }
368
+ // React.memo() expects a synchronous component function. Wrapping async functions or
369
+ // generators would produce a Promise/yielding component, which is not renderable.
370
+ if (callback.async ||
371
+ (callback.type === utils_1.AST_NODE_TYPES.FunctionExpression &&
372
+ callback.generator)) {
373
+ return null;
374
+ }
375
+ // Avoid rewriting non-React namespaces (e.g., Hooks.useCallback → Hooks.useMemo),
376
+ // even when useMemo/memo are available as named imports.
377
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
378
+ if (node.callee.object.type !== utils_1.AST_NODE_TYPES.Identifier ||
379
+ !reactImports.namespace ||
380
+ node.callee.object.name !== reactImports.namespace) {
381
+ return null;
382
+ }
383
+ }
384
+ // Prevent mixing namespaces: don't apply fix when hook and memo use different React imports.
385
+ if (memoNamespace &&
386
+ node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
387
+ node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
388
+ node.callee.object.name !== memoNamespace) {
389
+ return null;
390
+ }
391
+ const callbackText = sourceCode.getText(callback);
392
+ const callbackReplacement = `() => ${memoReference}(${callbackText})`;
393
+ const fixes = [
394
+ fixer.replaceText(callback, callbackReplacement),
395
+ ];
396
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
397
+ fixes.push(fixer.replaceText(node.callee, HOOK_REPLACEMENT[node.callee.name]));
398
+ }
399
+ else if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
400
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
401
+ fixes.push(fixer.replaceText(node.callee.property, HOOK_REPLACEMENT[node.callee.property.name]));
402
+ }
403
+ return fixes;
404
+ },
405
+ });
406
+ },
407
+ };
408
+ },
409
+ });
410
+ //# sourceMappingURL=memo-nested-react-components.js.map
@@ -1,3 +1,4 @@
1
+ import type { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'alwaysTrueCondition' | 'alwaysFalseCondition';
2
- export declare const noAlwaysTrueFalseConditions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export declare const noAlwaysTrueFalseConditions: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
3
4
  export {};
@@ -13,16 +13,69 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
13
13
  },
14
14
  schema: [],
15
15
  messages: {
16
- alwaysTrueCondition: 'This condition is always true, which may indicate a mistake or unnecessary code.',
17
- alwaysFalseCondition: 'This condition is always false, which may indicate a mistake or dead code.',
16
+ alwaysTrueCondition: 'What\'s wrong → Condition "{{condition}}" is always true.\nWhy it matters → The guarded branch runs every time, which can hide logic errors and leave redundant checks.\nHow to fix → Remove the check, or rewrite "{{condition}}" so it depends on runtime values instead of constants.',
17
+ alwaysFalseCondition: 'What\'s wrong → Condition "{{condition}}" is always false.\nWhy it matters → The guarded branch is unreachable, which leaves misleading or dead code.\nHow to fix → Remove the unreachable branch, or adjust "{{condition}}" so it can evaluate to true when intended.',
18
18
  },
19
19
  },
20
20
  defaultOptions: [],
21
21
  create(context) {
22
+ const sourceCode = context.getSourceCode();
23
+ const normalizedConditionText = (node) => sourceCode.getText(node).replace(/\s+/g, ' ').trim();
24
+ const conditionTextFor = (node) => ({
25
+ condition: normalizedConditionText(node),
26
+ });
22
27
  // Track nodes that have already been reported to prevent duplicate reports
23
28
  const reportedNodes = new Set();
24
29
  // Track parent nodes that have been evaluated to prevent duplicate reports on children
25
30
  const evaluatedParentNodes = new Set();
31
+ // Sentinel distinguishes "no const literal resolved" from valid literal values (including null/undefined)
32
+ const NOT_FOUND = Symbol('literal-not-found');
33
+ /**
34
+ * Walks the current scope chain to locate a variable by name so identifier
35
+ * lookups can resolve const initializers even when declared in parent scopes.
36
+ */
37
+ function findVariableInScopes(name) {
38
+ let currentScope = context.getScope();
39
+ while (currentScope) {
40
+ const variable = currentScope.variables.find((variableItem) => variableItem.name === name);
41
+ if (variable) {
42
+ return variable;
43
+ }
44
+ currentScope = currentScope.upper;
45
+ }
46
+ return undefined;
47
+ }
48
+ function isConstIdentifierDefinition(def, identifierName) {
49
+ return (def.type === 'Variable' &&
50
+ def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
51
+ def.node.id.type === utils_1.AST_NODE_TYPES.Identifier &&
52
+ def.node.id.name === identifierName &&
53
+ def.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
54
+ def.parent.kind === 'const');
55
+ }
56
+ function findConstIdentifierDefinition(variable, identifierName) {
57
+ return variable.defs.find((def) => isConstIdentifierDefinition(def, identifierName));
58
+ }
59
+ /**
60
+ * Resolves an identifier to the literal it is initialized with when declared
61
+ * as a const. Returns the NOT_FOUND sentinel when the binding is absent,
62
+ * not a const, or not initialized with a literal value.
63
+ */
64
+ function resolveIdentifierValue(identifier) {
65
+ const variable = findVariableInScopes(identifier.name);
66
+ if (!variable) {
67
+ return NOT_FOUND;
68
+ }
69
+ const definition = findConstIdentifierDefinition(variable, identifier.name);
70
+ if (!definition) {
71
+ return NOT_FOUND;
72
+ }
73
+ const initializer = definition.node.init;
74
+ if (initializer?.type === utils_1.AST_NODE_TYPES.Literal) {
75
+ return initializer.value;
76
+ }
77
+ return NOT_FOUND;
78
+ }
26
79
  /**
27
80
  * Checks if a literal value is always truthy or falsy
28
81
  */
@@ -1309,6 +1362,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1309
1362
  context.report({
1310
1363
  node,
1311
1364
  messageId: 'alwaysTrueCondition',
1365
+ data: conditionTextFor(node),
1312
1366
  });
1313
1367
  reportedNodes.add(node);
1314
1368
  }
@@ -1316,6 +1370,7 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1316
1370
  context.report({
1317
1371
  node,
1318
1372
  messageId: 'alwaysFalseCondition',
1373
+ data: conditionTextFor(node),
1319
1374
  });
1320
1375
  reportedNodes.add(node);
1321
1376
  }
@@ -1372,12 +1427,14 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1372
1427
  context.report({
1373
1428
  node: node.test,
1374
1429
  messageId: 'alwaysTrueCondition',
1430
+ data: conditionTextFor(node.test),
1375
1431
  });
1376
1432
  }
1377
1433
  else {
1378
1434
  context.report({
1379
1435
  node: node.test,
1380
1436
  messageId: 'alwaysFalseCondition',
1437
+ data: conditionTextFor(node.test),
1381
1438
  });
1382
1439
  }
1383
1440
  reportedNodes.add(node.test);
@@ -1385,14 +1442,17 @@ exports.noAlwaysTrueFalseConditions = (0, createRule_1.createRule)({
1385
1442
  }
1386
1443
  // Special cases for tests with variables
1387
1444
  if (switchStatement.discriminant.type === utils_1.AST_NODE_TYPES.Literal &&
1388
- node.test.type === utils_1.AST_NODE_TYPES.Identifier &&
1389
- node.test.name === 'value') {
1390
- // These special cases match the test expectations
1391
- // In a real implementation, you would want to track variable assignments
1392
- if (!reportedNodes.has(node.test)) {
1445
+ node.test.type === utils_1.AST_NODE_TYPES.Identifier) {
1446
+ const identifierValue = resolveIdentifierValue(node.test);
1447
+ if (identifierValue !== NOT_FOUND &&
1448
+ !reportedNodes.has(node.test)) {
1449
+ const messageId = identifierValue === switchStatement.discriminant.value
1450
+ ? 'alwaysTrueCondition'
1451
+ : 'alwaysFalseCondition';
1393
1452
  context.report({
1394
1453
  node: node.test,
1395
- messageId: 'alwaysFalseCondition',
1454
+ messageId,
1455
+ data: conditionTextFor(node.test),
1396
1456
  });
1397
1457
  reportedNodes.add(node.test);
1398
1458
  }
@@ -141,7 +141,7 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
141
141
  meta: {
142
142
  type: 'problem',
143
143
  docs: {
144
- description: 'Prevent using array.length in React hook dependency arrays. Instead, memoize stableHash(array) with useMemo and depend on the hash.',
144
+ description: 'Detects array.length entries in React hook dependency arrays because length ignores content changes; auto-fixes by memoizing stableHash(array) with useMemo and depending on the hash instead.',
145
145
  recommended: 'error',
146
146
  },
147
147
  fixable: 'code',
@@ -162,7 +162,7 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
162
162
  },
163
163
  ],
164
164
  messages: {
165
- noArrayLengthInDeps: 'Avoid using array.length in hook dependency arrays. Use a memoized stableHash(variable) via useMemo and depend on the hash instead.',
165
+ noArrayLengthInDeps: 'Dependency array includes length-based entries ({{dependencies}}). Array length only changes when items are added or removed, so hooks miss updates when array contents change at the same size. Memoize a stableHash of each array with useMemo and depend on that hash so the hook reruns when contents change.',
166
166
  },
167
167
  },
168
168
  defaultOptions: [{}],
@@ -200,12 +200,18 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
200
200
  }
201
201
  if (lengthDeps.length === 0)
202
202
  return;
203
+ const sourceCode = context.getSourceCode();
204
+ const dependencies = lengthDeps
205
+ .map(({ element }) => sourceCode.getText(element))
206
+ .join(', ');
203
207
  // Report once on the dependency array
204
208
  context.report({
205
209
  node: depsArg,
206
210
  messageId: 'noArrayLengthInDeps',
211
+ data: {
212
+ dependencies,
213
+ },
207
214
  fix(fixer) {
208
- const sourceCode = context.getSourceCode();
209
215
  const fixes = [];
210
216
  const hostFn = findEnclosingFunction(node) ?? sourceCode.ast;
211
217
  const declaredBases = ensureWeakMapEntry(perFuncDeclaredBases, hostFn, () => new Set());