eslint-config-typed 4.2.1 → 4.3.1

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 (170) hide show
  1. package/README.md +4 -0
  2. package/dist/configs/immer.d.mts +3 -0
  3. package/dist/configs/immer.d.mts.map +1 -0
  4. package/dist/configs/immer.mjs +11 -0
  5. package/dist/configs/immer.mjs.map +1 -0
  6. package/dist/configs/index.d.mts +2 -0
  7. package/dist/configs/index.d.mts.map +1 -1
  8. package/dist/configs/index.mjs +2 -0
  9. package/dist/configs/index.mjs.map +1 -1
  10. package/dist/configs/plugins.d.mts +1 -1
  11. package/dist/configs/plugins.d.mts.map +1 -1
  12. package/dist/configs/plugins.mjs +2 -0
  13. package/dist/configs/plugins.mjs.map +1 -1
  14. package/dist/configs/ts-data-forge.d.mts +3 -0
  15. package/dist/configs/ts-data-forge.d.mts.map +1 -0
  16. package/dist/configs/ts-data-forge.mjs +11 -0
  17. package/dist/configs/ts-data-forge.mjs.map +1 -0
  18. package/dist/configs/typescript.d.mts.map +1 -1
  19. package/dist/configs/typescript.mjs +0 -2
  20. package/dist/configs/typescript.mjs.map +1 -1
  21. package/dist/entry-point.mjs +4 -0
  22. package/dist/entry-point.mjs.map +1 -1
  23. package/dist/index.mjs +4 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/plugins/index.d.mts +1 -0
  26. package/dist/plugins/index.d.mts.map +1 -1
  27. package/dist/plugins/index.mjs +1 -0
  28. package/dist/plugins/index.mjs.map +1 -1
  29. package/dist/plugins/ts-data-forge/index.d.mts +2 -0
  30. package/dist/plugins/ts-data-forge/index.d.mts.map +1 -0
  31. package/dist/plugins/ts-data-forge/index.mjs +2 -0
  32. package/dist/plugins/ts-data-forge/index.mjs.map +1 -0
  33. package/dist/plugins/ts-data-forge/plugin.d.mts +3 -0
  34. package/dist/plugins/ts-data-forge/plugin.d.mts.map +1 -0
  35. package/dist/plugins/ts-data-forge/plugin.mjs +8 -0
  36. package/dist/plugins/ts-data-forge/plugin.mjs.map +1 -0
  37. package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts +5 -0
  38. package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts.map +1 -0
  39. package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs +35 -0
  40. package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs.map +1 -0
  41. package/dist/plugins/ts-data-forge/rules/constants.d.mts +2 -0
  42. package/dist/plugins/ts-data-forge/rules/constants.d.mts.map +1 -0
  43. package/dist/plugins/ts-data-forge/rules/constants.mjs +4 -0
  44. package/dist/plugins/ts-data-forge/rules/constants.mjs.map +1 -0
  45. package/dist/plugins/ts-data-forge/rules/import-utils.d.mts +5 -0
  46. package/dist/plugins/ts-data-forge/rules/import-utils.d.mts.map +1 -0
  47. package/dist/plugins/ts-data-forge/rules/import-utils.mjs +31 -0
  48. package/dist/plugins/ts-data-forge/rules/import-utils.mjs.map +1 -0
  49. package/dist/plugins/ts-data-forge/rules/index.d.mts +2 -0
  50. package/dist/plugins/ts-data-forge/rules/index.d.mts.map +1 -0
  51. package/dist/plugins/ts-data-forge/rules/index.mjs +2 -0
  52. package/dist/plugins/ts-data-forge/rules/index.mjs.map +1 -0
  53. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts +6 -0
  54. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts.map +1 -0
  55. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs +94 -0
  56. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs.map +1 -0
  57. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts +6 -0
  58. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts.map +1 -0
  59. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs +95 -0
  60. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs.map +1 -0
  61. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts +6 -0
  62. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts.map +1 -0
  63. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs +72 -0
  64. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs.map +1 -0
  65. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts +6 -0
  66. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts.map +1 -0
  67. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs +71 -0
  68. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs.map +1 -0
  69. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts +6 -0
  70. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts.map +1 -0
  71. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs +183 -0
  72. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs.map +1 -0
  73. package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts +6 -0
  74. package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts.map +1 -0
  75. package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs +86 -0
  76. package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs.map +1 -0
  77. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts +6 -0
  78. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts.map +1 -0
  79. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs +103 -0
  80. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs.map +1 -0
  81. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts +6 -0
  82. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts.map +1 -0
  83. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs +105 -0
  84. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs.map +1 -0
  85. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts +6 -0
  86. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts.map +1 -0
  87. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs +130 -0
  88. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs.map +1 -0
  89. package/dist/plugins/ts-data-forge/rules/rules.d.mts +12 -0
  90. package/dist/plugins/ts-data-forge/rules/rules.d.mts.map +1 -0
  91. package/dist/plugins/ts-data-forge/rules/rules.mjs +24 -0
  92. package/dist/plugins/ts-data-forge/rules/rules.mjs.map +1 -0
  93. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.d.mts.map +1 -1
  94. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs +6 -8
  95. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs.map +1 -1
  96. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.d.mts.map +1 -1
  97. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs +11 -15
  98. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs.map +1 -1
  99. package/dist/rules/eslint-jest-rules.d.mts +3 -0
  100. package/dist/rules/eslint-jest-rules.d.mts.map +1 -1
  101. package/dist/rules/eslint-jest-rules.mjs +3 -0
  102. package/dist/rules/eslint-jest-rules.mjs.map +1 -1
  103. package/dist/rules/eslint-ts-data-forge-rules.d.mts +12 -0
  104. package/dist/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
  105. package/dist/rules/eslint-ts-data-forge-rules.mjs +14 -0
  106. package/dist/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
  107. package/dist/rules/eslint-vitest-rules.d.mts +2 -2
  108. package/dist/rules/eslint-vitest-rules.mjs +2 -2
  109. package/dist/rules/eslint-vitest-rules.mjs.map +1 -1
  110. package/dist/rules/index.d.mts +1 -0
  111. package/dist/rules/index.d.mts.map +1 -1
  112. package/dist/rules/index.mjs +1 -0
  113. package/dist/rules/index.mjs.map +1 -1
  114. package/dist/types/define-known-rules.d.mts +2 -2
  115. package/dist/types/define-known-rules.d.mts.map +1 -1
  116. package/dist/types/define-known-rules.mjs.map +1 -1
  117. package/dist/types/rules/eslint-jest-rules.d.mts +119 -67
  118. package/dist/types/rules/eslint-jest-rules.d.mts.map +1 -1
  119. package/dist/types/rules/eslint-ts-data-forge-rules.d.mts +147 -0
  120. package/dist/types/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
  121. package/dist/types/rules/eslint-ts-data-forge-rules.mjs +2 -0
  122. package/dist/types/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
  123. package/dist/types/rules/eslint-vitest-rules.d.mts +41 -20
  124. package/dist/types/rules/eslint-vitest-rules.d.mts.map +1 -1
  125. package/dist/types/rules/index.d.mts +1 -0
  126. package/dist/types/rules/index.d.mts.map +1 -1
  127. package/package.json +15 -15
  128. package/src/configs/immer.mts +9 -0
  129. package/src/configs/index.mts +2 -0
  130. package/src/configs/plugins.mts +3 -0
  131. package/src/configs/ts-data-forge.mts +11 -0
  132. package/src/configs/typescript.mts +0 -2
  133. package/src/plugins/index.mts +1 -0
  134. package/src/plugins/strict-dependencies/rules/resolve-import-path.test.mts +7 -9
  135. package/src/plugins/ts-data-forge/index.mts +1 -0
  136. package/src/plugins/ts-data-forge/plugin.mts +6 -0
  137. package/src/plugins/ts-data-forge/rules/branded-number-types.mts +36 -0
  138. package/src/plugins/ts-data-forge/rules/constants.mts +1 -0
  139. package/src/plugins/ts-data-forge/rules/import-utils.mts +56 -0
  140. package/src/plugins/ts-data-forge/rules/index.mts +1 -0
  141. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mts +140 -0
  142. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.test.mts +175 -0
  143. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mts +144 -0
  144. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.test.mts +183 -0
  145. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.mts +97 -0
  146. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.test.mts +62 -0
  147. package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mts +106 -0
  148. package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.test.mts +83 -0
  149. package/src/plugins/ts-data-forge/rules/prefer-arr-sum.mts +269 -0
  150. package/src/plugins/ts-data-forge/rules/prefer-arr-sum.test.mts +171 -0
  151. package/src/plugins/ts-data-forge/rules/prefer-as-int.mts +130 -0
  152. package/src/plugins/ts-data-forge/rules/prefer-as-int.test.mts +267 -0
  153. package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.mts +144 -0
  154. package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.test.mts +156 -0
  155. package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mts +158 -0
  156. package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.test.mts +191 -0
  157. package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.mts +181 -0
  158. package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.test.mts +156 -0
  159. package/src/plugins/ts-data-forge/rules/rules.mts +22 -0
  160. package/src/plugins/ts-restrictions/rules/check-destructuring-completeness.mts +6 -8
  161. package/src/plugins/ts-restrictions/rules/no-restricted-cast-name.mts +11 -15
  162. package/src/rules/eslint-jest-rules.mts +3 -0
  163. package/src/rules/eslint-ts-data-forge-rules.mts +13 -0
  164. package/src/rules/eslint-vitest-rules.mts +2 -2
  165. package/src/rules/index.mts +1 -0
  166. package/src/types/define-known-rules.mts +2 -0
  167. package/src/types/rules/eslint-jest-rules.mts +122 -67
  168. package/src/types/rules/eslint-ts-data-forge-rules.mts +156 -0
  169. package/src/types/rules/eslint-vitest-rules.mts +46 -21
  170. package/src/types/rules/index.mts +1 -0
