@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
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceFieldPathSyntaxInDocSetter = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const getMethodName_1 = require("../utils/getMethodName");
6
7
  exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
7
8
  name: 'enforce-fieldpath-syntax-in-docsetter',
8
9
  meta: {
@@ -16,11 +17,12 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
16
17
  fixable: 'code',
17
18
  schema: [],
18
19
  messages: {
19
- enforceFieldPathSyntax: 'Use FieldPath syntax (dot notation) for nested fields in DocSetter. Instead of `{ roles: { contributor: value } }`, use `{ "roles.contributor": value }`.',
20
+ enforceFieldPathSyntax: 'What’s wrong: DocSetter {{methodName}} receives nested object data under "{{topLevelKey}}". → Why it matters: Firestore treats that nested map as a whole sub-document write, so partial updates can overwrite sibling fields you did not include. How to fix: Flatten nested properties into FieldPath (dot) keys before passing documentData (e.g., "{{exampleFieldPath}}") so only the intended leaves are written.',
20
21
  },
21
22
  },
22
23
  defaultOptions: [],
23
24
  create(context) {
25
+ const sourceCode = context.getSourceCode();
24
26
  // Track DocSetter variables
25
27
  const docSetterVariables = new Set();
26
28
  // Helper function to check if a node is a DocSetter method call
@@ -51,45 +53,14 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
51
53
  return (prop.type === utils_1.AST_NODE_TYPES.SpreadElement ||
52
54
  (prop.type === utils_1.AST_NODE_TYPES.Property && prop.computed === true));
53
55
  }
54
- // Helper function to check if an object has nested objects (excluding arrays)
55
- function hasNestedObjects(node, prefix = '') {
56
- for (const property of node.properties) {
57
- // Skip spread elements
58
- if (property.type === utils_1.AST_NODE_TYPES.SpreadElement) {
59
- continue;
60
- }
61
- if (property.type !== utils_1.AST_NODE_TYPES.Property) {
62
- continue;
63
- }
64
- // Skip computed properties (dynamic keys)
65
- if (property.computed) {
66
- continue;
67
- }
68
- const isNumericKey = property.key.type === utils_1.AST_NODE_TYPES.Literal &&
69
- (typeof property.key.value === 'number' ||
70
- (typeof property.key.value === 'string' &&
71
- /^\d+$/.test(property.key.value)));
72
- // Allow top-level numeric keys (treated like array indexes)
73
- if (prefix === '' && isNumericKey) {
74
- continue;
75
- }
76
- const value = property.value;
77
- // Skip if the property key is already using dot notation
78
- if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
79
- typeof property.key.value === 'string' &&
80
- property.key.value.includes('.')) {
81
- continue;
82
- }
83
- // Check if the value is an object (but not an array)
84
- if (value.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
85
- // Skip nested objects that contain spread elements or computed properties
86
- const hasSpreadOrComputed = value.properties.some((prop) => isSpreadOrComputed(prop));
87
- if (!hasSpreadOrComputed) {
88
- return true;
89
- }
90
- }
91
- }
92
- return false;
56
+ function isNumericKey(property) {
57
+ return (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
58
+ (typeof property.key.value === 'number' ||
59
+ (typeof property.key.value === 'string' &&
60
+ /^\d+$/.test(property.key.value))));
61
+ }
62
+ function hasRootNumericKey(node) {
63
+ return node.properties.some((property) => property.type === utils_1.AST_NODE_TYPES.Property && isNumericKey(property));
93
64
  }
94
65
  // Helper function to flatten nested objects into FieldPath syntax
95
66
  function flattenObject(obj, sourceCode, prefix = '') {
@@ -127,11 +98,8 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
127
98
  // Skip other key types
128
99
  continue;
129
100
  }
130
- const isNumericKey = property.key.type === utils_1.AST_NODE_TYPES.Literal &&
131
- (typeof property.key.value === 'number' ||
132
- (typeof property.key.value === 'string' &&
133
- /^\d+$/.test(property.key.value)));
134
- if (prefix === '' && isNumericKey) {
101
+ const numericKey = isNumericKey(property);
102
+ if (prefix === '' && numericKey) {
135
103
  continue;
136
104
  }
137
105
  const fullKey = prefix ? `${prefix}.${key}` : key;
@@ -152,11 +120,13 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
152
120
  return key.includes('.') || !/^(?:[$_A-Za-z][$\w]*)$/u.test(key);
153
121
  }
