@blumintinc/eslint-plugin-blumint 1.13.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/README.md +169 -139
  2. package/lib/index.js +200 -9
  3. package/lib/rules/array-methods-this-context.js +1 -1
  4. package/lib/rules/avoid-utils-directory.js +0 -4
  5. package/lib/rules/class-methods-read-top-to-bottom.js +24 -5
  6. package/lib/rules/consistent-callback-naming.js +43 -4
  7. package/lib/rules/dynamic-https-errors.d.ts +1 -1
  8. package/lib/rules/dynamic-https-errors.js +134 -44
  9. package/lib/rules/enforce-assert-safe-object-key.js +11 -11
  10. package/lib/rules/enforce-boolean-naming-prefixes.js +11 -235
  11. package/lib/rules/enforce-callback-memo.js +1 -1
  12. package/lib/rules/enforce-centralized-mock-firestore.js +1 -1
  13. package/lib/rules/enforce-console-error.js +35 -7
  14. package/lib/rules/enforce-date-ttime.d.ts +1 -0
  15. package/lib/rules/enforce-date-ttime.js +156 -0
  16. package/lib/rules/enforce-dynamic-file-naming.d.ts +2 -0
  17. package/lib/rules/enforce-dynamic-file-naming.js +53 -28
  18. package/lib/rules/enforce-dynamic-firebase-imports.d.ts +2 -1
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +3 -2
  20. package/lib/rules/enforce-early-destructuring.d.ts +2 -0
  21. package/lib/rules/enforce-early-destructuring.js +980 -0
  22. package/lib/rules/enforce-empty-object-check.d.ts +11 -0
  23. package/lib/rules/enforce-empty-object-check.js +502 -0
  24. package/lib/rules/enforce-exported-function-types.js +1 -1
  25. package/lib/rules/enforce-f-extension-for-entry-points.d.ts +8 -0
  26. package/lib/rules/enforce-f-extension-for-entry-points.js +283 -0
  27. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +100 -50
  28. package/lib/rules/enforce-firestore-doc-ref-generic.js +15 -3
  29. package/lib/rules/enforce-firestore-facade.js +4 -2
  30. package/lib/rules/enforce-firestore-set-merge.js +3 -4
  31. package/lib/rules/enforce-global-constants.js +4 -4
  32. package/lib/rules/enforce-id-capitalization.js +1 -1
  33. package/lib/rules/enforce-identifiable-firestore-type.js +108 -58
  34. package/lib/rules/enforce-memoize-async.js +71 -21
  35. package/lib/rules/enforce-memoize-getters.js +39 -15
  36. package/lib/rules/enforce-microdiff.js +1 -1
  37. package/lib/rules/enforce-object-literal-as-const.js +4 -4
  38. package/lib/rules/enforce-positive-naming.js +4 -0
  39. package/lib/rules/enforce-props-argument-name.js +44 -18
  40. package/lib/rules/enforce-safe-stringify.js +1 -1
  41. package/lib/rules/enforce-serializable-params.js +3 -3
  42. package/lib/rules/enforce-singular-type-names.js +1 -1
  43. package/lib/rules/enforce-stable-hash-spread-props.d.ts +13 -0
  44. package/lib/rules/enforce-stable-hash-spread-props.js +342 -0
  45. package/lib/rules/enforce-storage-context.d.ts +9 -0
  46. package/lib/rules/enforce-storage-context.js +646 -0
  47. package/lib/rules/enforce-timestamp-now.js +23 -7
  48. package/lib/rules/enforce-transform-memoization.d.ts +4 -0
  49. package/lib/rules/enforce-transform-memoization.js +416 -0
  50. package/lib/rules/enforce-typescript-markdown-code-blocks.js +7 -3
  51. package/lib/rules/enforce-unique-cursor-headers.d.ts +16 -0
  52. package/lib/rules/enforce-unique-cursor-headers.js +365 -0
  53. package/lib/rules/enforce-verb-noun-naming.js +3818 -4644
  54. package/lib/rules/ensure-pointer-events-none.js +28 -2
  55. package/lib/rules/export-if-in-doubt.js +27 -4
  56. package/lib/rules/extract-global-constants.js +40 -13
  57. package/lib/rules/fast-deep-equal-over-microdiff.js +29 -39
  58. package/lib/rules/firestore-transaction-reads-before-writes.js +50 -4
  59. package/lib/rules/flatten-push-calls.d.ts +2 -0
  60. package/lib/rules/flatten-push-calls.js +428 -0
  61. package/lib/rules/global-const-style.js +66 -23
  62. package/lib/rules/jsdoc-above-field.d.ts +11 -0
  63. package/lib/rules/jsdoc-above-field.js +208 -0
  64. package/lib/rules/key-only-outermost-element.js +21 -3
  65. package/lib/rules/logical-top-to-bottom-grouping.d.ts +5 -0
  66. package/lib/rules/logical-top-to-bottom-grouping.js +1258 -0
  67. package/lib/rules/memo-compare-deeply-complex-props.d.ts +3 -0
  68. package/lib/rules/memo-compare-deeply-complex-props.js +801 -0
  69. package/lib/rules/memo-nested-react-components.d.ts +8 -0
  70. package/lib/rules/memo-nested-react-components.js +415 -0
  71. package/lib/rules/no-always-true-false-conditions.d.ts +2 -1
  72. package/lib/rules/no-always-true-false-conditions.js +68 -8
  73. package/lib/rules/no-array-length-in-deps.js +9 -3
  74. package/lib/rules/no-async-foreach.js +215 -12
  75. package/lib/rules/no-circular-references.d.ts +2 -1
  76. package/lib/rules/no-circular-references.js +31 -32
  77. package/lib/rules/no-class-instance-destructuring.js +55 -12
  78. package/lib/rules/no-complex-cloud-params.js +7 -3
  79. package/lib/rules/no-compositing-layer-props.js +2 -2
  80. package/lib/rules/no-conditional-literals-in-jsx.js +37 -12
  81. package/lib/rules/no-console-error.d.ts +9 -0
  82. package/lib/rules/no-console-error.js +527 -0
  83. package/lib/rules/no-curly-brackets-around-commented-properties.d.ts +2 -0
  84. package/lib/rules/no-curly-brackets-around-commented-properties.js +258 -0
  85. package/lib/rules/no-empty-dependency-use-callbacks.d.ts +11 -0
  86. package/lib/rules/no-empty-dependency-use-callbacks.js +576 -0
  87. package/lib/rules/no-entire-object-hook-deps.js +52 -7
  88. package/lib/rules/no-excessive-parent-chain.js +4 -1
  89. package/lib/rules/no-explicit-return-type.d.ts +2 -1
  90. package/lib/rules/no-explicit-return-type.js +215 -26
  91. package/lib/rules/no-filter-without-return.js +5 -1
  92. package/lib/rules/no-firestore-jest-mock.d.ts +2 -1
  93. package/lib/rules/no-firestore-jest-mock.js +126 -8
  94. package/lib/rules/no-firestore-object-arrays.js +67 -12
  95. package/lib/rules/no-handler-suffix.d.ts +12 -0
  96. package/lib/rules/no-handler-suffix.js +305 -0
  97. package/lib/rules/no-hungarian.js +1 -1
  98. package/lib/rules/no-inline-component-prop.d.ts +10 -0
  99. package/lib/rules/no-inline-component-prop.js +465 -0
  100. package/lib/rules/no-jsx-in-hooks.js +6 -1
  101. package/lib/rules/no-jsx-whitespace-literal.js +8 -2
  102. package/lib/rules/no-margin-properties.js +6 -6
  103. package/lib/rules/no-memoize-on-static.js +9 -1
  104. package/lib/rules/no-misleading-boolean-prefixes.js +7 -3
  105. package/lib/rules/no-misused-switch-case.js +22 -1
  106. package/lib/rules/no-mixed-firestore-transactions.d.ts +3 -1
  107. package/lib/rules/no-mixed-firestore-transactions.js +296 -34
  108. package/lib/rules/no-mock-firebase-admin.js +5 -2
  109. package/lib/rules/no-object-values-on-strings.js +10 -2
  110. package/lib/rules/no-overridable-method-calls-in-constructor.js +29 -17
  111. package/lib/rules/no-passthrough-getters.d.ts +2 -2
  112. package/lib/rules/no-passthrough-getters.js +120 -2
  113. package/lib/rules/no-redundant-annotation-assertion.d.ts +2 -0
  114. package/lib/rules/no-redundant-annotation-assertion.js +402 -0
  115. package/lib/rules/no-redundant-param-types.js +16 -6
  116. package/lib/rules/no-redundant-this-params.d.ts +3 -0
  117. package/lib/rules/no-redundant-this-params.js +508 -0
  118. package/lib/rules/no-redundant-usecallback-wrapper.js +16 -3
  119. package/lib/rules/no-res-error-status-in-onrequest.d.ts +4 -0
  120. package/lib/rules/no-res-error-status-in-onrequest.js +521 -0
  121. package/lib/rules/no-restricted-properties-fix.js +11 -10
  122. package/lib/rules/no-separate-loading-state.js +7 -16
  123. package/lib/rules/no-stale-state-across-await.js +1 -1
  124. package/lib/rules/no-type-assertion-returns.js +43 -49
  125. package/lib/rules/no-undefined-null-passthrough.js +61 -31
  126. package/lib/rules/no-unmemoized-memo-without-props.d.ts +8 -0
  127. package/lib/rules/no-unmemoized-memo-without-props.js +426 -0
  128. package/lib/rules/no-unnecessary-destructuring-rename.d.ts +2 -0
  129. package/lib/rules/no-unnecessary-destructuring-rename.js +344 -0
  130. package/lib/rules/no-unnecessary-destructuring.js +14 -4
  131. package/lib/rules/no-unnecessary-verb-suffix.js +2 -1
  132. package/lib/rules/no-unpinned-dependencies.js +36 -5
  133. package/lib/rules/no-unused-props.d.ts +2 -2
  134. package/lib/rules/no-unused-props.js +295 -91
  135. package/lib/rules/no-unused-usestate.js +6 -2
  136. package/lib/rules/no-useless-fragment.js +28 -2
  137. package/lib/rules/no-useless-usememo-primitives.d.ts +9 -0
  138. package/lib/rules/no-useless-usememo-primitives.js +393 -0
  139. package/lib/rules/no-usememo-for-pass-by-value.d.ts +13 -0
  140. package/lib/rules/no-usememo-for-pass-by-value.js +757 -0
  141. package/lib/rules/no-uuidv4-base62-as-key.js +18 -13
  142. package/lib/rules/omit-index-html.d.ts +2 -1
  143. package/lib/rules/omit-index-html.js +62 -7
  144. package/lib/rules/optimize-object-boolean-conditions.js +6 -4
  145. package/lib/rules/parallelize-async-operations.js +143 -59
  146. package/lib/rules/prefer-batch-operations.d.ts +1 -3
  147. package/lib/rules/prefer-batch-operations.js +32 -5
  148. package/lib/rules/prefer-block-comments-for-declarations.js +9 -4
  149. package/lib/rules/prefer-clone-deep.js +3 -3
  150. package/lib/rules/prefer-destructuring-no-class.d.ts +2 -1
  151. package/lib/rules/prefer-destructuring-no-class.js +244 -59
  152. package/lib/rules/prefer-docsetter-setall.d.ts +2 -0
  153. package/lib/rules/prefer-docsetter-setall.js +243 -0
  154. package/lib/rules/prefer-field-paths-in-transforms.js +4 -3
  155. package/lib/rules/prefer-fragment-component.js +1 -1
  156. package/lib/rules/prefer-fragment-shorthand.js +2 -1
  157. package/lib/rules/prefer-getter-over-parameterless-method.d.ts +13 -0
  158. package/lib/rules/prefer-getter-over-parameterless-method.js +648 -0
  159. package/lib/rules/prefer-global-router-state-key.js +88 -13
  160. package/lib/rules/prefer-memoized-props.d.ts +3 -0
  161. package/lib/rules/prefer-memoized-props.js +445 -0
  162. package/lib/rules/prefer-next-dynamic.js +60 -69
  163. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +118 -8
  164. package/lib/rules/prefer-nullish-coalescing-override.d.ts +2 -1
  165. package/lib/rules/prefer-nullish-coalescing-override.js +11 -5
  166. package/lib/rules/prefer-params-over-parent-id.js +221 -172
  167. package/lib/rules/prefer-settings-object.js +27 -10
  168. package/lib/rules/prefer-type-alias-over-typeof-constant.js +9 -0
  169. package/lib/rules/prefer-type-over-interface.js +7 -2
  170. package/lib/rules/prefer-url-tostring-over-tojson.js +6 -3
  171. package/lib/rules/prefer-use-deep-compare-memo.js +8 -5
  172. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +1 -1
  173. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +1 -1
  174. package/lib/rules/prefer-utility-function-over-private-static.js +34 -2
  175. package/lib/rules/prevent-children-clobber.d.ts +2 -0
  176. package/lib/rules/prevent-children-clobber.js +540 -0
  177. package/lib/rules/react-memoize-literals.d.ts +4 -0
  178. package/lib/rules/react-memoize-literals.js +614 -0
  179. package/lib/rules/react-usememo-should-be-component.js +2 -2
  180. package/lib/rules/require-hooks-default-params.js +17 -16
  181. package/lib/rules/require-https-error-cause.d.ts +4 -0
  182. package/lib/rules/require-https-error-cause.js +155 -0
  183. package/lib/rules/require-https-error.js +43 -21
  184. package/lib/rules/require-image-optimized.js +1 -1
  185. package/lib/rules/require-memo.js +18 -10
  186. package/lib/rules/require-memoize-jsx-returners.d.ts +3 -0
  187. package/lib/rules/require-memoize-jsx-returners.js +485 -0
  188. package/lib/rules/require-usememo-object-literals.js +2 -3
  189. package/lib/rules/test-file-location-enforcement.js +1 -1
  190. package/lib/rules/use-latest-callback.js +6 -5
  191. package/lib/rules/vertically-group-related-functions.d.ts +16 -0
  192. package/lib/rules/vertically-group-related-functions.js +480 -0
  193. package/lib/utils/ASTHelpers.d.ts +47 -2
  194. package/lib/utils/ASTHelpers.js +370 -112
  195. package/lib/utils/getMethodName.d.ts +27 -0
  196. package/lib/utils/getMethodName.js +35 -0
  197. package/lib/utils/tsTypeClassifier.d.ts +30 -0
  198. package/lib/utils/tsTypeClassifier.js +149 -0
  199. package/package.json +3 -1
