@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,757 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.noUsememoForPassByValue = void 0;
7
+ const utils_1 = require("@typescript-eslint/utils");
8
+ const typescript_1 = __importDefault(require("typescript"));
9
+ const createRule_1 = require("../utils/createRule");
10
+ const DEFAULT_EXPENSIVE_PATTERNS = [
11
+ 'compute',
12
+ 'calculate',
13
+ 'derive',
14
+ 'generate',
15
+ 'expensive',
16
+ 'heavy',
17
+ 'hash',
18
+ ];
19
+ const PASS_BY_VALUE_FLAGS = typescript_1.default.TypeFlags.StringLike |
20
+ typescript_1.default.TypeFlags.NumberLike |
21
+ typescript_1.default.TypeFlags.BigIntLike |
22
+ typescript_1.default.TypeFlags.BooleanLike |
23
+ typescript_1.default.TypeFlags.Undefined |
24
+ typescript_1.default.TypeFlags.Null;
25
+ function isCustomHookName(name) {
26
+ if (!name)
27
+ return false;
28
+ return /^use[A-Z]/.test(name);
29
+ }
30
+ function getFunctionName(node) {
31
+ if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
32
+ node.id?.type === utils_1.AST_NODE_TYPES.Identifier) {
33
+ return node.id.name;
34
+ }
35
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
36
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
37
+ return node.parent.id.name;
38
+ }
39
+ return undefined;
40
+ }
41
+ function collectUseMemoImports(program) {
42
+ const useMemoNames = new Set();
43
+ const useMemoSpecifiers = new Map();
44
+ const reactNamespaceNames = new Set();
45
+ for (const statement of program.body) {
46
+ if (statement.type !== utils_1.AST_NODE_TYPES.ImportDeclaration ||
47
+ statement.source.value !== 'react') {
48
+ continue;
49
+ }
50
+ for (const specifier of statement.specifiers) {
51
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
52
+ specifier.imported.type === utils_1.AST_NODE_TYPES.Identifier &&
53
+ specifier.imported.name === 'useMemo') {
54
+ useMemoNames.add(specifier.local.name);
55
+ useMemoSpecifiers.set(specifier.local.name, specifier);
56
+ }
57
+ if (specifier.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier ||
58
+ specifier.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
59
+ reactNamespaceNames.add(specifier.local.name);
60
+ }
61
+ }
62
+ }
63
+ return { useMemoNames, useMemoSpecifiers, reactNamespaceNames };
64
+ }
65
+ function getReturnedExpression(callback) {
66
+ if (callback.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
67
+ if (callback.body.body.length !== 1) {
68
+ return null;
69
+ }
70
+ const [onlyStatement] = callback.body.body;
71
+ if (onlyStatement.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
72
+ onlyStatement.argument) {
73
+ return onlyStatement.argument;
74
+ }
75
+ return null;
76
+ }
77
+ return callback.body;
78
+ }
79
+ function isPassByValueType(type, checker) {
80
+ const description = checker.typeToString(type);
81
+ if (type.flags & (typescript_1.default.TypeFlags.Any | typescript_1.default.TypeFlags.Unknown)) {
82
+ return { passByValue: false, indeterminate: true, description };
83
+ }
84
+ if (type.flags & typescript_1.default.TypeFlags.Never) {
85
+ return { passByValue: false, indeterminate: false, description };
86
+ }
87
+ if (type.flags & typescript_1.default.TypeFlags.Union) {
88
+ const unionType = type;
89
+ let sawIndeterminate = false;
90
+ for (const part of unionType.types) {
91
+ const result = isPassByValueType(part, checker);
92
+ if (result.indeterminate) {
93
+ sawIndeterminate = true;
94
+ break;
95
+ }
96
+ if (!result.passByValue) {
97
+ return { passByValue: false, indeterminate: false, description };
98
+ }
99
+ }
100
+ if (sawIndeterminate) {
101
+ return { passByValue: false, indeterminate: true, description };
102
+ }
103
+ return { passByValue: true, indeterminate: false, description };
104
+ }
105
+ if (checker.isTupleType(type)) {
106
+ const typeArguments = checker.getTypeArguments(type);
107
+ let allPrimitive = true;
108
+ let sawIndeterminate = false;
109
+ for (const elementType of typeArguments) {
110
+ const result = isPassByValueType(elementType, checker);
111
+ if (result.indeterminate) {
112
+ sawIndeterminate = true;
113
+ break;
114
+ }
115
+ if (!result.passByValue) {
116
+ allPrimitive = false;
117
+ break;
118
+ }
119
+ }
120
+ if (sawIndeterminate) {
121
+ return { passByValue: false, indeterminate: true, description };
122
+ }
123
+ return { passByValue: allPrimitive, indeterminate: false, description };
124
+ }
125
+ if (checker.isArrayType(type) || checker.isArrayLikeType(type)) {
126
+ const typeArguments = checker.getTypeArguments(type);
127
+ const elementType = typeArguments[0];
128
+ if (elementType) {
129
+ if (elementType.flags & typescript_1.default.TypeFlags.Never) {
130
+ // TypeScript infers [] as never[]. The array still has reference identity, but this rule
131
+ // treats it as a primitive-only array by definition (vacuously) to avoid special-casing.
132
+ return { passByValue: true, indeterminate: false, description };
133
+ }
134
+ const result = isPassByValueType(elementType, checker);
135
+ return { ...result, description };
136
+ }
137
+ // Conservative: arrays with unresolvable element types are indeterminate.
138
+ return { passByValue: false, indeterminate: true, description };
139
+ }
140
+ if (type.flags & PASS_BY_VALUE_FLAGS) {
141
+ return { passByValue: true, indeterminate: false, description };
142
+ }
143
+ return { passByValue: false, indeterminate: false, description };
144
+ }
145
+ function getCalleeName(node) {
146
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier) {
147
+ return node.callee.name;
148
+ }
149
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
150
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
151
+ const object = node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier
152
+ ? node.callee.object.name
153
+ : null;
154
+ if (!object) {
155
+ return node.callee.property.name;
156
+ }
157
+ return `${object}.${node.callee.property.name}`;
158
+ }
159
+ return null;
160
+ }
161
+ function matchesExpensiveCalleePattern(callback, expensiveMatchers) {
162
+ const returnedExpression = getReturnedExpression(callback);
163
+ if (!returnedExpression ||
164
+ returnedExpression.type !== utils_1.AST_NODE_TYPES.CallExpression) {
165
+ return false;
166
+ }
167
+ const calleeName = getCalleeName(returnedExpression);
168
+ if (!calleeName) {
169
+ return false;
170
+ }
171
+ return expensiveMatchers.some((matcher) => matcher.test(calleeName));
172
+ }
173
+ function isImportedIdentifier(identifier, resolveVariable) {
174
+ // Treat identifiers as React bindings only when they resolve to an import.
175
+ // This avoids unsafe auto-fixes when names like "React" or "useMemo" are shadowed
176
+ // by parameters/locals or represent non-React values.
177
+ const variable = resolveVariable(identifier);
178
+ return variable?.defs.some((def) => def.type === 'ImportBinding') ?? false;
179
+ }
180
+ function isUseMemoCall(node, imports, resolveVariable) {
181
+ if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
182
+ imports.useMemoNames.has(node.callee.name)) {
183
+ return isImportedIdentifier(node.callee, resolveVariable);
184
+ }
185
+ if (node.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
186
+ !node.callee.computed &&
187
+ node.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
188
+ node.callee.property.name === 'useMemo' &&
189
+ node.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
190
+ imports.reactNamespaceNames.has(node.callee.object.name) &&
191
+ isImportedIdentifier(node.callee.object, resolveVariable)) {
192
+ return true;
193
+ }
194
+ return false;
195
+ }
196
+ function scanBackwardOverWhitespace(text, position) {
197
+ let pos = position;
198
+ while (pos > 0 && (text[pos - 1] === ' ' || text[pos - 1] === '\t')) {
199
+ pos -= 1;
200
+ }
201
+ return pos;
202
+ }
203
+ function scanForwardOverWhitespace(text, position) {
204
+ let pos = position;
205
+ while (pos < text.length && (text[pos] === ' ' || text[pos] === '\t')) {
206
+ pos += 1;
207
+ }
208
+ return pos;
209
+ }
210
+ function consumeLineBreak(text, position) {
211
+ if (position < text.length &&
212
+ text[position] === '\r' &&
213
+ text[position + 1] === '\n') {
214
+ return { consumed: true, newPosition: position + 2 };
215
+ }
216
+ if (position < text.length &&
217
+ (text[position] === '\n' || text[position] === '\r')) {
218
+ return { consumed: true, newPosition: position + 1 };
219
+ }
220
+ return { consumed: false, newPosition: position };
221
+ }
222
+ function hasDeclaredVariables(source) {
223
+ return (typeof source === 'object' &&
224
+ source !== null &&
225
+ 'getDeclaredVariables' in source &&
226
+ typeof source
227
+ .getDeclaredVariables === 'function');
228
+ }
229
+ function removeCompleteImport(importDeclaration, sourceCode, fixer) {
230
+ const text = sourceCode.getText();
231
+ let [start, end] = importDeclaration.range;
232
+ const beforeImport = scanBackwardOverWhitespace(text, start);
233
+ const lineBreakIndex = beforeImport - 1;
234
+ if (lineBreakIndex >= 0 &&
235
+ (text[lineBreakIndex] === '\n' || text[lineBreakIndex] === '\r')) {
236
+ const lineBreakStart = text[lineBreakIndex] === '\n' &&
237
+ lineBreakIndex > 0 &&
238
+ text[lineBreakIndex - 1] === '\r'
239
+ ? lineBreakIndex - 1
240
+ : lineBreakIndex;
241
+ const whitespaceBeforeLineBreak = scanBackwardOverWhitespace(text, lineBreakStart);
242
+ const precedingCharIndex = whitespaceBeforeLineBreak - 1;
243
+ const lineIsWhitespaceOnly = whitespaceBeforeLineBreak === 0 ||
244
+ (precedingCharIndex >= 0 &&
245
+ (text[precedingCharIndex] === '\n' ||
246
+ text[precedingCharIndex] === '\r'));
247
+ start = lineIsWhitespaceOnly ? whitespaceBeforeLineBreak : beforeImport;
248
+ }
249
+ else {
250
+ start = beforeImport;
251
+ }
252
+ const afterImportWhitespace = scanForwardOverWhitespace(text, end);
253
+ const firstLineBreak = consumeLineBreak(text, afterImportWhitespace);
254
+ let removalEnd = afterImportWhitespace;
255
+ if (firstLineBreak.consumed) {
256
+ const afterFirstBreak = firstLineBreak.newPosition;
257
+ const spacesAfterFirst = scanForwardOverWhitespace(text, afterFirstBreak);
258
+ const secondLineBreak = consumeLineBreak(text, spacesAfterFirst);
259
+ if (secondLineBreak.consumed) {
260
+ // Remove a trailing blank line but stop before consuming indentation of the next statement.
261
+ removalEnd = secondLineBreak.newPosition;
262
+ }
263
+ else {
264
+ // Keep indentation that belongs to the following statement.
265
+ removalEnd = afterFirstBreak;
266
+ }
267
+ }
268
+ return fixer.removeRange([start, removalEnd]);
269
+ }
270
+ function removePartialImport(importDeclaration, remainingSpecifiers, sourceCode, fixer) {
271
+ const defaultSpecifier = remainingSpecifiers.find((candidate) => candidate.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
272
+ const namespaceSpecifier = remainingSpecifiers.find((candidate) => candidate.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
273
+ const namedSpecifiers = remainingSpecifiers.filter((candidate) => candidate.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
274
+ const pieces = [];
275
+ if (defaultSpecifier) {
276
+ pieces.push(sourceCode.getText(defaultSpecifier));
277
+ }
278
+ if (namespaceSpecifier) {
279
+ pieces.push(sourceCode.getText(namespaceSpecifier));
280
+ }
281
+ if (namedSpecifiers.length > 0) {
282
+ pieces.push(`{ ${namedSpecifiers
283
+ .map((candidate) => sourceCode.getText(candidate))
284
+ .join(', ')} }`);
285
+ }
286
+ const importPrefix = importDeclaration.importKind === 'type' ? 'import type ' : 'import ';
287
+ const newImport = `${importPrefix}${pieces.join(', ')} from ${sourceCode.getText(importDeclaration.source)};`;
288
+ return fixer.replaceText(importDeclaration, newImport);
289
+ }
290
+ function getImportRemovalFix(specifier, sourceCode, fixer) {
291
+ const importDeclaration = specifier.parent;
292
+ if (!importDeclaration ||
293
+ importDeclaration.type !== utils_1.AST_NODE_TYPES.ImportDeclaration) {
294
+ return null;
295
+ }
296
+ const remainingSpecifiers = importDeclaration.specifiers.filter((candidate) => candidate !== specifier);
297
+ if (remainingSpecifiers.length === 0) {
298
+ return removeCompleteImport(importDeclaration, sourceCode, fixer);
299
+ }
300
+ return removePartialImport(importDeclaration, remainingSpecifiers, sourceCode, fixer);
301
+ }
302
+ function getReplacementText(callback, sourceCode) {
303
+ const returnedExpression = getReturnedExpression(callback);
304
+ if (!returnedExpression) {
305
+ return null;
306
+ }
307
+ if (callback.body.type === utils_1.AST_NODE_TYPES.BlockStatement &&
308
+ callback.body.body.length !== 1) {
309
+ return null;
310
+ }
311
+ return sourceCode.getText(returnedExpression);
312
+ }
313
+ function isSafeAtomicExpression(expression) {
314
+ switch (expression.type) {
315
+ case utils_1.AST_NODE_TYPES.Identifier:
316
+ case utils_1.AST_NODE_TYPES.Literal:
317
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
318
+ case utils_1.AST_NODE_TYPES.ThisExpression:
319
+ case utils_1.AST_NODE_TYPES.Super:
320
+ case utils_1.AST_NODE_TYPES.MemberExpression:
321
+ case utils_1.AST_NODE_TYPES.CallExpression:
322
+ case utils_1.AST_NODE_TYPES.NewExpression:
323
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
324
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
325
+ case utils_1.AST_NODE_TYPES.ArrowFunctionExpression:
326
+ case utils_1.AST_NODE_TYPES.FunctionExpression:
327
+ case utils_1.AST_NODE_TYPES.ClassExpression:
328
+ case utils_1.AST_NODE_TYPES.TaggedTemplateExpression:
329
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
330
+ case utils_1.AST_NODE_TYPES.UpdateExpression:
331
+ case utils_1.AST_NODE_TYPES.AwaitExpression:
332
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
333
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
334
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
335
+ return true;
336
+ default:
337
+ return false;
338
+ }
339
+ }
340
+ function shouldParenthesizeReplacement(node, replacementExpression, sourceCode) {
341
+ const alreadyParenthesized = utils_1.ASTUtils.isParenthesized(replacementExpression, sourceCode);
342
+ const parent = node.parent;
343
+ if (!parent) {
344
+ return false;
345
+ }
346
+ if (replacementExpression.type ===
347
+ utils_1.AST_NODE_TYPES.SequenceExpression) {
348
+ return true;
349
+ }
350
+ switch (parent.type) {
351
+ case utils_1.AST_NODE_TYPES.LogicalExpression:
352
+ case utils_1.AST_NODE_TYPES.BinaryExpression:
353
+ return (alreadyParenthesized || !isSafeAtomicExpression(replacementExpression));
354
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
355
+ case utils_1.AST_NODE_TYPES.AwaitExpression:
356
+ case utils_1.AST_NODE_TYPES.MemberExpression:
357
+ case utils_1.AST_NODE_TYPES.TaggedTemplateExpression:
358
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
359
+ case utils_1.AST_NODE_TYPES.ChainExpression:
360
+ return true;
361
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
362
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
363
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
364
+ return (alreadyParenthesized || !isSafeAtomicExpression(replacementExpression));
365
+ case utils_1.AST_NODE_TYPES.CallExpression:
366
+ case utils_1.AST_NODE_TYPES.NewExpression:
367
+ return (alreadyParenthesized ||
368
+ parent.callee === node ||
369
+ replacementExpression.type === utils_1.AST_NODE_TYPES.SequenceExpression);
370
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
371
+ if (parent.test !== node) {
372
+ return false;
373
+ }
374
+ return (alreadyParenthesized || !isSafeAtomicExpression(replacementExpression));
375
+ case utils_1.AST_NODE_TYPES.AssignmentExpression:
376
+ return false;
377
+ default:
378
+ return false;
379
+ }
380
+ }
381
+ function getRemovableImportSpecifier(callExpression, imports, sourceCode) {
382
+ if (callExpression.callee.type !== utils_1.AST_NODE_TYPES.Identifier) {
383
+ return null;
384
+ }
385
+ const specifier = imports.useMemoSpecifiers.get(callExpression.callee.name);
386
+ if (!specifier) {
387
+ return null;
388
+ }
389
+ const declaredVariables = hasDeclaredVariables(sourceCode)
390
+ ? sourceCode.getDeclaredVariables(specifier)
391
+ : sourceCode.getDeclaredVariables?.(specifier) ?? [];
392
+ const [variable] = declaredVariables;
393
+ if (!variable) {
394
+ // Defer removal when scope analysis cannot resolve the import.
395
+ return null;
396
+ }
397
+ const otherReferences = variable.references.filter((reference) => reference.identifier !== callExpression.callee);
398
+ if (otherReferences.length > 0) {
399
+ return null;
400
+ }
401
+ return specifier;
402
+ }
403
+ exports.noUsememoForPassByValue = (0, createRule_1.createRule)({
404
+ name: 'no-usememo-for-pass-by-value',
405
+ meta: {
406
+ type: 'suggestion',
407
+ docs: {
408
+ description: 'Disallow returning useMemo results from custom hooks when the memoized value is pass-by-value: primitives with value equality (string, number, boolean, null, undefined, bigint) or arrays/tuples composed exclusively of these primitives. Requires type information.',
409
+ recommended: 'error',
410
+ requiresTypeChecking: true,
411
+ },
412
+ fixable: 'code',
413
+ schema: [
414
+ {
415
+ type: 'object',
416
+ properties: {
417
+ allowExpensiveCalleePatterns: {
418
+ type: 'array',
419
+ items: { type: 'string' },
420
+ default: DEFAULT_EXPENSIVE_PATTERNS,
421
+ },
422
+ },
423
+ additionalProperties: false,
424
+ },
425
+ ],
426
+ messages: {
427
+ primitiveMemo: 'What’s wrong: custom hook "{{hookName}}" returns useMemo wrapping a pass-by-value value ({{valueType}}) → Why it matters: memoizing pass-by-value results cannot change identity and implies stability that is not real, which misleads callers and adds noise → How to fix: inline the returned expression and remove the useMemo import if it becomes unused.',
428
+ invalidRegex: 'What’s wrong: invalid regex pattern "{{pattern}}" in allowExpensiveCalleePatterns → Why it matters: invalid patterns prevent the rule from correctly identifying expensive computations, potentially causing false positives → How to fix: correct the regex pattern in your ESLint configuration.',
429
+ },
430
+ },
431
+ defaultOptions: [{}],
432
+ create(context) {
433
+ const sourceCode = context.getSourceCode();
434
+ const parserServices = sourceCode.parserServices ?? context.parserServices;
435
+ if (!parserServices?.program || !parserServices.esTreeNodeToTSNodeMap) {
436
+ return {};
437
+ }
438
+ const { program, esTreeNodeToTSNodeMap } = parserServices;
439
+ const checker = program.getTypeChecker();
440
+ const imports = collectUseMemoImports(sourceCode.ast);
441
+ const expensiveMatchers = [];
442
+ const invalidPatterns = [];
443
+ (context.options[0]?.allowExpensiveCalleePatterns ??
444
+ DEFAULT_EXPENSIVE_PATTERNS).forEach((pattern) => {
445
+ try {
446
+ expensiveMatchers.push(new RegExp(pattern));
447
+ }
448
+ catch {
449
+ invalidPatterns.push(pattern);
450
+ }
451
+ });
452
+ const functionStack = [];
453
+ const reported = new WeakSet();
454
+ const resolveVariable = (identifier) => utils_1.ASTUtils.findVariable(context.getScope(), identifier) ?? null;
455
+ function visitPatternNode(pattern, visitIdentifier) {
456
+ if (pattern.type === utils_1.AST_NODE_TYPES.Identifier) {
457
+ visitIdentifier(pattern);
458
+ return;
459
+ }
460
+ if (pattern.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
461
+ for (const element of pattern.elements) {
462
+ if (!element) {
463
+ continue;
464
+ }
465
+ visitPatternNode(element.type === utils_1.AST_NODE_TYPES.RestElement
466
+ ? element.argument
467
+ : element, visitIdentifier);
468
+ }
469
+ return;
470
+ }
471
+ if (pattern.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
472
+ for (const property of pattern.properties) {
473
+ if (property.type === utils_1.AST_NODE_TYPES.Property) {
474
+ visitPatternNode(property.value, visitIdentifier);
475
+ continue;
476
+ }
477
+ if (property.type === utils_1.AST_NODE_TYPES.RestElement) {
478
+ visitPatternNode(property.argument, visitIdentifier);
479
+ }
480
+ }
481
+ return;
482
+ }
483
+ if (pattern.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
484
+ visitPatternNode(pattern.left, visitIdentifier);
485
+ }
486
+ }
487
+ function traversePattern(pattern, resolveVariable, visitVariable) {
488
+ visitPatternNode(pattern, (identifier) => {
489
+ const variable = resolveVariable(identifier);
490
+ if (variable) {
491
+ visitVariable(variable);
492
+ }
493
+ });
494
+ }
495
+ function trackPatternVariables(pattern, memoCall, currentContext, resolveVariable) {
496
+ traversePattern(pattern, resolveVariable, (variable) => {
497
+ currentContext.memoVariables.set(variable, memoCall);
498
+ });
499
+ }
500
+ function untrackPatternVariables(pattern, currentContext, resolveVariable) {
501
+ traversePattern(pattern, resolveVariable, (variable) => {
502
+ currentContext.memoVariables.delete(variable);
503
+ });
504
+ }
505
+ function validateUseMemoArgument(node) {
506
+ const callback = node.arguments[0];
507
+ if (!callback ||
508
+ (callback.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
509
+ callback.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
510
+ return null;
511
+ }
512
+ const returnedExpression = getReturnedExpression(callback);
513
+ if (!returnedExpression) {
514
+ return null;
515
+ }
516
+ return { callback, returnedExpression };
517
+ }
518
+ function classifyUseMemoReturnType(node, returnedExpression) {
519
+ const tsNode = esTreeNodeToTSNodeMap.get(node);
520
+ if (!tsNode) {
521
+ return null;
522
+ }
523
+ const type = checker.getTypeAtLocation(tsNode);
524
+ let classification = isPassByValueType(type, checker);
525
+ if (classification.indeterminate) {
526
+ // When useMemo's type is indeterminate (any/unknown), the callback can still return a
527
+ // concrete pass-by-value type. Inspect the returned expression to reduce false negatives.
528
+ const returnedTsNode = esTreeNodeToTSNodeMap.get(returnedExpression);
529
+ if (returnedTsNode) {
530
+ const returnedType = checker.getTypeAtLocation(returnedTsNode);
531
+ const fallbackClassification = isPassByValueType(returnedType, checker);
532
+ if (!fallbackClassification.indeterminate) {
533
+ classification = fallbackClassification;
534
+ }
535
+ }
536
+ }
537
+ return classification;
538
+ }
539
+ function getUseMemoFix(node, callback, returnedExpression, fixer) {
540
+ const replacementText = getReplacementText(callback, sourceCode);
541
+ if (!replacementText) {
542
+ return null;
543
+ }
544
+ const needsParentheses = shouldParenthesizeReplacement(node, returnedExpression, sourceCode);
545
+ const replacement = needsParentheses
546
+ ? `(${replacementText})`
547
+ : replacementText;
548
+ const specifier = getRemovableImportSpecifier(node, imports, sourceCode);
549
+ const fixes = [fixer.replaceText(node, replacement)];
550
+ if (specifier) {
551
+ const removal = getImportRemovalFix(specifier, sourceCode, fixer);
552
+ if (removal) {
553
+ fixes.push(removal);
554
+ }
555
+ }
556
+ return fixes;
557
+ }
558
+ function checkUseMemoForPassByValue(node, currentContext) {
559
+ if (!currentContext?.isHook || reported.has(node)) {
560
+ return;
561
+ }
562
+ const validated = validateUseMemoArgument(node);
563
+ if (!validated) {
564
+ return;
565
+ }
566
+ const { callback, returnedExpression } = validated;
567
+ if (matchesExpensiveCalleePattern(callback, expensiveMatchers)) {
568
+ return;
569
+ }
570
+ const classification = classifyUseMemoReturnType(node, returnedExpression);
571
+ if (!classification ||
572
+ classification.indeterminate ||
573
+ !classification.passByValue) {
574
+ return;
575
+ }
576
+ reported.add(node);
577
+ context.report({
578
+ node,
579
+ messageId: 'primitiveMemo',
580
+ data: {
581
+ hookName: currentContext.hookName ?? 'this hook',
582
+ valueType: classification.description,
583
+ },
584
+ fix: (fixer) => getUseMemoFix(node, callback, returnedExpression, fixer),
585
+ });
586
+ }
587
+ function analyzeExpressionList(expressions, currentContext) {
588
+ for (const expr of expressions) {
589
+ if (!expr) {
590
+ continue;
591
+ }
592
+ if (expr.type === utils_1.AST_NODE_TYPES.SpreadElement) {
593
+ analyzeReturnedValue(expr.argument, currentContext);
594
+ continue;
595
+ }
596
+ analyzeReturnedValue(expr, currentContext);
597
+ }
598
+ }
599
+ function analyzeReturnedValue(expression, currentContext) {
600
+ if (!expression || !currentContext?.isHook) {
601
+ return;
602
+ }
603
+ switch (expression.type) {
604
+ case utils_1.AST_NODE_TYPES.CallExpression:
605
+ if (isUseMemoCall(expression, imports, resolveVariable)) {
606
+ checkUseMemoForPassByValue(expression, currentContext);
607
+ return;
608
+ }
609
+ analyzeExpressionList(expression.arguments, currentContext);
610
+ return;
611
+ case utils_1.AST_NODE_TYPES.NewExpression:
612
+ analyzeExpressionList(expression.arguments ?? [], currentContext);
613
+ return;
614
+ case utils_1.AST_NODE_TYPES.Identifier: {
615
+ const variable = resolveVariable(expression);
616
+ const memoCall = variable
617
+ ? currentContext.memoVariables.get(variable)
618
+ : undefined;
619
+ if (memoCall) {
620
+ checkUseMemoForPassByValue(memoCall, currentContext);
621
+ }
622
+ return;
623
+ }
624
+ case utils_1.AST_NODE_TYPES.ConditionalExpression:
625
+ analyzeReturnedValue(expression.test, currentContext);
626
+ analyzeReturnedValue(expression.consequent, currentContext);
627
+ analyzeReturnedValue(expression.alternate, currentContext);
628
+ return;
629
+ case utils_1.AST_NODE_TYPES.LogicalExpression:
630
+ analyzeReturnedValue(expression.left, currentContext);
631
+ analyzeReturnedValue(expression.right, currentContext);
632
+ return;
633
+ case utils_1.AST_NODE_TYPES.BinaryExpression:
634
+ if (expression.left.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) {
635
+ analyzeReturnedValue(expression.left, currentContext);
636
+ }
637
+ analyzeReturnedValue(expression.right, currentContext);
638
+ return;
639
+ case utils_1.AST_NODE_TYPES.UnaryExpression:
640
+ analyzeReturnedValue(expression.argument, currentContext);
641
+ return;
642
+ case utils_1.AST_NODE_TYPES.SequenceExpression: {
643
+ const lastExpression = expression.expressions[expression.expressions.length - 1];
644
+ analyzeReturnedValue(lastExpression, currentContext);
645
+ return;
646
+ }
647
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
648
+ analyzeExpressionList(expression.elements, currentContext);
649
+ return;
650
+ case utils_1.AST_NODE_TYPES.ObjectExpression: {
651
+ const propertyExpressions = expression.properties.map((property) => {
652
+ if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
653
+ return property.argument;
654
+ }
655
+ if (property.type === utils_1.AST_NODE_TYPES.Property && property.value) {
656
+ return property.value;
657
+ }
658
+ return null;
659
+ });
660
+ analyzeExpressionList(propertyExpressions, currentContext);
661
+ return;
662
+ }
663
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
664
+ case utils_1.AST_NODE_TYPES.TSTypeAssertion:
665
+ case utils_1.AST_NODE_TYPES.TSNonNullExpression:
666
+ case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
667
+ case utils_1.AST_NODE_TYPES.ChainExpression:
668
+ analyzeReturnedValue(expression.expression, currentContext);
669
+ return;
670
+ case utils_1.AST_NODE_TYPES.AwaitExpression:
671
+ analyzeReturnedValue(expression.argument, currentContext);
672
+ return;
673
+ }
674
+ }
675
+ return {
676
+ Program() {
677
+ invalidPatterns.forEach((pattern) => {
678
+ context.report({
679
+ node: sourceCode.ast,
680
+ messageId: 'invalidRegex',
681
+ data: { pattern },
682
+ });
683
+ });
684
+ },
685
+ FunctionDeclaration(node) {
686
+ const name = getFunctionName(node);
687
+ functionStack.push({
688
+ isHook: isCustomHookName(name),
689
+ hookName: name,
690
+ memoVariables: new WeakMap(),
691
+ });
692
+ },
693
+ 'FunctionDeclaration:exit'() {
694
+ functionStack.pop();
695
+ },
696
+ FunctionExpression(node) {
697
+ const name = getFunctionName(node);
698
+ functionStack.push({
699
+ isHook: isCustomHookName(name),
700
+ hookName: name,
701
+ memoVariables: new WeakMap(),
702
+ });
703
+ },
704
+ 'FunctionExpression:exit'() {
705
+ functionStack.pop();
706
+ },
707
+ ArrowFunctionExpression(node) {
708
+ const name = getFunctionName(node);
709
+ functionStack.push({
710
+ isHook: isCustomHookName(name),
711
+ hookName: name,
712
+ memoVariables: new WeakMap(),
713
+ });
714
+ },
715
+ 'ArrowFunctionExpression:exit'(node) {
716
+ const currentContext = functionStack[functionStack.length - 1];
717
+ if (currentContext?.isHook && node.expression) {
718
+ analyzeReturnedValue(node.body, currentContext);
719
+ }
720
+ functionStack.pop();
721
+ },
722
+ VariableDeclarator(node) {
723
+ const currentContext = functionStack[functionStack.length - 1];
724
+ if (!currentContext?.isHook ||
725
+ !node.init ||
726
+ node.init.type !== utils_1.AST_NODE_TYPES.CallExpression ||
727
+ !isUseMemoCall(node.init, imports, resolveVariable)) {
728
+ return;
729
+ }
730
+ trackPatternVariables(node.id, node.init, currentContext, resolveVariable);
731
+ },
732
+ AssignmentExpression(node) {
733
+ const currentContext = functionStack[functionStack.length - 1];
734
+ if (!currentContext?.isHook) {
735
+ return;
736
+ }
737
+ if (node.right.type === utils_1.AST_NODE_TYPES.CallExpression &&
738
+ isUseMemoCall(node.right, imports, resolveVariable)) {
739
+ trackPatternVariables(node.left, node.right, currentContext, resolveVariable);
740
+ return;
741
+ }
742
+ if (node.operator === '=') {
743
+ untrackPatternVariables(node.left, currentContext, resolveVariable);
744
+ }
745
+ },
746
+ ReturnStatement(node) {
747
+ const currentContext = functionStack[functionStack.length - 1];
748
+ if (!currentContext?.isHook) {
749
+ return;
750
+ }
751
+ analyzeReturnedValue(node.argument, currentContext);
752
+ },
753
+ };
754
+ },
755
+ });
756
+ exports.default = exports.noUsememoForPassByValue;
757
+ //# sourceMappingURL=no-usememo-for-pass-by-value.js.map