@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
@@ -15,23 +15,31 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
15
15
  meta: {
16
16
  type: 'suggestion',
17
17
  docs: {
18
- description: 'Ensures classes read linearly from top to bottom.',
18
+ description: 'Enforces a top-to-bottom class layout so callers lead into the helpers they rely on.',
19
19
  recommended: 'warn',
20
20
  },
21
21
  schema: [],
22
22
  messages: {
23
- classMethodsReadTopToBottom: 'Methods should be ordered for top-down readability.',
23
+ classMethodsReadTopToBottom: [
24
+ "What's wrong: In {{className}}, {{actualMember}} appears before {{expectedMember}}.",
25
+ 'Why it matters: Top-down flow enables local reasoning: you can verify each caller without scrolling back. Upward jumps make code reviews harder (must verify call chains in reverse), obscure which fields a helper assumes are initialized, and increase the risk of calling helpers before state is ready (leading to null reference errors or accessing uninitialized fields).',
26
+ 'How to fix: Move {{expectedMember}} above {{actualMember}} so the class reads top-to-bottom (fields to constructor to callers to helpers).',
27
+ ].join('\n'),
24
28
  },
25
29
  fixable: 'code', // To allow ESLint to autofix issues.
26
30
  },
27
31
  defaultOptions: [],