@@ -13,210 +13,259 @@ const HANDLER_TYPES = new Set([
13
13
  'RealtimeDbChangeHandler',
14
14
  'RealtimeDbChangeHandlerTransaction',
15
15
  ]);
16
- exports.preferParamsOverParentId = (0, createRule_1.createRule)({
17
- name: 'prefer-params-over-parent-id',
18
- meta: {
19
- type: 'suggestion',
20
- docs: {
21
- description: 'Prefer handler params for parent IDs instead of traversing ref.parent.id so Firebase triggers stay aligned with path templates and type-safe.',
22
- recommended: 'error',
23
- },
24
- fixable: 'code',
25
- schema: [],
26
- 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}}`).',
28
- },
29
- },
30
- defaultOptions: [],
31
- 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;
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;
62
31
  }
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;
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;
72
59
  }
73
- return checkTypeAnnotationForHandler(typeAnnotation.typeAnnotation);
60
+ current = current.parent;
74
61
  }
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;
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;
81
123
  }
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 };
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 };
87
145
  }
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 };
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
+ }
93
154
  }
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 };
155
+ return { properties };
107
156
  }
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
157
  }
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;
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;
124
175
  }
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;
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;
132
183
  }
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
- });
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;
153
190
  }
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
- }
191
+ const paramsInScope = findParamsInPattern(declarator.id);
192
+ if (paramsInScope) {
193
+ return paramsInScope;
181
194
  }
182
195
  }
183
- return false;
184
196
  }
197
+ }
198
+ return null;
199
+ };
200
+ exports.preferParamsOverParentId = (0, createRule_1.createRule)({
201
+ name: 'prefer-params-over-parent-id',
202
+ meta: {
203
+ type: 'suggestion',
204
+ docs: {
205
+ description: 'Prefer event.params over ref.parent.id for type-safe Firebase trigger paths.',
206
+ recommended: 'error',
207
+ },
208
+ fixable: 'code',
209
+ schema: [],
210
+ messages: {
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'),
218
+ },
219
+ },
220
+ defaultOptions: [],
221
+ create(context) {
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
  }
@@ -47,6 +47,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
47
47
  defaultOptions: [defaultOptions],
48
48
  create(context, [options]) {
49
49
  const finalOptions = { ...defaultOptions, ...options };
50
+ const { sourceCode } = context;
50
51
  function getParameterType(param) {
51
52
  if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
52
53
  return getParameterType(param.left);
@@ -56,13 +57,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
56
57
  const typeNode = param.typeAnnotation.typeAnnotation;
57
58
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
58
59
  // Include type parameters in the type signature to differentiate generic types
59
- const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
60
- ? typeNode.typeName.name
61
- : 'unknown';
60
+ const typeName = sourceCode.getText(typeNode.typeName);
62
61
  // If there are type parameters, include them in the type signature
63
62
  if (typeNode.typeParameters &&
64
63
  typeNode.typeParameters.params.length > 0) {
65
- const sourceCode = context.getSourceCode();
66
64
  const typeParams = typeNode.typeParameters.params
67
65
  .map((param) => sourceCode.getText(param))
68
66
  .join(', ');
@@ -76,13 +74,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
76
74
  const typeNode = param.typeAnnotation.typeAnnotation;
77
75
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
78
76
  // Include type parameters in the type signature to differentiate generic types
79
- const typeName = typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier
80
- ? typeNode.typeName.name
81
- : 'unknown';
77
+ const typeName = sourceCode.getText(typeNode.typeName);
82
78
  // If there are type parameters, include them in the type signature
83
79
  if (typeNode.typeParameters &&
84
80
  typeNode.typeParameters.params.length > 0) {
85
- const sourceCode = context.getSourceCode();
86
81
  const typeParams = typeNode.typeParameters.params
87
82
  .map((param) => sourceCode.getText(param))
88
83
  .join(', ');
@@ -98,12 +93,14 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
98
93
  return 'boolean';
99
94
  return typeNode.type;
100
95
  }
101
- return 'unknown';
96
+ return null;
102
97
  }
103
98
  function findDuplicateParameterType(params) {
104
99
  const typeMap = new Map();
105
100
  for (const param of params) {
106
101
  const type = getParameterType(param);
102
+ if (type === null)
103
+ continue;
107
104
  const count = (typeMap.get(type) || 0) + 1;
108
105
  typeMap.set(type, count);
109
106
  if (count > 1) {
@@ -248,6 +245,24 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
248
245
  }
249
246
  return false;
250
247
  }
248
+ function hasImplicitlyTypedParams(params) {
249
+ return params.some((param) => {
250
+ let current = param;
251
+ if (current.type === utils_1.AST_NODE_TYPES.TSParameterProperty) {
252
+ current = current.parameter;
253
+ }
254
+ if (current.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
255
+ current = current.left;
256
+ }
257
+ if (current.type === utils_1.AST_NODE_TYPES.Identifier ||
258
+ current.type === utils_1.AST_NODE_TYPES.ObjectPattern ||
259
+ current.type === utils_1.AST_NODE_TYPES.ArrayPattern ||
260
+ current.type === utils_1.AST_NODE_TYPES.RestElement) {
261
+ return !current.typeAnnotation;
262
+ }
263
+ return true;
264
+ });
265
+ }
251
266
  function shouldIgnoreNode(node) {
252
267
  // Ignore built-in objects and third-party modules
253
268
  if (isBuiltInOrThirdParty(node))
@@ -270,7 +285,7 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
270
285
  parent = parent.parent;
271
286
  }
272
287
  }
273
- // Ignore functions with A/B pattern parameters
288
+ // Ignore functions with A/B pattern parameters or missing type annotations
274
289
  if ((node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
275
290
  node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
276
291
  node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
@@ -279,6 +294,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
279
294
  Array.isArray(node.params)) {
280
295
  if (hasABPattern(node.params))
281
296
  return true;
297
+ if (hasImplicitlyTypedParams(node.params))
298
+ return true;
282
299
  }
283
300
  // Check if the function is a handler with transaction parameter
284
301
  // This is a common pattern in Firebase/Firestore handlers
@@ -225,6 +225,15 @@ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
225
225
  TSTypeQuery(node) {
226
226
  if (!collected)
227
227
  return;
228
+ // Skip if inside a type alias declaration (Issue #1117)
229
+ // This allows 'type T = typeof CONST' as the canonical way to define the alias.
230
+ let current = node.parent;
231
+ while (current) {
232
+ if (current.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
233
+ return;
234
+ }
235
+ current = current.parent;
236
+ }
228
237
  // Skip `keyof typeof X`
229
238
  if (node.parent &&
230
239
  node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
@@ -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: [],