@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
@@ -13,6 +13,190 @@ const HANDLER_TYPES = new Set([
13
13
  'RealtimeDbChangeHandler',
14
14
  'RealtimeDbChangeHandlerTransaction',
15
15
  ]);
16
+ const getQualifiedNameIdentifier = (typeName) => {
17
+ if (typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
18
+ return typeName.name;
19
+ }
20
+ if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
21
+ typeName.right.type === utils_1.AST_NODE_TYPES.Identifier) {
22
+ return typeName.right.name;
23
+ }
24
+ return null;
25
+ };
26
+ const checkTypeAnnotationForHandler = (typeNode) => {
27
+ switch (typeNode.type) {
28
+ case utils_1.AST_NODE_TYPES.TSTypeReference: {
29
+ const typeIdentifier = getQualifiedNameIdentifier(typeNode.typeName);
30
+ return typeIdentifier ? HANDLER_TYPES.has(typeIdentifier) : false;
31
+ }
32
+ case utils_1.AST_NODE_TYPES.TSUnionType:
33
+ return typeNode.types.some(checkTypeAnnotationForHandler);
34
+ case utils_1.AST_NODE_TYPES.TSIntersectionType:
35
+ return typeNode.types.some(checkTypeAnnotationForHandler);
36
+ default:
37
+ return false;
38
+ }
39
+ };
40
+ const findTypeAnnotationInContext = (node) => {
41
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
42
+ node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
43
+ node.parent.id.typeAnnotation) {
44
+ return node.parent.id.typeAnnotation;
45
+ }
46
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
47
+ node.parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
48
+ node.parent.left.typeAnnotation) {
49
+ return node.parent.left.typeAnnotation;
50
+ }
51
+ if (node.parent?.type === utils_1.AST_NODE_TYPES.Property &&
52
+ node.parent.value === node) {
53
+ let current = node.parent.parent;
54
+ while (current) {
55
+ if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
56
+ current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
57
+ current.id.typeAnnotation) {
58
+ return current.id.typeAnnotation;
59
+ }
60
+ current = current.parent;
61
+ }
62
+ }
63
+ return undefined;
64
+ };
65
+ const isFirebaseChangeHandler = (node) => {
66
+ if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
67
+ node.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
68
+ node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
69
+ return false;
70
+ }
71
+ const typeAnnotation = findTypeAnnotationInContext(node);
72
+ if (!typeAnnotation) {
73
+ return false;
74
+ }
75
+ return checkTypeAnnotationForHandler(typeAnnotation.typeAnnotation);
76
+ };
77
+ const isParentIdAccess = (node) => {
78
+ if (node.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
79
+ node.property.name !== 'id') {
80
+ return { isMatch: false, depth: 0 };
81
+ }
82
+ const chain = [];
83
+ let current = node.object;
84
+ while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
85
+ if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
86
+ return { isMatch: false, depth: 0 };
87
+ }
88
+ chain.unshift(current.property.name);
89
+ current = current.object;
90
+ }
91
+ if (chain.length < 2) {
92
+ return { isMatch: false, depth: 0 };
93
+ }
94
+ const refIndex = chain.lastIndexOf('ref');
95
+ if (refIndex === -1) {
96
+ return { isMatch: false, depth: 0 };
97
+ }
98
+ const parentSegment = chain.slice(refIndex + 1);
99
+ if (parentSegment.length === 0) {
100
+ return { isMatch: false, depth: 0 };
101
+ }
102
+ const invalidParent = parentSegment.some((segment) => segment !== 'parent');
103
+ if (invalidParent) {
104
+ return { isMatch: false, depth: 0 };
105
+ }
106
+ const depth = parentSegment.length;
107
+ return { isMatch: depth > 0, depth };
108
+ };
109
+ const getParentParamName = (depth) => {
110
+ if (depth === 1) {
111
+ return 'userId';
112
+ }
113
+ if (depth === 2) {
114
+ return 'parentId';
115
+ }
116
+ return `parent${depth}Id`;
117
+ };
118
+ const findHandlerFunction = (node, handlerNodes) => {
119
+ let current = node;
120
+ while (current) {
121
+ if (handlerNodes.has(current)) {
122
+ return current;
123
+ }
124
+ current = current.parent;
125
+ }
126
+ return null;
127
+ };
128
+ const hasOptionalChaining = (node) => {
129
+ let current = node;
130
+ while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
131
+ if (current.optional) {
132
+ return true;
133
+ }
134
+ current = current.object;
135
+ }
136
+ return false;
137
+ };
138
+ const findParamsInPattern = (pattern) => {
139
+ for (const prop of pattern.properties) {
140
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
141
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
142
+ prop.key.name === 'params') {
143
+ if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
144
+ return { identifier: prop.value.name };
145
+ }
146
+ if (prop.value.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
147
+ const properties = new Map();
148
+ for (const p of prop.value.properties) {
149
+ if (p.type === utils_1.AST_NODE_TYPES.Property &&
150
+ p.key.type === utils_1.AST_NODE_TYPES.Identifier &&
151
+ p.value.type === utils_1.AST_NODE_TYPES.Identifier) {
152
+ properties.set(p.key.name, p.value.name);
153
+ }
154
+ }
155
+ return { properties };
156
+ }
157
+ }
158
+ }
159
+ return null;
160
+ };
161
+ const getParamsInScope = (handlerNode) => {
162
+ if (handlerNode.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
163
+ handlerNode.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
164
+ handlerNode.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
165
+ return null;
166
+ }
167
+ const firstParam = handlerNode.params[0];
168
+ if (!firstParam) {
169
+ return null;
170
+ }
171
+ if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
172
+ const paramsInScope = findParamsInPattern(firstParam);
173
+ if (paramsInScope) {
174
+ return paramsInScope;
175
+ }
176
+ }
177
+ if (handlerNode.body &&
178
+ handlerNode.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
179
+ const eventParamName = firstParam.type === utils_1.AST_NODE_TYPES.Identifier ? firstParam.name : 'event';
180
+ for (const statement of handlerNode.body.body) {
181
+ if (statement.type !== utils_1.AST_NODE_TYPES.VariableDeclaration) {
182
+ continue;
183
+ }
184
+ for (const declarator of statement.declarations) {
185
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.ObjectPattern ||
186
+ !declarator.init ||
187
+ declarator.init.type !== utils_1.AST_NODE_TYPES.Identifier ||
188
+ declarator.init.name !== eventParamName) {
189
+ continue;
190
+ }
191
+ const paramsInScope = findParamsInPattern(declarator.id);
192
+ if (paramsInScope) {
193
+ return paramsInScope;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ return null;
199
+ };
16
200
  exports.preferParamsOverParentId = (0, createRule_1.createRule)({
17
201
  name: 'prefer-params-over-parent-id',
18
202
  meta: {
@@ -24,199 +208,64 @@ exports.preferParamsOverParentId = (0, createRule_1.createRule)({
24
208
  fixable: 'code',
25
209
  schema: [],
26
210
  messages: {
27
- preferParams: 'Accessing parent IDs through `ref.parent.id` bypasses the handler params and breaks when collection nesting changes. Use the params object for stable, typed IDs instead (destructure `const { params: { {{paramName}} } } = event` or read `params.{{paramName}}`).',
211
+ preferParams: [
212
+ "What's wrong: This code reads an ID via `ref.parent...id` instead of using the trigger's params.",
213
+ '',
214
+ 'Why it matters: Walking `ref.parent` ties the handler to the current path depth; when collections change, it can yield the wrong ID (or a collection name) and bypasses the typed params the trigger provides.',
215
+ '',
216
+ 'How to fix: Read the ID from `params.{{paramName}}` (or destructure `const { params } = event` and then access `params.{{paramName}}`).',
217
+ ].join('\n'),
28
218
  },
29
219
  },
30
220
  defaultOptions: [],
31
221
  create(context) {
32
- // Track functions that are Firebase change handlers
33
- const handlerFunctions = new Set();
34
- function findTypeAnnotationInContext(node) {
35
- // Check variable declarator type annotation
36
- if (node.parent?.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
37
- node.parent.id.type === utils_1.AST_NODE_TYPES.Identifier &&
38
- node.parent.id.typeAnnotation) {
39
- return node.parent.id.typeAnnotation;
40
- }
41
- // Check assignment expression with type annotation
42
- if (node.parent?.type === utils_1.AST_NODE_TYPES.AssignmentExpression &&
43
- node.parent.left.type === utils_1.AST_NODE_TYPES.Identifier &&
44
- node.parent.left.typeAnnotation) {
45
- return node.parent.left.typeAnnotation;
46
- }
47
- // Check property definition type annotation
48
- if (node.parent?.type === utils_1.AST_NODE_TYPES.Property &&
49
- node.parent.value === node) {
50
- // Look up the tree for type annotation
51
- let current = node.parent.parent;
52
- while (current) {
53
- if (current.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
54
- current.id.type === utils_1.AST_NODE_TYPES.Identifier &&
55
- current.id.typeAnnotation) {
56
- return current.id.typeAnnotation;
57
- }
58
- current = current.parent;
59
- }
60
- }
61
- return undefined;
62
- }
63
- function isFirebaseChangeHandler(node) {
64
- if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
65
- node.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
66
- node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
67
- return false;
68
- }
69
- const typeAnnotation = findTypeAnnotationInContext(node);
70
- if (!typeAnnotation) {
71
- return false;
72
- }
73
- return checkTypeAnnotationForHandler(typeAnnotation.typeAnnotation);
74
- }
75
- function checkTypeAnnotationForHandler(typeNode) {
76
- if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
77
- typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
78
- return HANDLER_TYPES.has(typeNode.typeName.name);
79
- }
80
- return false;
81
- }
82
- function isParentIdAccess(node) {
83
- // Check if this is a .ref.parent[.parent...].id pattern
84
- if (node.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
85
- node.property.name !== 'id') {
86
- return { isMatch: false, depth: 0 };
87
- }
88
- const chain = [];
89
- let current = node.object;
90
- while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
91
- if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
92
- return { isMatch: false, depth: 0 };
93
- }
94
- chain.unshift(current.property.name);
95
- current = current.object;
96
- }
97
- if (chain.length < 2) {
98
- return { isMatch: false, depth: 0 };
99
- }
100
- const refIndex = chain.indexOf('ref');
101
- if (refIndex === -1) {
102
- return { isMatch: false, depth: 0 };
103
- }
104
- const parentSegment = chain.slice(refIndex + 1);
105
- if (parentSegment.length === 0) {
106
- return { isMatch: false, depth: 0 };
107
- }
108
- const invalidParent = parentSegment.some((segment) => segment !== 'parent');
109
- if (invalidParent) {
110
- return { isMatch: false, depth: 0 };
111
- }
112
- const depth = parentSegment.length;
113
- return { isMatch: depth > 0, depth };
114
- }
115
- function findHandlerFunction(node) {
116
- let current = node;
117
- while (current) {
118
- if (handlerFunctions.has(current)) {
119
- return current;
120
- }
121
- current = current.parent;
122
- }
123
- return null;
124
- }
125
- function hasOptionalChaining(node) {
126
- let current = node;
127
- while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
128
- if (current.optional) {
129
- return true;
130
- }
131
- current = current.object;
132
- }
133
- return false;
134
- }
135
- function isParamsInScope(handlerNode) {
136
- // Check if params is destructured from the event parameter
137
- if (handlerNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
138
- handlerNode.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
139
- handlerNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
140
- const firstParam = handlerNode.params[0];
141
- if (!firstParam)
142
- return false;
143
- // Check for destructuring pattern: ({ params }) or ({ data, params })
144
- if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
145
- return firstParam.properties.some((prop) => {
146
- if (prop.type === utils_1.AST_NODE_TYPES.Property &&
147
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
148
- prop.key.name === 'params') {
149
- return true;
150
- }
151
- return false;
152
- });
153
- }
154
- // Check for variable declarations inside the function that destructure params
155
- if (handlerNode.body &&
156
- handlerNode.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
157
- for (const statement of handlerNode.body.body) {
158
- if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
159
- for (const declarator of statement.declarations) {
160
- if (declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
161
- declarator.init &&
162
- declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
163
- // Check if destructuring from event parameter
164
- const eventParamName = firstParam.type === utils_1.AST_NODE_TYPES.Identifier
165
- ? firstParam.name
166
- : 'event';
167
- if (declarator.init.name === eventParamName) {
168
- return declarator.id.properties.some((prop) => {
169
- if (prop.type === utils_1.AST_NODE_TYPES.Property &&
170
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
171
- prop.key.name === 'params') {
172
- return true;
173
- }
174
- return false;
175
- });
176
- }
177
- }
178
- }
179
- }
180
- }
181
- }
182
- }
183
- return false;
184
- }
222
+ const handlerNodes = new Set();
185
223
  return {
186
224
  // Track Firebase change handler functions
187
225
  'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
188
226
  if (isFirebaseChangeHandler(node)) {
189
- handlerFunctions.add(node);
227
+ handlerNodes.add(node);
190
228
  }
191
229
  },