@@ -0,0 +1,269 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { Arr, pipe } from 'ts-data-forge';
7
+ import { type TypeReference } from 'typescript';
8
+ import {
9
+ buildImportFixes,
10
+ getNamedImports,
11
+ getTsDataForgeImport,
12
+ } from './import-utils.mjs';
13
+
14
+ type Options = readonly [];
15
+
16
+ type MessageIds = 'useArrSum' | 'useArrSumBy';
17
+
18
+ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
19
+ meta: {
20
+ type: 'suggestion',
21
+ docs: {
22
+ description:
23
+ 'Replace `xs.reduce((a, b) => a + b, 0)` with `Arr.sum(xs)` or `Arr.sumBy(xs, fn)` from ts-data-forge.',
24
+ },
25
+ fixable: 'code',
26
+ schema: [],
27
+ messages: {
28
+ useArrSum: 'Replace with `Arr.sum({{arrayName}})` from ts-data-forge.',
29
+ useArrSumBy:
30
+ 'Replace with `Arr.sumBy({{arrayName}}, {{mapper}})` from ts-data-forge.',
31
+ },
32
+ },
33
+
34
+ create: (context) => {
35
+ const sourceCode = context.sourceCode;
36
+
37
+ const program = sourceCode.ast;
38
+
39
+ const tsDataForgeImport = getTsDataForgeImport(program);
40
+
41
+ const services = context.sourceCode.parserServices;
42
+
43
+ const mut_nodesToFix: {
44
+ node: TSESTree.CallExpression;
45
+ arrayExpression: TSESTree.Expression;
46
+ messageId: 'useArrSum' | 'useArrSumBy';
47
+ mapper?: string;
48
+ }[] = [];
49
+
50
+ return {
51
+ CallExpression: (node) => {
52
+ // Check for xs.reduce(...)
53
+ if (
54
+ node.callee.type !== AST_NODE_TYPES.MemberExpression ||
55
+ node.callee.property.type !== AST_NODE_TYPES.Identifier ||
56
+ node.callee.property.name !== 'reduce'
57
+ ) {
58
+ return;
59
+ }
60
+
61
+ const arrayExpression = node.callee.object;
62
+
63
+ // Check if we have 2 arguments: reducer function and initial value 0
64
+ if (!Arr.isArrayOfLength(node.arguments, 2)) return;
65
+
66
+ const reducer = node.arguments[0];
67
+
68
+ const initialValue = node.arguments[1];
69
+
70
+ // Check initial value is 0
71
+ if (
72
+ initialValue.type !== AST_NODE_TYPES.Literal ||
73
+ initialValue.value !== 0
74
+ ) {
75
+ return;
76
+ }
77
+
78
+ // Check if reducer is an arrow function with 2 parameters
79
+ if (
80
+ reducer.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
81
+ !Arr.isArrayOfLength(reducer.params, 2)
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ const [param1, param2] = reducer.params;
87
+
88
+ if (
89
+ param1.type !== AST_NODE_TYPES.Identifier ||
90
+ param2.type !== AST_NODE_TYPES.Identifier
91
+ ) {
92
+ return;
93
+ }
94
+
95
+ const body = reducer.body;
96
+
97
+ const checker = services?.program?.getTypeChecker();
98
+
99
+ if (checker === undefined) return;
100
+
101
+ // Case 1: (a, b) => a + b
102
+ if (
103
+ body.type === AST_NODE_TYPES.BinaryExpression &&
104
+ body.operator === '+' &&
105
+ body.left.type === AST_NODE_TYPES.Identifier &&
106
+ body.left.name === param1.name &&
107
+ body.right.type === AST_NODE_TYPES.Identifier &&
108
+ body.right.name === param2.name
109
+ ) {
110
+ // Check if arrayExpression type is number[] or compatible
111
+
112
+ const type = pipe(
113
+ services?.esTreeNodeToTSNodeMap?.get(arrayExpression),
114
+ ).mapNullable((tsNode) => checker.getTypeAtLocation(tsNode)).value;
115
+
116
+ if (
117
+ type !== undefined && // Check if it's an array type or tuple type
118
+ (checker.isArrayType(type) || checker.isTupleType(type))
119
+ ) {
120
+ // Get type arguments using the official API
121
+ // TypeReference has typeArguments that we can access
122
+ const typeArguments = checker.getTypeArguments(
123
+ // eslint-disable-next-line total-functions/no-unsafe-type-assertion
124
+ type as TypeReference,
125
+ );
126
+
127
+ if (Arr.isNonEmpty(typeArguments)) {
128
+ const elementType = typeArguments[0];
129
+
130
+ const numberType = checker.getNumberType();
131
+
132
+ // Check if element type is assignable to number
133
+ if (checker.isTypeAssignableTo(elementType, numberType)) {
134
+ mut_nodesToFix.push({
135
+ node,
136
+ arrayExpression,
137
+ messageId: 'useArrSum',
138
+ });
139
+ }
140
+ }
141
+ }
142
+
143
+ return;
144
+ }
145
+
146
+ // Case 2: (a, b) => a.prop + b.prop or a['prop'] + b['prop']
147
+ if (
148
+ body.type === AST_NODE_TYPES.BinaryExpression &&
149
+ body.operator === '+' &&
150
+ body.left.type === AST_NODE_TYPES.MemberExpression &&
151
+ body.right.type === AST_NODE_TYPES.MemberExpression
152
+ ) {
153
+ const leftObj = body.left.object;
154
+
155
+ const rightObj = body.right.object;
156
+
157
+ if (
158
+ leftObj.type !== AST_NODE_TYPES.Identifier ||
159
+ leftObj.name !== param1.name ||
160
+ rightObj.type !== AST_NODE_TYPES.Identifier ||
161
+ rightObj.name !== param2.name
162
+ ) {
163
+ return;
164
+ }
165
+
166
+ // Check if both access the same property
167
+ const leftProp = body.left.property;
168
+
169
+ const rightProp = body.right.property;
170
+
171
+ let mut_propName: string | undefined;
172
+
173
+ if (
174
+ leftProp.type === AST_NODE_TYPES.Identifier &&
175
+ !body.left.computed &&
176
+ rightProp.type === AST_NODE_TYPES.Identifier &&
177
+ !body.right.computed &&
178
+ leftProp.name === rightProp.name
179
+ ) {
180
+ mut_propName = leftProp.name;
181
+ } else if (
182
+ leftProp.type === AST_NODE_TYPES.Literal &&
183
+ body.left.computed &&
184
+ rightProp.type === AST_NODE_TYPES.Literal &&
185
+ body.right.computed &&
186
+ leftProp.value === rightProp.value &&
187
+ typeof leftProp.value === 'string'
188
+ ) {
189
+ mut_propName = leftProp.value;
190
+ }
191
+
192
+ if (mut_propName === undefined) return;
193
+
194
+ // Check if property type is number
195
+ if (services?.program !== undefined && services.program !== null) {
196
+ const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(body.left);
197
+
198
+ const tsRightProp = services.esTreeNodeToTSNodeMap?.get(body.right);
199
+
200
+ if (tsLeftProp !== undefined && tsRightProp !== undefined) {
201
+ const leftPropType = checker.getTypeAtLocation(tsLeftProp);
202
+
203
+ const rightPropType = checker.getTypeAtLocation(tsRightProp);
204
+
205
+ const numberType = checker.getNumberType();
206
+
207
+ // Check if property type is assignable to number
208
+ if (
209
+ !checker.isTypeAssignableTo(leftPropType, numberType) ||
210
+ !checker.isTypeAssignableTo(rightPropType, numberType)
211
+ ) {
212
+ return;
213
+ }
214
+ }
215
+ }
216
+
217
+ // Generate mapper function
218
+ const mapperParam = param1.name;
219
+
220
+ const mapper = body.left.computed
221
+ ? `${mapperParam} => ${mapperParam}['${mut_propName}']`
222
+ : `${mapperParam} => ${mapperParam}.${mut_propName}`;
223
+
224
+ mut_nodesToFix.push({
225
+ node,
226
+ arrayExpression,
227
+ messageId: 'useArrSumBy',
228
+ mapper,
229
+ });
230
+ }
231
+ },
232
+ 'Program:exit': () => {
233
+ const namedImports = getNamedImports(tsDataForgeImport);
234
+
235
+ const hasArrImport = namedImports.includes('Arr');
236
+
237
+ for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
238
+ const arrayText = sourceCode.getText(nodeInfo.arrayExpression);
239
+
240
+ context.report({
241
+ node: nodeInfo.node,
242
+ messageId: nodeInfo.messageId,
243
+ data: {
244
+ arrayName: arrayText,
245
+ mapper: nodeInfo.mapper ?? '',
246
+ },
247
+ fix: (fixer) => {
248
+ const replacement =
249
+ nodeInfo.messageId === 'useArrSum'
250
+ ? `Arr.sum(${arrayText})`
251
+ : `Arr.sumBy(${arrayText}, ${nodeInfo.mapper})`;
252
+
253
+ const importFixes =
254
+ index === 0 && !hasArrImport
255
+ ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
256
+ : [];
257
+
258
+ return [
259
+ ...importFixes,
260
+ fixer.replaceText(nodeInfo.node, replacement),
261
+ ];
262
+ },
263
+ });
264
+ }
265
+ },
266
+ };
267
+ },
268
+ defaultOptions: [],
269
+ };
@@ -0,0 +1,171 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferArrSum } from './prefer-arr-sum.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ projectService: {
13
+ allowDefaultProject: ['*.ts*'],
14
+ },
15
+ tsconfigRootDir: `${import.meta.dirname}/../..`,
16
+ },
17
+ },
18
+ });
19
+
20
+ describe('prefer-arr-sum', () => {
21
+ tester.run('prefer-arr-sum', preferArrSum, {
22
+ valid: [
23
+ {
24
+ name: 'ignores reduce with different operation',
25
+ code: dedent`
26
+ const xs = [1, 2, 3];
27
+ const result = xs.reduce((a, b) => a * b, 1);
28
+ `,
29
+ },
30
+ {
31
+ name: 'ignores reduce with non-zero initial value',
32
+ code: dedent`
33
+ const xs = [1, 2, 3];
34
+ const result = xs.reduce((a, b) => a + b, 10);
35
+ `,
36
+ },
37
+ {
38
+ name: 'ignores non-number array',
39
+ code: dedent`
40
+ const xs = [1, "2", "3"];
41
+ const result = xs.reduce((a, b) => a + b, 0);
42
+ `,
43
+ },
44
+ {
45
+ name: 'ignores non-number value array',
46
+ code: dedent`
47
+ const xs = [{ v: 1 }, { v: "2" }];
48
+ const sum = xs.reduce((a, b) => a.v + b.v, 0);
49
+ `,
50
+ },
51
+ ],
52
+ invalid: [
53
+ {
54
+ name: 'replaces xs.reduce((a, b) => a + b, 0) with Arr.sum',
55
+ code: dedent`
56
+ const xs: readonly number[] = [1, 2, 3];
57
+ const sum = xs.reduce((a, b) => a + b, 0);
58
+ `,
59
+ output: dedent`
60
+ import { Arr } from 'ts-data-forge';
61
+ const xs: readonly number[] = [1, 2, 3];
62
+ const sum = Arr.sum(xs);
63
+ `,
64
+ errors: [{ messageId: 'useArrSum' }],
65
+ },
66
+
67
+ ...([
68
+ {
69
+ name: 'no type annotation array',
70
+ code: dedent`
71
+ const xs = [1, 2, 3];
72
+ const sum = xs.reduce((a, b) => a + b, 0);
73
+ `,
74
+ output: dedent`
75
+ import { Arr } from 'ts-data-forge';
76
+ const xs = [1, 2, 3];
77
+ const sum = Arr.sum(xs);
78
+ `,
79
+ errors: [{ messageId: 'useArrSum' }],
80
+ },
81
+ {
82
+ name: 'no type annotation array with const assertion',
83
+ code: dedent`
84
+ const xs = [1, 2, 3] as const;
85
+ const sum = xs.reduce((a, b) => a + b, 0);
86
+ `,
87
+ output: dedent`
88
+ import { Arr } from 'ts-data-forge';
89
+ const xs = [1, 2, 3] as const;
90
+ const sum = Arr.sum(xs);
91
+ `,
92
+ errors: [{ messageId: 'useArrSum' }],
93
+ },
94
+ {
95
+ name: 'no type annotation array with satisfies operator',
96
+ code: dedent`
97
+ const xs = [1, 2, 3] as const satisfies readonly number[];
98
+ const sum = xs.reduce((a, b) => a + b, 0);
99
+ `,
100
+ output: dedent`
101
+ import { Arr } from 'ts-data-forge';
102
+ const xs = [1, 2, 3] as const satisfies readonly number[];
103
+ const sum = Arr.sum(xs);
104
+ `,
105
+ errors: [{ messageId: 'useArrSum' }],
106
+ },
107
+ ] as const),
108
+
109
+ ...([
110
+ {
111
+ name: 'replaces xs.reduce with property access with Arr.sumBy',
112
+ code: dedent`
113
+ const xs: readonly { v: number }[] = [{ v: 1 }, { v: 2 }];
114
+ const sum = xs.reduce((a, b) => a.v + b.v, 0);
115
+ `,
116
+ output: dedent`
117
+ import { Arr } from 'ts-data-forge';
118
+ const xs: readonly { v: number }[] = [{ v: 1 }, { v: 2 }];
119
+ const sum = Arr.sumBy(xs, a => a.v);
120
+ `,
121
+ errors: [{ messageId: 'useArrSumBy' }],
122
+ },
123
+ ...([
124
+ {
125
+ name: 'replaces xs.reduce with property access with Arr.sumBy',
126
+ code: dedent`
127
+ const xs = [{ v: 1 }, { v: 2 }] as const;
128
+ const sum = xs.reduce((a, b) => a.v + b.v, 0);
129
+ `,
130
+ output: dedent`
131
+ import { Arr } from 'ts-data-forge';
132
+ const xs = [{ v: 1 }, { v: 2 }] as const;
133
+ const sum = Arr.sumBy(xs, a => a.v);
134
+ `,
135
+ errors: [{ messageId: 'useArrSumBy' }],
136
+ },
137
+ ] as const),
138
+ {
139
+ name: 'replaces xs.reduce with bracket property access with Arr.sumBy',
140
+ code: dedent`
141
+ const xs: readonly { v: number }[] = [{ v: 1 }, { v: 2 }];
142
+ const sum = xs.reduce((a, b) => a['v'] + b['v'], 0);
143
+ `,
144
+ output: dedent`
145
+ import { Arr } from 'ts-data-forge';
146
+ const xs: readonly { v: number }[] = [{ v: 1 }, { v: 2 }];
147
+ const sum = Arr.sumBy(xs, a => a['v']);
148
+ `,
149
+ errors: [{ messageId: 'useArrSumBy' }],
150
+ },
151
+ ] as const),
152
+
153
+ {
154
+ name: 'keeps existing Arr import',
155
+ code: dedent`
156
+ import { Arr } from 'ts-data-forge';
157
+
158
+ const xs: readonly number[] = [1, 2, 3];
159
+ const sum = xs.reduce((a, b) => a + b, 0);
160
+ `,
161
+ output: dedent`
162
+ import { Arr } from 'ts-data-forge';
163
+
164
+ const xs: readonly number[] = [1, 2, 3];
165
+ const sum = Arr.sum(xs);
166
+ `,
167
+ errors: [{ messageId: 'useArrSum' }],
168
+ },
169
+ ],
170
+ });
171
+ }, 20000);
@@ -0,0 +1,130 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
7
+ import {
8
+ buildImportFixes,
9
+ getNamedImports,
10
+ getTsDataForgeImport,
11
+ } from './import-utils.mjs';
12
+
13
+ type Options = readonly [];
14
+
15
+ type MessageIds = 'useBrandedNumberCastFunction';
16
+
17
+ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description:
22
+ 'Replace branded number type assertions (e.g., `as Int`) with corresponding functions (e.g., `asInt()`) from ts-data-forge.',
23
+ },
24
+ fixable: 'code',
25
+ schema: [],
26
+ messages: {
27
+ useBrandedNumberCastFunction:
28
+ 'Replace `as {{typeName}}` with `{{functionName}}()` from ts-data-forge.',
29
+ },
30
+ },
31
+
32
+ create: (context) => {
33
+ const sourceCode = context.sourceCode;
34
+
35
+ const program = sourceCode.ast;
36
+
37
+ const tsDataForgeImport = getTsDataForgeImport(program);
38
+
39
+ const mut_nodesToFix: {
40
+ node: TSESTree.TSAsExpression;
41
+ typeName: string;
42
+ functionName: string;
43
+ }[] = [];
44
+
45
+ return {
46
+ TSAsExpression: (node) => {
47
+ const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
48
+
49
+ if (typeInfo === undefined) return;
50
+
51
+ mut_nodesToFix.push({
52
+ node,
53
+ typeName: typeInfo.typeName,
54
+ functionName: typeInfo.functionName,
55
+ });
56
+ },
57
+ 'Program:exit': () => {
58
+ const namedImports = getNamedImports(tsDataForgeImport);
59
+
60
+ // Group nodes by function name to handle imports efficiently
61
+ const mut_functionNameToNodes = new Map<
62
+ string,
63
+ Readonly<{
64
+ node: TSESTree.TSAsExpression;
65
+ typeName: string;
66
+ functionName: string;
67
+ }>[]
68
+ >();
69
+
70
+ for (const nodeInfo of mut_nodesToFix) {
71
+ const mut_nodes =
72
+ mut_functionNameToNodes.get(nodeInfo.functionName) ?? [];
73
+
74
+ mut_nodes.push(nodeInfo);
75
+
76
+ mut_functionNameToNodes.set(nodeInfo.functionName, mut_nodes);
77
+ }
78
+
79
+ // Process each group
80
+ for (const [functionName, nodes] of mut_functionNameToNodes) {
81
+ const hasImport = namedImports.includes(functionName);
82
+
83
+ for (const [index, { node, typeName }] of nodes.entries()) {
84
+ context.report({
85
+ node,
86
+ messageId: 'useBrandedNumberCastFunction',
87
+ data: {
88
+ typeName,
89
+ functionName,
90
+ },
91
+ fix: (fixer) => {
92
+ const replacement = `${functionName}(${sourceCode.getText(node.expression)})`;
93
+
94
+ // Add import only for the first node of this function and only if not already imported
95
+ const importFixes =
96
+ index === 0 && !hasImport
97
+ ? buildImportFixes(fixer, program, tsDataForgeImport, [
98
+ functionName,
99
+ ])
100
+ : [];
101
+
102
+ return [...importFixes, fixer.replaceText(node, replacement)];
103
+ },
104
+ });
105
+ }
106
+ }
107
+ },
108
+ };
109
+ },
110
+ defaultOptions: [],
111
+ };
112
+
113
+ const getBrandedNumberTypeInfo = (
114
+ typeAnnotation: DeepReadonly<TSESTree.TypeNode>,
115
+ ): Readonly<{ typeName: string; functionName: string }> | undefined => {
116
+ if (typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) return undefined;
117
+
118
+ if (typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier)
119
+ return undefined;
120
+
121
+ const typeName = typeAnnotation.typeName.name;
122
+
123
+ if (!brandedNumberTypeNameToFunctionName.has(typeName)) return undefined;
124
+
125
+ const functionName = brandedNumberTypeNameToFunctionName.get(typeName);
126
+
127
+ if (functionName === undefined) return undefined;
128
+
129
+ return { typeName, functionName };
130
+ };