28
32
  create(context) {
29
- let className;
33
+ const classNames = new WeakMap();
30
34
  return {
31
35
  ClassDeclaration(node) {
32
- className = node.id?.name || '';
36
+ classNames.set(node.body, node.id?.name || '');
37
+ },
38
+ ClassExpression(node) {
39
+ classNames.set(node.body, node.id?.name || '');
33
40
  },
34
41
  'ClassBody:exit'(node) {
42
+ const className = classNames.get(node) || '';
35
43
  const graphBuilder = new ClassGraphBuilder_1.ClassGraphBuilder(className, node);
36
44
  const sortedOrder = graphBuilder.memberNamesSorted;
37
45
  const actualOrder = node.body
@@ -51,7 +59,13 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
51
59
  return; // Skip if there are actual duplicates
52
60
  }
53
61
  for (let i = 0; i < actualOrder.length; i++) {
54
- if (actualOrder[i] !== sortedOrder[i]) {
62
+ const actualMember = actualOrder[i];
63
+ const expectedMember = sortedOrder[i];
64
+ if (!actualMember || !expectedMember) {
65
+ throw new Error(`class-methods-read-top-to-bottom invariant violated while comparing members in ${className || 'an unnamed class'} at position ${i}: actualMember=${String(actualMember)}, expectedMember=${String(expectedMember)}, actualOrder.length=${actualOrder.length}, sortedOrder.length=${sortedOrder.length}`);
66
+ }
67
+ if (actualMember !== expectedMember) {
68
+ const classNameReport = className || 'this class';
55
69
  const sourceCode = context.getSourceCode();
56
70
  const newClassBody = sortedOrder
57
71
  .map((n) => {
@@ -75,6 +89,11 @@ exports.classMethodsReadTopToBottom = (0, createRule_1.createRule)({
75
89
  return context.report({
76
90
  node,
77
91
  messageId: 'classMethodsReadTopToBottom',
92
+ data: {
93
+ className: classNameReport,
94
+ actualMember,
95
+ expectedMember,
96
+ },
78
97
  fix(fixer) {
79
98
  return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], // Exclude the curly braces
80
99
  newClassBody);
@@ -37,8 +37,12 @@ module.exports = (0, createRule_1.createRule)({
37
37
  fixable: 'code',
38
38
  schema: [],
39
39
  messages: {
40
- callbackPropPrefix: 'Callback props (function type props) must be prefixed with "on" (e.g., onClick, onChange)',
41
- callbackFunctionPrefix: 'Callback functions should not use "handle" prefix, use descriptive verb phrases instead',
40
+ callbackPropPrefix: 'Callback prop "{{propName}}" is a function but lacks the "on" prefix. ' +
41
+ 'Consistent "on" prefixes signal event handlers to consumers and distinguish callbacks from data props. ' +
42
+ 'Rename to "on{{eventName}}".',
43
+ callbackFunctionPrefix: 'Function "{{functionName}}" uses the "handle" prefix. ' +
44
+ 'The "handle" prefix is redundant and less descriptive than action-oriented verb phrases. ' +
45
+ 'Rename using a descriptive verb (e.g., click instead of handleClick).',
42
46
  },
43
47
  },
44
48
  defaultOptions: [],
@@ -91,6 +95,28 @@ module.exports = (0, createRule_1.createRule)({
91
95
  const type = checker.getTypeAtLocation(tsNode);
92
96
  return type.getCallSignatures().length > 0;
93
97
  }
98
+ function isRenderFunction(node) {
99
+ const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
100
+ const type = checker.getTypeAtLocation(tsNode);
101
+ const signatures = type.getCallSignatures();
102
+ if (signatures.length === 0)
103
+ return false;
104
+ const isReactType = (t) => {
105
+ const typeStr = checker.typeToString(t);
106
+ return (typeStr.includes('JSX.Element') ||
107
+ typeStr.includes('ReactElement') ||
108
+ typeStr.includes('ReactNode'));
109
+ };
110
+ return signatures.some((signature) => {
111
+ const returnType = checker.getReturnTypeOfSignature(signature);
112
+ if (isReactType(returnType))
113
+ return true;
114
+ if (returnType.isUnion()) {
115
+ return returnType.types.some((t) => isReactType(t));
116
+ }
117
+ return false;
118
+ });
119
+ }
94
120
  return {
95
121
  // Check JSX attributes for callback props
96
122
  JSXAttribute(node) {
@@ -137,13 +163,19 @@ module.exports = (0, createRule_1.createRule)({
137
163
  if (isFunctionType(node.value.expression) &&
138
164
  propName &&
139
165
  !propName.startsWith('on') &&
166
+ !propName.startsWith('render') &&
167
+ !isRenderFunction(node.value.expression) &&
140
168
  !isReactComponentType(node.value.expression)) {
169
+ const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
141
170
  context.report({
142
171
  node,
143
172
  messageId: 'callbackPropPrefix',
173
+ data: {
174
+ propName,
175
+ eventName,
176
+ },
144
177
  fix(fixer) {
145
178
  // Convert camelCase to PascalCase for the event name
146
- const eventName = propName.charAt(0).toUpperCase() + propName.slice(1);
147
179
  return fixer.replaceText(node.name, `on${eventName}`);
148
180
  },
149
181
  });
@@ -159,6 +191,7 @@ module.exports = (0, createRule_1.createRule)({
159
191
  context.report({
160
192
  node,
161
193
  messageId: 'callbackFunctionPrefix',
194
+ data: { functionName },
162
195
  });
163
196
  return;
164
197
  }
@@ -169,6 +202,7 @@ module.exports = (0, createRule_1.createRule)({
169
202
  context.report({
170
203
  node,
171
204
  messageId: 'callbackFunctionPrefix',
205
+ data: { functionName },
172
206
  });
173
207
  return;
174
208
  }
@@ -210,7 +244,7 @@ module.exports = (0, createRule_1.createRule)({
210
244
  }
211
245
  }
212
246
  // Get references from global scope
213
- const sourceCode = context.getSourceCode();
247
+ const sourceCode = context.sourceCode;
214
248
  if (sourceCode.scopeManager?.globalScope) {
215
249
  const globalVar = sourceCode.scopeManager.globalScope.variables.find((v) => v.name === functionName);
216
250
  if (globalVar) {
@@ -220,6 +254,7 @@ module.exports = (0, createRule_1.createRule)({
220
254
  context.report({
221
255
  node,
222
256
  messageId: 'callbackFunctionPrefix',
257
+ data: { functionName },
223
258
  fix(fixer) {
224
259
  // Remove 'handle' prefix and convert first character to lowercase
225
260
  const newName = functionName.slice(6).charAt(0).toLowerCase() +
@@ -248,6 +283,7 @@ module.exports = (0, createRule_1.createRule)({
248
283
  context.report({
249
284
  node: node.key,
250
285
  messageId: 'callbackFunctionPrefix',
286
+ data: { functionName: name },
251
287
  });
252
288
  return;
253
289
  }
@@ -256,12 +292,14 @@ module.exports = (0, createRule_1.createRule)({
256
292
  context.report({
257
293
  node: node.key,
258
294
  messageId: 'callbackFunctionPrefix',
295
+ data: { functionName: name },
259
296
  });
260
297
  return;
261
298
  }
262
299
  context.report({
263
300
  node: node.key,
264
301
  messageId: 'callbackFunctionPrefix',
302
+ data: { functionName: name },
265
303
  fix(fixer) {
266
304
  // Remove 'handle' prefix and convert first character to lowercase
267
305
  const newName = name.slice(6).charAt(0).toLowerCase() + name.slice(7);
@@ -277,6 +315,7 @@ module.exports = (0, createRule_1.createRule)({
277
315
  context.report({
278
316
  node,
279
317
  messageId: 'callbackFunctionPrefix',
318
+ data: { functionName: node.parameter.name },
280
319
  });
281
320
  }
282
321
  },
@@ -1,4 +1,4 @@
1
1
  import { TSESLint } from '@typescript-eslint/utils';
2
- type MessageIds = 'dynamicHttpsErrors' | 'missingThirdArgument';
2
+ type MessageIds = 'dynamicHttpsErrors' | 'missingThirdArgument' | 'missingDetailsProperty' | 'missingDetailsDueToSpread' | 'unexpectedExtraArgumentForObjectCall';
3
3
  export declare const dynamicHttpsErrors: TSESLint.RuleModule<MessageIds, never[]>;
4
4
  export {};
@@ -4,81 +4,171 @@ exports.dynamicHttpsErrors = void 0;
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const utils_1 = require("@typescript-eslint/utils");
6
6
  const isHttpsErrorCall = (callee) => {
7
- if (callee.type === 'MemberExpression') {
8
- return (callee.object.type === 'Identifier' &&
7
+ if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression) {
8
+ return (callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
9
9
  callee.object.name === 'https' &&
10
- callee.property.type === 'Identifier' &&
10
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
11
11
  callee.property.name === 'HttpsError');
12
12
  }
13
- else if (callee.type === 'Identifier') {
13
+ else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
14
14
  return callee.name === 'HttpsError';
15
15
  }
16
16
  return false;
17
17
  };
18
+ const findPropertyByName = (properties, name) => properties.find((p) => p.type === utils_1.AST_NODE_TYPES.Property &&
19
+ !p.computed &&
20
+ ((p.key.type === utils_1.AST_NODE_TYPES.Identifier && p.key.name === name) ||
21
+ (p.key.type === utils_1.AST_NODE_TYPES.Literal && p.key.value === name)));
18
22
  exports.dynamicHttpsErrors = (0, createRule_1.createRule)({
19
23
  name: 'dynamic-https-errors',
20
24
  meta: {
21
25
  type: 'suggestion',
22
26
  docs: {
23
- description: 'Dynamic error details should only be in the third argument of the HttpsError constructor. The second argument is hashed to produce a unique id. All HttpsError constructor calls must include a third argument for contextual details.',
27
+ description: 'Keep HttpsError messages static and move request-specific details to the third argument so error identifiers remain stable and debugging context is preserved.',
24
28
  recommended: 'error',
25
29
  },
26
30
  schema: [],
27
31
  messages: {
28
- dynamicHttpsErrors: 'Found dynamic error details in the second argument of the HttpsError constructor - the "message" field. This field is hashed to produce a unique id for error monitoring. Move any dynamic details to the third argument - the "details" field - to preserve the unique id and to monitor the error correctly.',
29
- missingThirdArgument: "HttpsError constructor calls must include a third argument for contextual details. The third argument should contain relevant context that aids in debugging without affecting the error's unique identifier.",
32
+ dynamicHttpsErrors: 'The HttpsError message (second argument) must stay static. Template expressions here change the hashed message and explode the number of error ids for the same failure. Keep this argument constant and move interpolated values into the third "details" argument so monitoring groups the error while still capturing request context.',
33
+ missingThirdArgument: 'HttpsError calls must include a third "details" argument. The message (second argument) is hashed into a stable identifier, so omitting details leaves errors hard to debug and encourages packing variables into the hashed message. Provide a third argument with the request-specific context (object or string) to keep identifiers stable and diagnostics useful.',
34
+ missingDetailsProperty: 'HttpsError calls must include a "details" property. The message is hashed into a stable identifier, so omitting details leaves errors hard to debug and encourages packing variables into the hashed message. Provide a details property with the request-specific context (object or string) to keep identifiers stable and diagnostics useful.',
35
+ missingDetailsDueToSpread: 'HttpsError calls must include a "details" property. This call uses an object spread, which prevents static verification that "details" is present. Ensure the spread object contains "details" or provide it explicitly to keep identifiers stable and diagnostics useful.',
36
+ unexpectedExtraArgumentForObjectCall: 'Object-based HttpsError calls must have exactly one argument containing code, message, and details properties. Remove extra arguments or use the positional signature (code, message, details).',
30
37
  },
31
38
  },
32
39
  defaultOptions: [],
33
40
  create(context) {
41
+ const unwrapTSAssertions = (node) => {
42
+ let inner = node;
43
+ while (inner.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
44
+ inner.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
45
+ inner.type === utils_1.AST_NODE_TYPES.TSNonNullExpression ||
46
+ inner.type === utils_1.AST_NODE_TYPES.TSTypeAssertion) {
47
+ inner = inner.expression;
48
+ }
49
+ return inner;
50
+ };
51
+ // Only string concatenation with "+" can be static; all other operators
52
+ // are treated as dynamic to avoid hashing non-literal message content.
53
+ const isDynamicBinaryExpression = (expression) => {
54
+ if (expression.operator !== '+')
55
+ return true;
56
+ const isStaticLiteral = (expr) => {
57
+ const inner = unwrapTSAssertions(expr);
58
+ return (inner.type === utils_1.AST_NODE_TYPES.Literal &&
59
+ typeof inner.value === 'string');
60
+ };
61
+ const isSafe = (expr) => {
62
+ if (expr.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
63
+ return false;
64
+ }
65
+ const inner = unwrapTSAssertions(expr);
66
+ if (inner.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
67
+ return !isDynamicBinaryExpression(inner);
68
+ }
69
+ return isStaticLiteral(inner);
70
+ };
71
+ return !(isSafe(expression.left) && isSafe(expression.right));
72
+ };
73
+ /**
74
+ * Determines if a node should be validated for staticness.
75
+ *
76
+ * Pragmatic Exception: Identifier, MemberExpression, and ChainExpression nodes
77
+ * (e.g., `props.message`, `props?.message`, `ERROR_MSG`) are excluded from
78
+ * staticness validation. While they can be dynamic and may affect message
79
+ * stability, they are permitted to support common React/props patterns and
80
+ * constants, preserving developer ergonomics as an intentional trade-off.
81
+ */
82
+ const shouldValidateForStaticness = (node) => {
83
+ return (node.type !== utils_1.AST_NODE_TYPES.Identifier &&
84
+ node.type !== utils_1.AST_NODE_TYPES.MemberExpression &&
85
+ node.type !== utils_1.AST_NODE_TYPES.ChainExpression &&
86
+ node.type !== utils_1.AST_NODE_TYPES.SpreadElement &&
87
+ // Explicitly exclude patterns and other non-Expression nodes that can appear in Property.value
88
+ node.type !== utils_1.AST_NODE_TYPES.ArrayPattern &&
89
+ node.type !== utils_1.AST_NODE_TYPES.AssignmentPattern &&
90
+ node.type !== utils_1.AST_NODE_TYPES.ObjectPattern &&
91
+ node.type !== utils_1.AST_NODE_TYPES.RestElement &&
92
+ node.type !== utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression);
93
+ };
94
+ /**
95
+ * Checks if the message node is static.
96
+ *
97
+ * A message is considered static if it's a Literal (string), a TemplateLiteral with no expressions,
98
+ * or a BinaryExpression (string concatenation with '+') where all parts are static.
99
+ *
100
+ * This function reports `dynamicHttpsErrors` for all other expression types (CallExpression,
101
+ * ConditionalExpression, etc.) that reach it, except for those explicitly excluded by
102
+ * `shouldValidateForStaticness`.
103
+ */
104
+ const checkMessageIsStatic = (messageNode) => {
105
+ const currentNode = unwrapTSAssertions(messageNode);
106
+ if (currentNode.type === utils_1.AST_NODE_TYPES.Literal) {
107
+ return;
108
+ }
109
+ if (currentNode.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
110
+ currentNode.expressions.length === 0) {
111
+ return;
112
+ }
113
+ if (currentNode.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
114
+ if (isDynamicBinaryExpression(currentNode)) {
115
+ context.report({
116
+ node: messageNode,
117
+ messageId: 'dynamicHttpsErrors',
118
+ });
119
+ }
120
+ return;
121
+ }
122
+ // Catch-all for other dynamic forms (CallExpression, ConditionalExpression, etc.)
123
+ context.report({
124
+ node: messageNode,
125
+ messageId: 'dynamicHttpsErrors',
126
+ });
127
+ };
34
128
  const checkForHttpsError = (node) => {
35
129
  const callee = node.callee;
36
- if (isHttpsErrorCall(callee)) {
37
- // Check for missing third argument
38
- if (node.arguments.length < 3) {
130
+ if (!isHttpsErrorCall(callee))
131
+ return;
132
+ // Signature 1: Object-based constructor (HttpsErrorProps)
133
+ if (node.arguments.length >= 1 &&
134
+ node.arguments[0].type === utils_1.AST_NODE_TYPES.ObjectExpression) {
135
+ if (node.arguments.length > 1) {
39
136
  context.report({
40
137
  node,
41
- messageId: 'missingThirdArgument',
138
+ messageId: 'unexpectedExtraArgumentForObjectCall',
42
139
  });
43
140
  return;
44
141
  }
45
- // Check for dynamic content in second argument (existing functionality)
46
- const secondArg = node.arguments[1];
47
- if (!secondArg)
48
- return;
49
- if (secondArg.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
50
- secondArg.expressions.length > 0) {
142
+ const props = node.arguments[0];
143
+ const messageProperty = findPropertyByName(props.properties, 'message');
144
+ const detailsProperty = findPropertyByName(props.properties, 'details');
145
+ if (!detailsProperty) {
146
+ const hasSpread = props.properties.some((p) => p.type === utils_1.AST_NODE_TYPES.SpreadElement);
51
147
  context.report({
52
- node: secondArg,
53
- messageId: 'dynamicHttpsErrors',
148
+ node,
149
+ messageId: hasSpread
150
+ ? 'missingDetailsDueToSpread'
151
+ : 'missingDetailsProperty',
54
152
  });
55
- return;
56
153
  }
57
- // Only string concatenation with "+" can be static; all other operators
58
- // are treated as dynamic to avoid hashing non-literal message content.
59
- const isDynamicBinaryExpression = (expression) => {
60
- if (expression.operator !== '+')
61
- return true;
62
- const isStaticLiteral = (expr) => expr.type === utils_1.AST_NODE_TYPES.Literal &&
63
- typeof expr.value === 'string';
64
- const isSafe = (expr) => {
65
- if (expr.type === utils_1.AST_NODE_TYPES.PrivateIdentifier) {
66
- return false;
67
- }
68
- if (expr.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
69
- return !isDynamicBinaryExpression(expr);
70
- }
71
- return isStaticLiteral(expr);
72
- };
73
- return !(isSafe(expression.left) && isSafe(expression.right));
74
- };
75
- if (secondArg.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
76
- isDynamicBinaryExpression(secondArg)) {
77
- context.report({
78
- node: secondArg,
79
- messageId: 'dynamicHttpsErrors',
80
- });
154
+ if (messageProperty &&
155
+ shouldValidateForStaticness(messageProperty.value)) {
156
+ checkMessageIsStatic(messageProperty.value);
81
157
  }
158
+ return;
159
+ }
160
+ // Signature 2: Positional arguments (code, message, details)
161
+ // Check for missing third argument
162
+ if (node.arguments.length < 3) {
163
+ context.report({
164
+ node,
165
+ messageId: 'missingThirdArgument',
166
+ });
167
+ }
168
+ // Check for dynamic content in second argument
169
+ const secondArg = node.arguments[1];
170
+ if (secondArg && shouldValidateForStaticness(secondArg)) {
171
+ checkMessageIsStatic(secondArg);
82
172
  }
83
173
  };
84
174
  return {
@@ -34,7 +34,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
34
34
  * Helper function to add assertSafe import if needed
35
35
  */
36
36
  const addAssertSafeImport = (fixer) => {
37
- const program = context.getSourceCode().ast;
37
+ const program = context.sourceCode.ast;
38
38
  const firstImport = program.body.find((node) => node.type === utils_1.AST_NODE_TYPES.ImportDeclaration);
39
39
  const importStatement = `import { assertSafe } from '${importPath}';\n`;
40
40
  if (firstImport) {
@@ -83,7 +83,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
83
83
  key.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
84
84
  key.callee.name === 'String') {
85
85
  const arg = key.arguments[0];
86
- const argText = context.getSourceCode().getText(arg);
86
+ const argText = context.sourceCode.getText(arg);
87
87
  reportUseAssertSafe(key, argText);
88
88
  }
89
89
  // Check for template literals like `${id}`
@@ -93,7 +93,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
93
93
  key.quasis[0].value.raw === '' &&
94
94
  key.quasis[1].value.raw === '') {
95
95
  const expr = key.expressions[0];
96
- const exprText = context.getSourceCode().getText(expr);
96
+ const exprText = context.sourceCode.getText(expr);
97
97
  reportUseAssertSafe(key, exprText);
98
98
  }
99
99
  }
@@ -107,7 +107,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
107
107
  left.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
108
108
  left.callee.name === 'String') {
109
109
  const arg = left.arguments[0];
110
- const argText = context.getSourceCode().getText(arg);
110
+ const argText = context.sourceCode.getText(arg);
111
111
  reportUseAssertSafe(left, argText);
112
112
  }
113
113
  // Check for template literals like `${id}`
@@ -117,7 +117,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
117
117
  left.quasis[0].value.raw === '' &&
118
118
  left.quasis[1].value.raw === '') {
119
119
  const expr = left.expressions[0];
120
- const exprText = context.getSourceCode().getText(expr);
120
+ const exprText = context.sourceCode.getText(expr);
121
121
  reportUseAssertSafe(left, exprText);
122
122
  }
123
123
  }
@@ -158,7 +158,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
158
158
  property.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
159
159
  property.callee.name === 'String') {
160
160
  const arg = property.arguments[0];
161
- const argText = context.getSourceCode().getText(arg);
161
+ const argText = context.sourceCode.getText(arg);
162
162
  reportUseAssertSafe(property, argText);
163
163
  return;
164
164
  }
@@ -179,7 +179,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
179
179
  return;
180
180
  }
181
181
  const expr = property.expressions[0];
182
- const exprText = context.getSourceCode().getText(expr);
182
+ const exprText = context.sourceCode.getText(expr);
183
183
  reportUseAssertSafe(property, exprText);
184
184
  return;
185
185
  }
@@ -193,7 +193,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
193
193
  if (isLikelyArray) {
194
194
  return;
195
195
  }
196
- const propText = context.getSourceCode().getText(property);
196
+ const propText = context.sourceCode.getText(property);
197
197
  reportUseAssertSafe(property, propText);
198
198
  return;
199
199
  }
@@ -203,7 +203,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
203
203
  if (isLikelyArray) {
204
204
  return;
205
205
  }
206
- const propText = context.getSourceCode().getText(property);
206
+ const propText = context.sourceCode.getText(property);
207
207
  reportUseAssertSafe(property, propText);
208
208
  return;
209
209
  }
@@ -215,7 +215,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
215
215
  if (isLikelyArray) {
216
216
  return;
217
217
  }
218
- const propText = context.getSourceCode().getText(property);
218
+ const propText = context.sourceCode.getText(property);
219
219
  reportUseAssertSafe(property, propText);
220
220
  return;
221
221
  }
@@ -228,7 +228,7 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
228
228
  if (isLikelyArray) {
229
229
  return;
230
230
  }
231
- const propText = context.getSourceCode().getText(property);
231
+ const propText = context.sourceCode.getText(property);
232
232
  reportUseAssertSafe(property, propText);
233
233
  return;
234
234
  }