154
122
  // Helper function to convert an object to FieldPath syntax
155
- function convertToFieldPathSyntax(node, sourceCode) {
123
+ function convertToFieldPathSyntax(node, sourceCode, preflattened) {
156
124
  const idProperty = node.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
157
125
  prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
158
126
  prop.key.name === 'id');
159
- const flattenedProperties = flattenObject(node, sourceCode);
127
+ const flattenedProperties = {
128
+ ...(preflattened ?? flattenObject(node, sourceCode)),
129
+ };
160
130
  if (idProperty && idProperty.type === utils_1.AST_NODE_TYPES.Property) {
161
131
  delete flattenedProperties['id'];
162
132
  }
@@ -175,6 +145,72 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
175
145
  result += '}';
176
146
  return result;
177
147
  }
148
+ function getPropertyKeyText(property) {
149
+ if (property.key.type === utils_1.AST_NODE_TYPES.Identifier) {
150
+ return property.key.name;
151
+ }
152
+ if (property.key.type === utils_1.AST_NODE_TYPES.Literal &&
153
+ (typeof property.key.value === 'string' ||
154
+ typeof property.key.value === 'number')) {
155
+ return String(property.key.value);
156
+ }
157
+ return undefined;
158
+ }
159
+ function getFirstNestedObjectProperty(node, keyPredicate) {
160
+ for (const property of node.properties) {
161
+ if (isSpreadOrComputed(property)) {
162
+ continue;
163
+ }
164
+ if (property.type !== utils_1.AST_NODE_TYPES.Property) {
165
+ continue;
166
+ }
167
+ const propertyKeyText = getPropertyKeyText(property);
168
+ if (!propertyKeyText) {
169
+ continue;
170
+ }
171
+ if (keyPredicate && !keyPredicate(propertyKeyText)) {
172
+ continue;
173
+ }
174
+ // Root-level numeric keys typically model array-style buckets rather than
175
+ // Firestore document fields, so ignore them when identifying nested objects
176
+ if (node.parent?.type !== utils_1.AST_NODE_TYPES.Property &&
177
+ isNumericKey(property)) {
178
+ continue;
179
+ }
180
+ if (property.value.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
181
+ continue;
182
+ }
183
+ const hasSpreadOrComputed = property.value.properties.some((prop) => isSpreadOrComputed(prop));
184
+ if (hasSpreadOrComputed) {
185
+ continue;
186
+ }
187
+ return property;
188
+ }
189
+ return undefined;
190
+ }
191
+ function extractViolationDetails(firstArg, sourceCode) {
192
+ const firstNestedPropertyWithoutDots = getFirstNestedObjectProperty(firstArg, (key) => !key.includes('.'));
193
+ const firstNestedProperty = firstNestedPropertyWithoutDots ||
194
+ getFirstNestedObjectProperty(firstArg);
195
+ if (!firstNestedProperty) {
196
+ return null;
197
+ }
198
+ const propertyKeyText = getPropertyKeyText(firstNestedProperty);
199
+ const exampleFieldPathFromProperty = propertyKeyText &&
200
+ flattenObject(firstNestedProperty.value, sourceCode, propertyKeyText);
201
+ const flattenedProperties = flattenObject(firstArg, sourceCode);
202
+ const exampleFieldPath = (exampleFieldPathFromProperty &&
203
+ Object.keys(exampleFieldPathFromProperty).find((key) => key.includes('.'))) ??
204
+ (propertyKeyText &&
205
+ Object.keys(flattenedProperties).find((key) => key.startsWith(`${propertyKeyText}.`))) ??
206
+ Object.keys(flattenedProperties).find((key) => key.includes('.')) ??
207
+ 'field.nested';
208
+ return {
209
+ topLevelKey: propertyKeyText ?? 'nested field',
210
+ exampleFieldPath,
211
+ flattenedProperties,
212
+ };
213
+ }
178
214
  return {
179
215
  // Track DocSetter variable declarations
180
216
  VariableDeclarator(node) {
@@ -192,16 +228,30 @@ exports.enforceFieldPathSyntaxInDocSetter = (0, createRule_1.createRule)({
192
228
  }
193
229
  // Check if the first argument is an object literal
194
230
  const firstArg = node.arguments[0];
195
- if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression ||
196
- !hasNestedObjects(firstArg)) {
231
+ if (firstArg?.type !== utils_1.AST_NODE_TYPES.ObjectExpression) {
232
+ return;
233
+ }
234
+ if (hasRootNumericKey(firstArg)) {
235
+ return;
236
+ }
237
+ const violationDetails = extractViolationDetails(firstArg, sourceCode);
238
+ if (!violationDetails) {
197
239
  return;
198
240
  }
199
241
  // Report and fix the issue
242
+ const callee = node.callee;
200
243
  context.report({
201
244
  node: firstArg,
202
245
  messageId: 'enforceFieldPathSyntax',
246
+ data: {
247
+ methodName: `${(0, getMethodName_1.getMemberExpressionName)(callee, sourceCode, {
248
+ computedFallbackToText: false,
249
+ }) || 'set'}()`,
250
+ topLevelKey: violationDetails.topLevelKey,
251
+ exampleFieldPath: violationDetails.exampleFieldPath,
252
+ },
203
253
  fix(fixer) {
204
- const newText = convertToFieldPathSyntax(firstArg, context.getSourceCode());
254
+ const newText = convertToFieldPathSyntax(firstArg, sourceCode, violationDetails.flattenedProperties);
205
255
  return fixer.replaceText(firstArg, newText);
206
256
  },
207
257
  });
@@ -24,8 +24,20 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
24
24
  },
25
25
  schema: [],
26
26
  messages: {
27
- missingGeneric: '{{ type }} must specify a generic type argument for type safety. Instead of `const docRef = doc(collection)`, use `const docRef = doc<YourType>(collection)`.',
28
- invalidGeneric: '{{ type }} must not use "any" or "{}" as generic type argument. Define a proper interface/type for your document: `interface UserDoc { name: string; age: number; }` and use it: `const docRef = doc<UserDoc>(collection)`.',
27
+ missingGeneric: [
28
+ "What's wrong: {{ type }} is missing its document schema generic (the document data type).",
29
+ '',
30
+ 'Why it matters: Without the generic, Firestore references fall back to loose DocumentData, so TypeScript cannot catch field typos or missing required properties before they reach Firestore.',
31
+ '',
32
+ 'How to fix: Add the document interface/type as the generic (e.g., const ref: {{ type }}<UserDoc> = ... or doc<UserDoc>(collection)).',
33
+ ].join('\n'),
34
+ invalidGeneric: [
35
+ 'What\'s wrong: {{ type }} uses "any" or an empty object ({}) in its schema generic.',
36
+ '',
37
+ 'Why it matters: This erases the document schema and disables TypeScript checks on Firestore reads and writes, so malformed payloads and missing fields can pass silently.',
38
+ '',
39
+ 'How to fix: Define a concrete interface/type for the document (e.g., interface UserDoc { name: string }) and use it as the generic instead of "any" or {}.',
40
+ ].join('\n'),
29
41
  },
30
42
  },
31
43
  defaultOptions: [],
@@ -61,7 +73,7 @@ exports.enforceFirestoreDocRefGeneric = (0, createRule_1.createRule)({
61
73
  }
62
74
  // Prevent infinite recursion
63
75
  typeCache.set(typeName, false);
64
- const program = context.getSourceCode().ast;
76
+ const program = context.sourceCode.ast;
65
77
  const interfaceDecl = program.body.find((n) => n.type === utils_1.AST_NODE_TYPES.TSInterfaceDeclaration &&
66
78
  n.id.name === typeName);
67
79
  if (interfaceDecl) {
@@ -94,7 +94,7 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
94
94
  const firestoreTransactionVariables = new Set();
95
95
  const docSetterVariables = new Set();
96
96
  const batchManagerVariables = new Set();
97
- const sourceCode = context.getSourceCode();
97
+ const sourceCode = context.sourceCode;
98
98
  const clearFirestoreTrackingFor = (name) => {
99
99
  firestoreDocRefVariables.delete(name);
100
100
  firestoreCollectionVariables.delete(name);
@@ -149,7 +149,9 @@ exports.enforceFirestoreFacade = (0, createRule_1.createRule)({
149
149
  return true;
150
150
  }
151
151
  if (target.type === utils_1.AST_NODE_TYPES.Identifier &&
152
- (target.name === 'transaction' || target.name === 'tx' || target.name === 't')) {
152
+ (target.name === 'transaction' ||
153
+ target.name === 'tx' ||
154
+ target.name === 't')) {
153
155
  firestoreTransactionVariables.add(varName);
154
156
  return true;
155
157
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.enforceFirestoreSetMerge = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
7
8
  name: 'enforce-firestore-set-merge',
8
9
  meta: {
@@ -93,9 +94,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
93
94
  return true;
94
95
  }
95
96
  // Check if it's a Firestore document reference by looking at imports
96
- const program = context
97
- .getAncestors()
98
- .find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
97
+ const program = ASTHelpers_1.ASTHelpers.getAncestors(context, node).find((node) => node.type === utils_1.AST_NODE_TYPES.Program);
99
98
  if (program) {
100
99
  for (const node of program.body) {
101
100
  if (node.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
@@ -212,7 +211,7 @@ exports.enforceFirestoreSetMerge = (0, createRule_1.createRule)({
212
211
  node,
213
212
  messageId: 'preferSetMerge',
214
213
  fix(fixer) {
215
- const newText = convertUpdateToSetMerge(node, context.getSourceCode());
214
+ const newText = convertUpdateToSetMerge(node, context.sourceCode);
216
215
  return fixer.replaceText(node, newText);
217
216
  },
218
217
  });
@@ -21,7 +21,7 @@ exports.enforceGlobalConstants = (0, createRule_1.createRule)({
21
21
  },
22
22
  defaultOptions: [],
23
23
  create(context) {
24
- const sourceCode = context.getSourceCode();
24
+ const sourceCode = context.sourceCode;
25
25
  function isHookName(name) {
26
26
  return /^use[A-Z]/.test(name);
27
27
  }
@@ -5,8 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.enforceIdentifiableFirestoreType = void 0;
7
7
  const utils_1 = require("@typescript-eslint/utils");
8
- const createRule_1 = require("../utils/createRule");
9
8
  const path_1 = __importDefault(require("path"));
9
+ const createRule_1 = require("../utils/createRule");
10
+ const TRANSPARENT_TYPE_NAMES = new Set(['Readonly', 'Resolve']);
10
11
  exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
11
12
  name: 'enforce-identifiable-firestore-type',
12
13
  meta: {
@@ -63,94 +64,143 @@ exports.enforceIdentifiableFirestoreType = (0, createRule_1.createRule)({
63
64
  TSTypeAliasDeclaration(node) {
64
65
  if (node.id.name === folderName) {
65
66
  hasExpectedType = true;
66
- // Find Identifiable in type's dependencies
67
+ const findTypeAliasAnnotation = (typeName) => {
68
+ let scope = context.getScope();
69
+ while (scope) {
70
+ const variable = scope.variables.find((variableNode) => variableNode.name === typeName);
71
+ if (variable) {
72
+ const definition = variable.defs.find((definition) => definition.node.type ===
73
+ utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
74
+ if (definition &&
75
+ definition.node.type ===
76
+ utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration &&
77
+ definition.node.typeAnnotation) {
78
+ return definition.node.typeAnnotation;
79
+ }
80
+ }
81
+ scope = scope.upper;
82
+ }
83
+ return null;
84
+ };
85
+ const isParenthesizedType = (node) => node?.type === 'TSParenthesizedType';
86
+ const isReadonlyTypeOperator = (node) => node?.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
87
+ node.operator === 'readonly';
88
+ const unwrapTransparentType = (typeNode) => {
89
+ let current = typeNode;
90
+ while (current) {
91
+ if (isParenthesizedType(current)) {
92
+ const parenthesized = current;
93
+ current = parenthesized.typeAnnotation ?? null;
94
+ continue;
95
+ }
96
+ if (isReadonlyTypeOperator(current)) {
97
+ current = current.typeAnnotation ?? null;
98
+ continue;
99
+ }
100
+ break;
101
+ }
102
+ return current ?? null;
103
+ };
67
104
  const findIdentifiable = (type, checkedTypes = new Set()) => {
68
- if (!type)
105
+ const resolvedType = unwrapTransparentType(type);
106
+ if (!resolvedType) {
69
107
  return false;
70
- if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
71
- type.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
72
- const typeName = type.typeName.name;
108
+ }
109
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
110
+ resolvedType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
111
+ const typeName = resolvedType.typeName.name;
73
112
  if (typeName === 'Identifiable') {
74
113
  return true;
75
114
  }
76
- if (!checkedTypes.has(typeName)) {
77
- checkedTypes.add(typeName);
78
- // Look for the type in all scopes
79
- const scope = context.getScope();
80
- const variable = scope.variables.find((v) => v.name === typeName);
81
- if (variable) {
82
- const def = variable.defs.find((d) => d.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
83
- if (def &&
84
- 'typeAnnotation' in def.node &&
85
- def.node.typeAnnotation) {
86
- return findIdentifiable(def.node.typeAnnotation, checkedTypes);
87
- }
88
- }
89
- // Try looking in the parent scope
90
- if (scope.upper) {
91
- const parentVariable = scope.upper.variables.find((v) => v.name === typeName);
92
- if (parentVariable) {
93
- const def = parentVariable.defs.find((d) => d.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration);
94
- if (def &&
95
- 'typeAnnotation' in def.node &&
96
- def.node.typeAnnotation) {
97
- return findIdentifiable(def.node.typeAnnotation, checkedTypes);
98
- }
99
- }
100
- }
115
+ if (TRANSPARENT_TYPE_NAMES.has(typeName) &&
116
+ resolvedType.typeParameters?.params?.some((param) => findIdentifiable(param, checkedTypes))) {
117
+ return true;
118
+ }
119
+ if (checkedTypes.has(typeName)) {
120
+ return false;
101
121
  }
122
+ checkedTypes.add(typeName);
123
+ const aliasAnnotation = findTypeAliasAnnotation(typeName);
124
+ return findIdentifiable(aliasAnnotation, checkedTypes);
102
125
  }
103
- else if (type.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
104
- // For intersection types, check each part
105
- return type.types.some((part) => findIdentifiable(part, checkedTypes));
126
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
127
+ return resolvedType.types.some((part) => findIdentifiable(part, new Set(checkedTypes)));
106
128
  }
107
129
  return false;
108
130
  };
109
- // Check if type extends Identifiable
110
- const checkIdentifiableExtension = (type) => {
111
- if (!type)
112
- return false;
113
- return findIdentifiable(type);
114
- };
115
131
  // Check if type has id: string field
116
- const checkIdField = (type) => {
117
- // Check for id: string field in type literal
118
- if (type.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
119
- return type.members.some((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
132
+ const checkIdField = (type, visitedTypes = new Set()) => {
133
+ const resolvedType = unwrapTransparentType(type);
134
+ if (!resolvedType) {
135
+ return false;
136
+ }
137
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
138
+ return resolvedType.members.some((member) => member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
120
139
  member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
121
140
  member.key.name === 'id' &&
122
141
  member.typeAnnotation?.typeAnnotation.type ===
123
142
  utils_1.AST_NODE_TYPES.TSStringKeyword);
124
143
  }
125
- // Check intersection types
126
- if (type.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
127
- return type.types.some(checkIdField);
144
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
145
+ return resolvedType.types.some((part) => checkIdField(part, new Set(visitedTypes)));
146
+ }
147
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
148
+ if (resolvedType.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
149
+ return false;
150
+ }
151
+ const typeName = resolvedType.typeName.name;
152
+ if (typeName === 'Identifiable') {
153
+ return true;
154
+ }
155
+ if (TRANSPARENT_TYPE_NAMES.has(typeName) &&
156
+ resolvedType.typeParameters?.params?.some((param) => checkIdField(param, visitedTypes))) {
157
+ return true;
158
+ }
159
+ if (visitedTypes.has(typeName)) {
160
+ return false;
161
+ }
162
+ visitedTypes.add(typeName);
163
+ const referencedType = findTypeAliasAnnotation(typeName);
164
+ return checkIdField(referencedType, visitedTypes);
128
165
  }
129
166
  return false;
130
167
  };
131
168
  // Check if type is wrapped in a utility type
132
169
  const isUtilityType = (type) => {
170
+ if (!type) {
171
+ return false;
172
+ }
133
173
  return (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
134
174
  type.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
135
175
  type.typeName.name === 'Resolve');
136
176
  };
137
177
  // Recursively check the type and its parameters
138
- const checkType = (type) => {
139
- // Check if type extends Identifiable
140
- if (checkIdentifiableExtension(type)) {
178
+ const checkType = (type, visitedTypes = new Set()) => {
179
+ const resolvedType = unwrapTransparentType(type);
180
+ if (!resolvedType) {
181
+ return false;
182
+ }
183
+ if (findIdentifiable(resolvedType)) {
141
184
  return true;
142
185
  }
143
- // Check if type has id: string field (only for utility types)
144
- if (isUtilityType(type) &&
145
- checkIdField(type.typeParameters.params[0])) {
186
+ if (isUtilityType(resolvedType) &&
187
+ resolvedType.typeParameters?.params?.[0] &&
188
+ checkIdField(resolvedType.typeParameters.params[0])) {
146
189
  return true;
147
190
  }
148
- // Check if type is wrapped in a utility type
149
- if (type.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
150
- type.typeParameters?.params?.[0]) {
151
- return checkType(type.typeParameters.params[0]);
191
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
192
+ resolvedType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
193
+ const typeName = resolvedType.typeName.name;
194
+ if (visitedTypes.has(typeName)) {
195
+ return false;
196
+ }
197
+ visitedTypes.add(typeName);
198
+ const aliasAnnotation = findTypeAliasAnnotation(typeName);
199
+ return checkType(aliasAnnotation, visitedTypes);
200
+ }
201
+ if (resolvedType.type === utils_1.AST_NODE_TYPES.TSIntersectionType) {
202
+ return resolvedType.types.some((part) => checkType(part, new Set(visitedTypes)));
152
203
  }
153
- // For direct type definitions, require extending Identifiable
154
204
  return false;
155
205
  };
156
206
  typeHasIdentifiable = checkType(node.typeAnnotation);
@@ -90,7 +90,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
90
90
  messageId: 'requireMemoize',
91
91
  fix(fixer) {
92
92
  const fixes = [];
93
- const sourceCode = context.getSourceCode();
93
+ const sourceCode = context.sourceCode;
94
94
  const decoratorIdent = memoizeNamespace
95
95
  ? `${memoizeNamespace}.Memoize`
96
96
  : memoizeAlias;
@@ -122,11 +122,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
122
122
  const insertionTarget = node.decorators && node.decorators.length > 0
123
123
  ? node.decorators[0]
124
124
  : node;
125
- const insertionStart = insertionTarget.range
126
- ? insertionTarget.range[0]
127
- : node.range
128
- ? node.range[0]
129
- : 0;
125
+ const insertionStart = insertionTarget.range?.[0] ?? node.range?.[0] ?? 0;
130
126
  const text = sourceCode.text;
131
127
  const lineStart = text.lastIndexOf('\n', insertionStart - 1) + 1;
132
128
  const leadingWhitespace = text.slice(lineStart, insertionStart).match(/^[ \t]*/)?.[0] ?? '';
@@ -42,7 +42,7 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
42
42
  fixable: 'code',
43
43
  schema: [],
44
44
  messages: {
45
- requireMemoizeGetter: 'Private getters should be decorated with @Memoize() to preserve instance state and avoid redundant instantiation. Import Memoize from "@blumintinc/typescript-memoize".',
45
+ requireMemoizeGetter: 'Private getter "{{name}}" should use @Memoize() so repeated accesses reuse the same instance instead of re-instantiating and losing internal state. Add @Memoize() and import Memoize from "@blumintinc/typescript-memoize" to avoid redundant setup work.',
46
46
  },
47
47
  },
48
48
  defaultOptions: [],
@@ -52,17 +52,28 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
52
52
  if (!/\.tsx?$/i.test(filename)) {
53
53
  return {};
54
54
  }
55
+ const sourceCode = context.getSourceCode();
55
56
  let hasMemoizeImport = false;
56
57
  let memoizeAlias = 'Memoize';
58
+ let memoizeNamespace = null;
59
+ let hasNamedImport = false;
57
60
  let scheduledImportFix = false;
58
61
  return {
59
62
  ImportDeclaration(node) {
60
63
  if (MEMOIZE_MODULES.has(String(node.source.value))) {
61
- const spec = node.specifiers.find((s) => s.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
62
- s.imported.name === 'Memoize');
63
- if (spec) {
64
- hasMemoizeImport = true;
65
- memoizeAlias = spec.local?.name ?? 'Memoize';
64
+ for (const spec of node.specifiers) {
65
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
66
+ spec.imported.name === 'Memoize') {
67
+ hasMemoizeImport = true;
68
+ hasNamedImport = true;
69
+ memoizeAlias = spec.local.name;
70
+ }
71
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
72
+ hasMemoizeImport = true;
73
+ if (!hasNamedImport) {
74
+ memoizeNamespace = spec.local.name;
75
+ }
76
+ }
66
77
  }
67
78
  }
68
79
  },
@@ -76,16 +87,29 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
76
87
  // enforce only "private" accessibility (undefined => public)
77
88
  if (node.accessibility !== 'private')
78
89
  return;
79
- // Already memoized?
80
- const hasDecorator = node.decorators?.some((d) => isMemoizeDecorator(d, memoizeAlias));
90
+ const decoratorAliases = memoizeAlias === 'Memoize' ? ['Memoize'] : ['Memoize', memoizeAlias];
91
+ const hasDecorator = node.decorators?.some((decorator) => decoratorAliases.some((alias) => isMemoizeDecorator(decorator, alias)));
81
92
  if (hasDecorator)
82
93
  return;
94
+ const propertyName = node.computed
95
+ ? '[computed]'
96
+ : sourceCode.getText(node.key);
83
97
  context.report({
84
98
  node,
85
99
  messageId: 'requireMemoizeGetter',
100
+ data: { name: propertyName },
86
101
  fix(fixer) {
87
102
  const fixes = [];
88
- const sourceCode = context.getSourceCode();
103
+ const getDecoratorIdent = () => {
104
+ if (hasNamedImport) {
105
+ return memoizeAlias;
106
+ }
107
+ if (memoizeNamespace) {
108
+ return `${memoizeNamespace}.Memoize`;
109
+ }
110
+ return memoizeAlias;
111
+ };
112
+ const decoratorIdent = getDecoratorIdent();
89
113
  // Insert import if needed, at the top alongside other imports
90
114
  if (!hasMemoizeImport && !scheduledImportFix) {
91
115
  const programBody = sourceCode.ast.body;
@@ -106,12 +130,12 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
106
130
  scheduledImportFix = true;
107
131
  }
108
132
  // Insert decorator above the getter (or before the first decorator), preserving indentation
109
- const indent = ' '.repeat(node.loc.start.column);
110
- const lineStart = node.range
111
- ? sourceCode.text.lastIndexOf('\n', node.range[0] - 1) + 1
112
- : 0;
113
- const insertPosition = Math.max(0, lineStart);
114
- fixes.push(fixer.insertTextBeforeRange([insertPosition, insertPosition], `${indent}@${memoizeAlias}()\n`));
133
+ const insertionTarget = node.decorators?.[0] ?? node;
134
+ const insertionStart = insertionTarget.range[0];
135
+ const text = sourceCode.text;
136
+ const lineStart = text.lastIndexOf('\n', insertionStart - 1) + 1;
137
+ const leadingWhitespace = text.slice(lineStart, insertionStart).match(/^[ \t]*/)?.[0] ?? '';
138
+ fixes.push(fixer.insertTextBeforeRange([lineStart, lineStart], `${leadingWhitespace}@${decoratorIdent}()\n`));
115
139
  return fixes;
116
140
  },
117
141
  });