@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ /**
3
+ * @fileoverview Enforce the use of event.params over .ref.parent.id in Firebase change handlers
4
+ * @author BluMint
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.preferParamsOverParentId = void 0;
8
+ const utils_1 = require("@typescript-eslint/utils");
9
+ const createRule_1 = require("../utils/createRule");
10
+ const HANDLER_TYPES = new Set([
11
+ 'DocumentChangeHandler',
12
+ 'DocumentChangeHandlerTransaction',
13
+ 'RealtimeDbChangeHandler',
14
+ 'RealtimeDbChangeHandlerTransaction',
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;
62
+ }
63
+ function isFirebaseChangeHandler(node) {
64
+ if (node.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
65
+ node.type !== utils_1.AST_NODE_TYPES.FunctionExpression &&
66
+ node.type !== utils_1.AST_NODE_TYPES.FunctionDeclaration) {
67
+ return false;
68
+ }
69
+ const typeAnnotation = findTypeAnnotationInContext(node);
70
+ if (!typeAnnotation) {
71
+ return false;
72
+ }
73
+ return checkTypeAnnotationForHandler(typeAnnotation.typeAnnotation);
74
+ }
75
+ function checkTypeAnnotationForHandler(typeNode) {
76
+ if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
77
+ typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
78
+ return HANDLER_TYPES.has(typeNode.typeName.name);
79
+ }
80
+ return false;
81
+ }
82
+ function isParentIdAccess(node) {
83
+ // Check if this is a .ref.parent[.parent...].id pattern
84
+ if (node.property.type !== utils_1.AST_NODE_TYPES.Identifier ||
85
+ node.property.name !== 'id') {
86
+ return { isMatch: false, depth: 0 };
87
+ }
88
+ const chain = [];
89
+ let current = node.object;
90
+ while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
91
+ if (current.property.type !== utils_1.AST_NODE_TYPES.Identifier) {
92
+ return { isMatch: false, depth: 0 };
93
+ }
94
+ chain.unshift(current.property.name);
95
+ current = current.object;
96
+ }
97
+ if (chain.length < 2) {
98
+ return { isMatch: false, depth: 0 };
99
+ }
100
+ const refIndex = chain.indexOf('ref');
101
+ if (refIndex === -1) {
102
+ return { isMatch: false, depth: 0 };
103
+ }
104
+ const parentSegment = chain.slice(refIndex + 1);
105
+ if (parentSegment.length === 0) {
106
+ return { isMatch: false, depth: 0 };
107
+ }
108
+ const invalidParent = parentSegment.some((segment) => segment !== 'parent');
109
+ if (invalidParent) {
110
+ return { isMatch: false, depth: 0 };
111
+ }
112
+ const depth = parentSegment.length;
113
+ return { isMatch: depth > 0, depth };
114
+ }
115
+ function findHandlerFunction(node) {
116
+ let current = node;
117
+ while (current) {
118
+ if (handlerFunctions.has(current)) {
119
+ return current;
120
+ }
121
+ current = current.parent;
122
+ }
123
+ return null;
124
+ }
125
+ function hasOptionalChaining(node) {
126
+ let current = node;
127
+ while (current && current.type === utils_1.AST_NODE_TYPES.MemberExpression) {
128
+ if (current.optional) {
129
+ return true;
130
+ }
131
+ current = current.object;
132
+ }
133
+ return false;
134
+ }
135
+ function isParamsInScope(handlerNode) {
136
+ // Check if params is destructured from the event parameter
137
+ if (handlerNode.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
138
+ handlerNode.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
139
+ handlerNode.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
140
+ const firstParam = handlerNode.params[0];
141
+ if (!firstParam)
142
+ return false;
143
+ // Check for destructuring pattern: ({ params }) or ({ data, params })
144
+ if (firstParam.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
145
+ return firstParam.properties.some((prop) => {
146
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
147
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
148
+ prop.key.name === 'params') {
149
+ return true;
150
+ }
151
+ return false;
152
+ });
153
+ }
154
+ // Check for variable declarations inside the function that destructure params
155
+ if (handlerNode.body &&
156
+ handlerNode.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
157
+ for (const statement of handlerNode.body.body) {
158
+ if (statement.type === utils_1.AST_NODE_TYPES.VariableDeclaration) {
159
+ for (const declarator of statement.declarations) {
160
+ if (declarator.id.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
161
+ declarator.init &&
162
+ declarator.init.type === utils_1.AST_NODE_TYPES.Identifier) {
163
+ // Check if destructuring from event parameter
164
+ const eventParamName = firstParam.type === utils_1.AST_NODE_TYPES.Identifier
165
+ ? firstParam.name
166
+ : 'event';
167
+ if (declarator.init.name === eventParamName) {
168
+ return declarator.id.properties.some((prop) => {
169
+ if (prop.type === utils_1.AST_NODE_TYPES.Property &&
170
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
171
+ prop.key.name === 'params') {
172
+ return true;
173
+ }
174
+ return false;
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ return false;
184
+ }
185
+ return {
186
+ // Track Firebase change handler functions
187
+ 'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(node) {
188
+ if (isFirebaseChangeHandler(node)) {
189
+ handlerFunctions.add(node);
190
+ }
191
+ },
192
+ // Detect .ref.parent.id patterns
193
+ MemberExpression(node) {
194
+ const parentAccess = isParentIdAccess(node);
195
+ if (parentAccess.isMatch) {
196
+ const handlerNode = findHandlerFunction(node);
197
+ if (handlerNode) {
198
+ 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`;
207
+ context.report({
208
+ node,
209
+ messageId: 'preferParams',
210
+ data: {
211
+ paramName: paramSuggestion,
212
+ },
213
+ fix: paramsInScope
214
+ ? (fixer) => {
215
+ const replacement = hasOptional
216
+ ? `params?.${paramSuggestion}`
217
+ : `params.${paramSuggestion}`;
218
+ return fixer.replaceText(node, replacement);
219
+ }
220
+ : undefined,
221
+ });
222
+ }
223
+ }
224
+ },
225
+ };
226
+ },
227
+ });
228
+ //# sourceMappingURL=prefer-params-over-parent-id.js.map
@@ -40,8 +40,8 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
40
40
  },
41
41
  ],
42
42
  messages: {
43
- tooManyParams: 'Function has too many parameters ({{count}}). Use a settings object instead.',
44
- sameTypeParams: 'Function has multiple parameters of the same type. Use a settings object instead.',
43
+ tooManyParams: "Function accepts {{count}} positional parameters (limit {{minimum}}). Long positional lists hide each argument's meaning and make call sites easy to mis-order. Pass a single settings object so callers name each field and keep the call readable.",
44
+ sameTypeParams: 'Function receives {{paramCount}} positional parameters including multiple "{{type}}" values. Repeated types in positional arguments invite swapped values and force callers to remember parameter order. Replace the positional list with a settings object so each value is labeled and self-documenting.',
45
45
  },
46
46
  },
47
47
  defaultOptions: [defaultOptions],
@@ -60,10 +60,12 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
60
60
  ? typeNode.typeName.name
61
61
  : 'unknown';
62
62
  // If there are type parameters, include them in the type signature
63
- if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) {
63
+ if (typeNode.typeParameters &&
64
+ typeNode.typeParameters.params.length > 0) {
65
+ const sourceCode = context.getSourceCode();
64
66
  const typeParams = typeNode.typeParameters.params
65
- .map(param => param.type)
66
- .join('_');
67
+ .map((param) => sourceCode.getText(param))
68
+ .join(', ');
67
69
  return `${typeName}<${typeParams}>`;
68
70
  }
69
71
  return typeName;
@@ -78,10 +80,12 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
78
80
  ? typeNode.typeName.name
79
81
  : 'unknown';
80
82
  // If there are type parameters, include them in the type signature
81
- if (typeNode.typeParameters && typeNode.typeParameters.params.length > 0) {
83
+ if (typeNode.typeParameters &&
84
+ typeNode.typeParameters.params.length > 0) {
85
+ const sourceCode = context.getSourceCode();
82
86
  const typeParams = typeNode.typeParameters.params
83
- .map(param => param.type)
84
- .join('_');
87
+ .map((param) => sourceCode.getText(param))
88
+ .join(', ');
85
89
  return `${typeName}<${typeParams}>`;
86
90
  }
87
91
  return typeName;
@@ -96,17 +100,17 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
96
100
  }
97
101
  return 'unknown';
98
102
  }
99
- function hasSameTypeParameters(params) {
103
+ function findDuplicateParameterType(params) {
100
104
  const typeMap = new Map();
101
105
  for (const param of params) {
102
106
  const type = getParameterType(param);
103
107
  const count = (typeMap.get(type) || 0) + 1;
104
108
  typeMap.set(type, count);
105
109
  if (count > 1) {
106
- return true;
110
+ return type;
107
111
  }
108
112
  }
109
- return false;
113
+ return null;
110
114
  }
111
115
  function isBuiltInOrThirdParty(node) {
112
116
  // Check if the node is part of a new expression (constructor call)
@@ -287,8 +291,10 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
287
291
  // Check if the function name or its type annotation suggests it's a handler with transaction
288
292
  if (functionName.includes('Transaction') ||
289
293
  functionName.includes('WithTransaction') ||
290
- (node.parent.id.typeAnnotation?.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
291
- node.parent.id.typeAnnotation.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
294
+ (node.parent.id.typeAnnotation?.typeAnnotation.type ===
295
+ utils_1.AST_NODE_TYPES.TSTypeReference &&
296
+ node.parent.id.typeAnnotation.typeAnnotation.typeName.type ===
297
+ utils_1.AST_NODE_TYPES.Identifier &&
292
298
  (node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('Transaction') ||
293
299
  node.parent.id.typeAnnotation.typeAnnotation.typeName.name.includes('WithTransaction')))) {
294
300
  return true;
@@ -306,16 +312,18 @@ exports.preferSettingsObject = (0, createRule_1.createRule)({
306
312
  context.report({
307
313
  node,
308
314
  messageId: 'tooManyParams',
309
- data: { count: params.length },
315
+ data: { count: params.length, minimum: minParams },
310
316
  });
311
317
  return;
312
318
  }
313
319
  // Then check for same type parameters if enabled
314
320
  if (finalOptions.checkSameTypeParameters && params.length >= 2) {
315
- if (hasSameTypeParameters(params)) {
321
+ const duplicateType = findDuplicateParameterType(params);
322
+ if (duplicateType) {
316
323
  context.report({
317
324
  node,
318
325
  messageId: 'sameTypeParams',
326
+ data: { type: duplicateType, paramCount: params.length },
319
327
  });
320
328
  }
321
329
  }
@@ -0,0 +1,3 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export type MessageIds = 'preferTypeAlias' | 'defineTypeBeforeConstant';
3
+ export declare const preferTypeAliasOverTypeofConstant: TSESLint.RuleModule<MessageIds, never[]>;
@@ -0,0 +1,259 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preferTypeAliasOverTypeofConstant = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const PAREN_TYPE = utils_1.AST_NODE_TYPES.TSParenthesizedType ??
7
+ 'TSParenthesizedType';
8
+ const isParenthesizedType = (node) => {
9
+ return node.type === PAREN_TYPE;
10
+ };
11
+ /** Utility to convert CONSTANT_CASE or snake/kebab to PascalCase */
12
+ function toPascalCase(name) {
13
+ return name
14
+ .split(/[_-]+/)
15
+ .filter(Boolean)
16
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
17
+ .join('');
18
+ }
19
+ /** Determine if a value initializer represents a function or class (which we should ignore). */
20
+ function isFunctionOrClassInitializer(init) {
21
+ if (!init)
22
+ return false;
23
+ return (init.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
24
+ init.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
25
+ init.type === utils_1.AST_NODE_TYPES.ClassExpression);
26
+ }
27
+ /** Determine if node is a simple constant literal or object/array literal possibly with `as const` */
28
+ function isConstantLikeInitializer(init) {
29
+ if (!init)
30
+ return false;
31
+ if (isFunctionOrClassInitializer(init))
32
+ return false;
33
+ switch (init.type) {
34
+ case utils_1.AST_NODE_TYPES.Literal:
35
+ return true;
36
+ case utils_1.AST_NODE_TYPES.TemplateLiteral:
37
+ return init.expressions.length === 0;
38
+ case utils_1.AST_NODE_TYPES.ObjectExpression:
39
+ return true;
40
+ case utils_1.AST_NODE_TYPES.ArrayExpression:
41
+ return true;
42
+ case utils_1.AST_NODE_TYPES.TSAsExpression:
43
+ return isConstantLikeInitializer(init.expression);
44
+ case utils_1.AST_NODE_TYPES.UnaryExpression: {
45
+ // treat unary constants (e.g., -1) as constant-like; exclude typeof
46
+ if (init.operator === 'typeof')
47
+ return false;
48
+ return isConstantLikeInitializer(init.argument);
49
+ }
50
+ default:
51
+ return false;
52
+ }
53
+ }
54
+ /** Traverse a TSType and collect referenced identifier names (e.g., A, B in A | B). */
55
+ function collectReferencedTypeNames(node, acc = new Set()) {
56
+ switch (node.type) {
57
+ case utils_1.AST_NODE_TYPES.TSTypeReference: {
58
+ if (node.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
59
+ acc.add(node.typeName.name);
60
+ }
61
+ if (node.typeParameters) {
62
+ for (const p of node.typeParameters.params)
63
+ collectReferencedTypeNames(p, acc);
64
+ }
65
+ break;
66
+ }
67
+ case utils_1.AST_NODE_TYPES.TSUnionType:
68
+ case utils_1.AST_NODE_TYPES.TSIntersectionType: {
69
+ for (const t of node.types)
70
+ collectReferencedTypeNames(t, acc);
71
+ break;
72
+ }
73
+ case utils_1.AST_NODE_TYPES.TSArrayType: {
74
+ const arr = node;
75
+ collectReferencedTypeNames(arr.elementType, acc);
76
+ break;
77
+ }
78
+ case utils_1.AST_NODE_TYPES.TSTypeOperator: {
79
+ const op = node;
80
+ if (op.typeAnnotation)
81
+ collectReferencedTypeNames(op.typeAnnotation, acc);
82
+ break;
83
+ }
84
+ case utils_1.AST_NODE_TYPES.TSTupleType: {
85
+ const tup = node;
86
+ for (const e of tup.elementTypes)
87
+ collectReferencedTypeNames(e, acc);
88
+ break;
89
+ }
90
+ default: {
91
+ if (isParenthesizedType(node)) {
92
+ collectReferencedTypeNames(node.typeAnnotation, acc);
93
+ }
94
+ break;
95
+ }
96
+ }
97
+ return acc;
98
+ }
99
+ /** Collects module-level consts and type aliases for quick lookup */
100
+ function collectTopLevelContext(program) {
101
+ const topLevelConstInitByName = new Map();
102
+ const topLevelConstNodeByName = new Map();
103
+ const typeAliasByName = new Map();
104
+ const importedValueNames = new Set();
105
+ const importedTypeNames = new Set();
106
+ for (const stmt of program.body) {
107
+ if (stmt.type === utils_1.AST_NODE_TYPES.ImportDeclaration) {
108
+ const isTypeImport = stmt.importKind === 'type';
109
+ for (const spec of stmt.specifiers) {
110
+ // import type { Foo } from '...'
111
+ // import { Foo } from '...' as value import
112
+ if (spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier) {
113
+ if (isTypeImport || spec.importKind === 'type') {
114
+ importedTypeNames.add(spec.local.name);
115
+ }
116
+ else {
117
+ importedValueNames.add(spec.local.name);
118
+ }
119
+ }
120
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier) {
121
+ if (isTypeImport) {
122
+ importedTypeNames.add(spec.local.name);
123
+ }
124
+ else {
125
+ importedValueNames.add(spec.local.name);
126
+ }
127
+ }
128
+ else if (spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier) {
129
+ // Namespace imports expose names via MemberExpression; not tracked individually
130
+ }
131
+ }
132
+ }
133
+ else if (stmt.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
134
+ stmt.kind === 'const') {
135
+ for (const decl of stmt.declarations) {
136
+ if (decl.id.type === utils_1.AST_NODE_TYPES.Identifier) {
137
+ topLevelConstInitByName.set(decl.id.name, decl.init ?? null);
138
+ topLevelConstNodeByName.set(decl.id.name, decl);
139
+ }
140
+ }
141
+ }
142
+ else if (stmt.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
143
+ typeAliasByName.set(stmt.id.name, stmt);
144
+ }
145
+ else if (stmt.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
146
+ stmt.declaration) {
147
+ // Handle exported declarations: export const FOO = ...; export type Foo = ...
148
+ const decl = stmt.declaration;
149
+ if (decl.type === utils_1.AST_NODE_TYPES.VariableDeclaration &&
150
+ decl.kind === 'const') {
151
+ for (const d of decl.declarations) {
152
+ if (d.id.type === utils_1.AST_NODE_TYPES.Identifier) {
153
+ topLevelConstInitByName.set(d.id.name, d.init ?? null);
154
+ topLevelConstNodeByName.set(d.id.name, d);
155
+ }
156
+ }
157
+ }
158
+ else if (decl.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
159
+ typeAliasByName.set(decl.id.name, decl);
160
+ }
161
+ }
162
+ }
163
+ return {
164
+ topLevelConstInitByName,
165
+ topLevelConstNodeByName,
166
+ typeAliasByName,
167
+ importedValueNames,
168
+ importedTypeNames,
169
+ };
170
+ }
171
+ exports.preferTypeAliasOverTypeofConstant = (0, createRule_1.createRule)({
172
+ name: 'prefer-type-alias-over-typeof-constant',
173
+ meta: {
174
+ type: 'suggestion',
175
+ docs: {
176
+ description: 'Prefer named type aliases over `typeof` on same-file global constants; ensure types are declared before constants.',
177
+ recommended: 'error',
178
+ },
179
+ hasSuggestions: false,
180
+ schema: [],
181
+ messages: {
182
+ preferTypeAlias: 'Type derived from same-file constant "{{constName}}" couples the type to its runtime value and scatters literal unions across the file. Create a named alias such as "{{suggested}}" and reference that alias instead of `typeof {{constName}}` so the type stays stable even if the value changes.',
183
+ defineTypeBeforeConstant: 'Type alias "{{typeName}}" appears after constant "{{constName}}", which hides the shape from readers and risks using an undeclared alias. Declare "{{typeName}}" before "{{constName}}" so the type is visible where it is consumed and can be reused consistently.',
184
+ },
185
+ },
186
+ defaultOptions: [],
187
+ create(context) {
188
+ let collected;
189
+ return {
190
+ Program(program) {
191
+ collected = collectTopLevelContext(program);
192
+ },
193
+ // Enforce that type aliases referenced by const annotations appear before the const
194
+ VariableDeclarator(node) {
195
+ if (!collected)
196
+ return;
197
+ if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
198
+ return;
199
+ const constName = node.id.name;
200
+ // Only for top-level consts
201
+ const decl = collected.topLevelConstNodeByName.get(constName);
202
+ if (!decl || decl !== node)
203
+ return;
204
+ const parentDecl = node.parent;
205
+ if (!parentDecl || parentDecl.kind !== 'const')
206
+ return;
207
+ const typeAnn = node.id.typeAnnotation?.typeAnnotation;
208
+ if (!typeAnn)
209
+ return;
210
+ for (const typeName of collectReferencedTypeNames(typeAnn)) {
211
+ // If imported type, allow any order
212
+ if (collected.importedTypeNames.has(typeName))
213
+ continue;
214
+ const typeDecl = collected.typeAliasByName.get(typeName);
215
+ if (typeDecl && typeDecl.range[0] > parentDecl.range[0]) {
216
+ context.report({
217
+ node: node.id,
218
+ messageId: 'defineTypeBeforeConstant',
219
+ data: { typeName, constName },
220
+ });
221
+ }
222
+ }
223
+ },
224
+ // Core rule: flag `typeof CONST` when CONST is a same-file top-level const and constant-like
225
+ TSTypeQuery(node) {
226
+ if (!collected)
227
+ return;
228
+ // Skip `keyof typeof X`
229
+ if (node.parent &&
230
+ node.parent.type === utils_1.AST_NODE_TYPES.TSTypeOperator &&
231
+ node.parent.operator === 'keyof') {
232
+ return;
233
+ }
234
+ if (node.exprName.type !== utils_1.AST_NODE_TYPES.Identifier) {
235
+ return;
236
+ }
237
+ const name = node.exprName.name;
238
+ // Imported values: do not enforce (may suggest in future)
239
+ if (collected.importedValueNames.has(name)) {
240
+ return;
241
+ }
242
+ const init = collected.topLevelConstInitByName.get(name);
243
+ if (init === undefined) {
244
+ // Not a top-level const in this file
245
+ return;
246
+ }
247
+ if (!isConstantLikeInitializer(init)) {
248
+ return;
249
+ }
250
+ context.report({
251
+ node,
252
+ messageId: 'preferTypeAlias',
253
+ data: { constName: name, suggested: toPascalCase(name) },
254
+ });
255
+ },
256
+ };
257
+ },
258
+ });
259
+ //# sourceMappingURL=prefer-type-alias-over-typeof-constant.js.map
@@ -0,0 +1,2 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const preferUrlToStringOverToJson: TSESLint.RuleModule<'preferToString', never[]>;