192
230
  // Detect .ref.parent.id patterns
193
231
  MemberExpression(node) {
194
232
  const parentAccess = isParentIdAccess(node);
195
233
  if (parentAccess.isMatch) {
196
- const handlerNode = findHandlerFunction(node);
234
+ const handlerNode = findHandlerFunction(node, handlerNodes);
197
235
  if (handlerNode) {
198
236
  const hasOptional = hasOptionalChaining(node);
199
- const paramsInScope = isParamsInScope(handlerNode);
200
- // Suggest different parameter names based on depth
201
- // Note: These conventions may vary by data model; adjust if needed
202
- const paramSuggestion = parentAccess.depth === 1
203
- ? 'userId'
204
- : parentAccess.depth === 2
205
- ? 'parentId'
206
- : `parent${parentAccess.depth}Id`;
237
+ const paramsInScope = getParamsInScope(handlerNode);
238
+ if (!paramsInScope) {
239
+ context.report({
240
+ node,
241
+ messageId: 'preferParams',
242
+ data: {
243
+ paramName: getParentParamName(parentAccess.depth),
244
+ },
245
+ });
246
+ return;
247
+ }
248
+ const paramSuggestion = getParentParamName(parentAccess.depth);
249
+ let replacement = null;
250
+ if (paramsInScope.identifier) {
251
+ replacement = hasOptional
252
+ ? `${paramsInScope.identifier}?.${paramSuggestion}`
253
+ : `${paramsInScope.identifier}.${paramSuggestion}`;
254
+ }
255
+ else if (paramsInScope.properties) {
256
+ const localName = paramsInScope.properties.get(paramSuggestion);
257
+ if (localName) {
258
+ replacement = localName;
259
+ }
260
+ }
207
261
  context.report({
208
262
  node,
209
263
  messageId: 'preferParams',
210
264
  data: {
211
265
  paramName: paramSuggestion,
212
266
  },
213
- fix: paramsInScope
214
- ? (fixer) => {
215
- const replacement = hasOptional
216
- ? `params?.${paramSuggestion}`
217
- : `params.${paramSuggestion}`;
218
- return fixer.replaceText(node, replacement);
219
- }
267
+ fix: replacement
268
+ ? (fixer) => fixer.replaceText(node, replacement)
220
269
  : undefined,
221
270
  });
222
271
  }
@@ -62,7 +62,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
62
62
  // If there are type parameters, include them in the type signature
63
63
  if (typeNode.typeParameters &&
64
64
  typeNode.typeParameters.params.length > 0) {
65
- const sourceCode = context.getSourceCode();
65
+ const sourceCode = context.sourceCode;
66
66
  const typeParams = typeNode.typeParameters.params
67
67
  .map((param) => sourceCode.getText(param))
68
68
  .join(', ');
@@ -82,7 +82,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
82
82
  // If there are type parameters, include them in the type signature
83
83
  if (typeNode.typeParameters &&
84
84
  typeNode.typeParameters.params.length > 0) {
85
- const sourceCode = context.getSourceCode();
85
+ const sourceCode = context.sourceCode;
86
86
  const typeParams = typeNode.typeParameters.params
87
87
  .map((param) => sourceCode.getText(param))
88
88
  .join(', ');
@@ -12,7 +12,9 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
12
12
  },
13
13
  schema: [],
14
14
  messages: {
15
- preferType: 'Prefer using type alias over interface.',
15
+ preferType: 'Interface "{{interfaceName}}" should be declared as a type alias. ' +
16
+ 'Interfaces can merge across declarations and extend in chains, which fragments the resulting shape across files and makes composition harder to predict and trace. ' +
17
+ 'Replace `interface` with `type` and use intersections (for example, `type {{interfaceName}} = Base & { field: string }`) to keep the contract closed and predictable.',
16
18
  },
17
19
  fixable: 'code',
18
20
  },
@@ -23,8 +25,11 @@ exports.preferTypeOverInterface = (0, createRule_1.createRule)({
23
25
  context.report({
24
26
  node,
25
27
  messageId: 'preferType',
28
+ data: {
29
+ interfaceName: node.id.name,
30
+ },
26
31
  fix(fixer) {
27
- const sourceCode = context.getSourceCode();
32
+ const sourceCode = context.sourceCode;
28
33
  const openingBrace = sourceCode.getTokenAfter(node.id, {
29
34
  filter: (token) => token.value === '{',
30
35
  });
@@ -50,7 +50,7 @@ function isOptionalMemberExpression(expr) {
50
50
  }
51
51
  exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
52
52
  create(context) {
53
- const sourceCode = context.getSourceCode();
53
+ const sourceCode = context.sourceCode;
54
54
  const parserServices = sourceCode.parserServices;
55
55
  const urlIdentifierNames = new Set();
56
56
  let checker = null;
@@ -132,6 +132,9 @@ exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
132
132
  context.report({
133
133
  node,
134
134
  messageId: 'preferToString',
135
+ data: {
136
+ urlText: sourceCode.getText(objectExpr),
137
+ },
135
138
  fix(fixer) {
136
139
  // If inside JSON.stringify and not optional chain, replace the entire call with just the object
137
140
  if (insideJSONStringify &&
@@ -151,14 +154,14 @@ exports.preferUrlToStringOverToJson = (0, createRule_1.createRule)({
151
154
  meta: {
152
155
  type: 'suggestion',
153
156
  docs: {
154
- description: 'Enforce the use of toString() over toJSON() on URL objects. Prefer passing URL objects directly to JSON.stringify, which will call toJSON automatically. See docs/rules/prefer-url-tostring-over-tojson.md for examples.',
157
+ description: 'Enforce the use of toString() over toJSON() on URL objects.',
155
158
  recommended: 'error',
156
159
  requiresTypeChecking: false,
157
160
  },
158
161
  fixable: 'code',
159
162
  schema: [],
160
163
  messages: {
161
- preferToString: 'Use toString() instead of toJSON() on URL objects. When serializing with JSON.stringify, pass the URL object directly.',
164
+ preferToString: 'URL value {{urlText}} calls toJSON() explicitly. URL.toJSON() delegates directly to toString(), so the call is redundant. Inside JSON.stringify it is unnecessary because JSON.stringify already invokes toJSON automatically; elsewhere it hides intent because readers must remember the delegation to see it produces the same string as toString. Use toString() when you need an explicit string, or pass the URL directly to JSON.stringify to rely on its built-in serialization.',
162
165
  },
163
166
  },
164
167
  defaultOptions: [],
@@ -106,7 +106,7 @@ function isNonPrimitiveWithTypes(context, expr) {
106
106
  }
107
107
  function collectMemoizedIdentifiers(context) {
108
108
  const memoized = new Set();
109
- const sourceCode = context.getSourceCode();
109
+ const sourceCode = context.sourceCode;
110
110
  const program = sourceCode.ast;
111
111
  function visit(node) {
112
112
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclarator) {
@@ -156,7 +156,7 @@ function collectMemoizedIdentifiers(context) {
156
156
  }
157
157
  function ensureDeepCompareImportFixes(context, fixer) {
158
158
  const fixes = [];
159
- const sourceCode = context.getSourceCode();
159
+ const sourceCode = context.sourceCode;
160
160
  const program = sourceCode.ast;
161
161
  // If already imported anywhere, skip adding
162
162
  const hasImport = program.body.some((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
@@ -227,7 +227,7 @@ function removeImportSpecifierFixes(sourceCode, fixer, importDecl, specifier) {
227
227
  return fixes;
228
228
  }
229
229
  function isImportedIdentifier(context, name) {
230
- const sourceCode = context.getSourceCode();
230
+ const sourceCode = context.sourceCode;
231
231
  const program = sourceCode.ast;
232
232
  for (const node of program.body) {
233
233
  if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
@@ -319,7 +319,7 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
319
319
  fixable: 'code',
320
320
  schema: [],
321
321
  messages: {
322
- preferUseDeepCompareMemo: 'Use useDeepCompareMemo instead of useMemo because dependency array contains unmemoized non-primitive values.',
322
+ preferUseDeepCompareMemo: 'Dependency array for "{{hook}}" includes objects/arrays/functions that change identity each render, so React treats them as changed and reruns the memoized computation, triggering avoidable renders. Use useDeepCompareMemo (or memoize those dependencies first) so comparisons use deep equality and the memo stays stable.',
323
323
  },
324
324
  },
325
325
  defaultOptions: [],
@@ -386,6 +386,9 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
386
386
  context.report({
387
387
  node,
388
388
  messageId: 'preferUseDeepCompareMemo',
389
+ data: {
390
+ hook: 'useMemo',
391
+ },
389
392
  fix(fixer) {
390
393
  const fixes = [];
391
394
  // Replace callee
@@ -398,7 +401,7 @@ exports.preferUseDeepCompareMemo = (0, createRule_1.createRule)({
398
401
  // Ensure import exists
399
402
  fixes.push(...ensureDeepCompareImportFixes(context, fixer));
400
403
  // Clean up now-unused React/useMemo imports if safe
401
- const sourceCode = context.getSourceCode();
404
+ const sourceCode = context.sourceCode;
402
405
  const program = sourceCode.ast;
403
406
  const reactImport = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
404
407
  n.source.value === 'react');
@@ -166,7 +166,7 @@ function getMemoizedFunctionDescription(node) {
166
166
  return 'this callback value';
167
167
  }
168
168
  function reportAndFix(node, context) {
169
- const sourceCode = context.getSourceCode();
169
+ const sourceCode = context.sourceCode;
170
170
  const useMemoCallback = node.arguments[0];
171
171
  const dependencyArray = node.arguments[1]
172
172
  ? sourceCode.getText(node.arguments[1])
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.preferUtilityFunctionOverPrivateStatic = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
+ const getMethodName_1 = require("../utils/getMethodName");
5
6
  exports.preferUtilityFunctionOverPrivateStatic = (0, createRule_1.createRule)({
6
7
  name: 'prefer-utility-function-over-private-static',
7
8
  meta: {
@@ -12,7 +13,7 @@ exports.preferUtilityFunctionOverPrivateStatic = (0, createRule_1.createRule)({
12
13
  },
13
14
  schema: [],
14
15
  messages: {
15
- preferUtilityFunctionOverPrivateStatic: 'Private static methods should be abstracted into standalone utility functions for better reusability and testability',
16
+ preferUtilityFunctionOverPrivateStatic: 'Private static method "{{methodName}}" in class "{{className}}" does not use class state. Keeping class-agnostic helpers as private statics hides reusable logic and signals coupling that is not present. Extract this logic into a standalone utility function (module-level or shared) and import it where needed so it can be reused and unit tested independently.',
16
17
  },
17
18
  },
18
19
  defaultOptions: [],
@@ -47,9 +48,32 @@ exports.preferUtilityFunctionOverPrivateStatic = (0, createRule_1.createRule)({
47
48
  }
48
49
  return false;
49
50
  };
51
+ const getClassName = (method) => {
52
+ const classNode = method.parent?.parent;
53
+ if (classNode &&
54
+ (classNode.type === 'ClassDeclaration' ||
55
+ classNode.type === 'ClassExpression')) {
56
+ if (classNode.id && classNode.id.type === 'Identifier') {
57
+ return classNode.id.name;
58
+ }
59
+ if (classNode.type === 'ClassExpression' &&
60
+ classNode.parent &&
61
+ classNode.parent.type === 'VariableDeclarator' &&
62
+ classNode.parent.id.type === 'Identifier') {
63
+ return classNode.parent.id.name;
64
+ }
65
+ if (classNode.type === 'ClassExpression' &&
66
+ classNode.parent &&
67
+ classNode.parent.type === 'AssignmentExpression' &&
68
+ classNode.parent.left.type === 'Identifier') {
69
+ return classNode.parent.left.name;
70
+ }
71
+ }
72
+ return 'this class';
73
+ };
50
74
  return {
51
75
  'MethodDefinition[static=true][accessibility="private"]'(node) {
52
- const sourceCode = context.getSourceCode();
76
+ const sourceCode = context.sourceCode;
53
77
  const methodBody = node.value.body;
54
78
  if (!methodBody) {
55
79
  return;
@@ -65,9 +89,17 @@ exports.preferUtilityFunctionOverPrivateStatic = (0, createRule_1.createRule)({
65
89
  const usesThis = hasThisReference(methodBody);
66
90
  // If the method doesn't use 'this', it's a good candidate for extraction
67
91
  if (!usesThis) {
92
+ const methodName = (0, getMethodName_1.getMethodName)(node, sourceCode, {
93
+ computedFallbackToText: false,
94
+ }) || '<unknown>';
95
+ const className = getClassName(node);
68
96
  context.report({
69
97
  node,
70
98
  messageId: 'preferUtilityFunctionOverPrivateStatic',
99
+ data: {
100
+ methodName,
101
+ className,
102
+ },
71
103
  });
72
104
  }
73
105
  },
@@ -0,0 +1,2 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const preventChildrenClobber: TSESLint.RuleModule<"childrenClobbered", [], TSESLint.RuleListener>;