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.
- package/README.md +4 -0
- package/dist/configs/immer.d.mts +3 -0
- package/dist/configs/immer.d.mts.map +1 -0
- package/dist/configs/immer.mjs +11 -0
- package/dist/configs/immer.mjs.map +1 -0
- package/dist/configs/index.d.mts +2 -0
- package/dist/configs/index.d.mts.map +1 -1
- package/dist/configs/index.mjs +2 -0
- package/dist/configs/index.mjs.map +1 -1
- package/dist/configs/plugins.d.mts +1 -1
- package/dist/configs/plugins.d.mts.map +1 -1
- package/dist/configs/plugins.mjs +2 -0
- package/dist/configs/plugins.mjs.map +1 -1
- package/dist/configs/ts-data-forge.d.mts +3 -0
- package/dist/configs/ts-data-forge.d.mts.map +1 -0
- package/dist/configs/ts-data-forge.mjs +11 -0
- package/dist/configs/ts-data-forge.mjs.map +1 -0
- package/dist/configs/typescript.d.mts.map +1 -1
- package/dist/configs/typescript.mjs +0 -2
- package/dist/configs/typescript.mjs.map +1 -1
- package/dist/entry-point.mjs +4 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/index.mjs +4 -0
- package/dist/index.mjs.map +1 -1
- package/dist/plugins/index.d.mts +1 -0
- package/dist/plugins/index.d.mts.map +1 -1
- package/dist/plugins/index.mjs +1 -0
- package/dist/plugins/index.mjs.map +1 -1
- package/dist/plugins/ts-data-forge/index.d.mts +2 -0
- package/dist/plugins/ts-data-forge/index.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/index.mjs +2 -0
- package/dist/plugins/ts-data-forge/index.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/plugin.d.mts +3 -0
- package/dist/plugins/ts-data-forge/plugin.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/plugin.mjs +8 -0
- package/dist/plugins/ts-data-forge/plugin.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts +5 -0
- package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs +35 -0
- package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/constants.d.mts +2 -0
- package/dist/plugins/ts-data-forge/rules/constants.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/constants.mjs +4 -0
- package/dist/plugins/ts-data-forge/rules/constants.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/import-utils.d.mts +5 -0
- package/dist/plugins/ts-data-forge/rules/import-utils.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/import-utils.mjs +31 -0
- package/dist/plugins/ts-data-forge/rules/import-utils.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/index.d.mts +2 -0
- package/dist/plugins/ts-data-forge/rules/index.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/index.mjs +2 -0
- package/dist/plugins/ts-data-forge/rules/index.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs +94 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs +95 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs +72 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs +71 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs +183 -0
- package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs +86 -0
- package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs +103 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs +105 -0
- package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts +6 -0
- package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs +130 -0
- package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs.map +1 -0
- package/dist/plugins/ts-data-forge/rules/rules.d.mts +12 -0
- package/dist/plugins/ts-data-forge/rules/rules.d.mts.map +1 -0
- package/dist/plugins/ts-data-forge/rules/rules.mjs +24 -0
- package/dist/plugins/ts-data-forge/rules/rules.mjs.map +1 -0
- package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.d.mts.map +1 -1
- package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs +6 -8
- package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs.map +1 -1
- package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.d.mts.map +1 -1
- package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs +11 -15
- package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs.map +1 -1
- package/dist/rules/eslint-jest-rules.d.mts +3 -0
- package/dist/rules/eslint-jest-rules.d.mts.map +1 -1
- package/dist/rules/eslint-jest-rules.mjs +3 -0
- package/dist/rules/eslint-jest-rules.mjs.map +1 -1
- package/dist/rules/eslint-ts-data-forge-rules.d.mts +12 -0
- package/dist/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
- package/dist/rules/eslint-ts-data-forge-rules.mjs +14 -0
- package/dist/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
- package/dist/rules/eslint-vitest-rules.d.mts +2 -2
- package/dist/rules/eslint-vitest-rules.mjs +2 -2
- package/dist/rules/eslint-vitest-rules.mjs.map +1 -1
- package/dist/rules/index.d.mts +1 -0
- package/dist/rules/index.d.mts.map +1 -1
- package/dist/rules/index.mjs +1 -0
- package/dist/rules/index.mjs.map +1 -1
- package/dist/types/define-known-rules.d.mts +2 -2
- package/dist/types/define-known-rules.d.mts.map +1 -1
- package/dist/types/define-known-rules.mjs.map +1 -1
- package/dist/types/rules/eslint-jest-rules.d.mts +119 -67
- package/dist/types/rules/eslint-jest-rules.d.mts.map +1 -1
- package/dist/types/rules/eslint-ts-data-forge-rules.d.mts +147 -0
- package/dist/types/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
- package/dist/types/rules/eslint-ts-data-forge-rules.mjs +2 -0
- package/dist/types/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
- package/dist/types/rules/eslint-vitest-rules.d.mts +41 -20
- package/dist/types/rules/eslint-vitest-rules.d.mts.map +1 -1
- package/dist/types/rules/index.d.mts +1 -0
- package/dist/types/rules/index.d.mts.map +1 -1
- package/package.json +15 -15
- package/src/configs/immer.mts +9 -0
- package/src/configs/index.mts +2 -0
- package/src/configs/plugins.mts +3 -0
- package/src/configs/ts-data-forge.mts +11 -0
- package/src/configs/typescript.mts +0 -2
- package/src/plugins/index.mts +1 -0
- package/src/plugins/strict-dependencies/rules/resolve-import-path.test.mts +7 -9
- package/src/plugins/ts-data-forge/index.mts +1 -0
- package/src/plugins/ts-data-forge/plugin.mts +6 -0
- package/src/plugins/ts-data-forge/rules/branded-number-types.mts +36 -0
- package/src/plugins/ts-data-forge/rules/constants.mts +1 -0
- package/src/plugins/ts-data-forge/rules/import-utils.mts +56 -0
- package/src/plugins/ts-data-forge/rules/index.mts +1 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mts +140 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.test.mts +175 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mts +144 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.test.mts +183 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.mts +97 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.test.mts +62 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mts +106 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.test.mts +83 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-sum.mts +269 -0
- package/src/plugins/ts-data-forge/rules/prefer-arr-sum.test.mts +171 -0
- package/src/plugins/ts-data-forge/rules/prefer-as-int.mts +130 -0
- package/src/plugins/ts-data-forge/rules/prefer-as-int.test.mts +267 -0
- package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.mts +144 -0
- package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.test.mts +156 -0
- package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mts +158 -0
- package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.test.mts +191 -0
- package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.mts +181 -0
- package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.test.mts +156 -0
- package/src/plugins/ts-data-forge/rules/rules.mts +22 -0
- package/src/plugins/ts-restrictions/rules/check-destructuring-completeness.mts +6 -8
- package/src/plugins/ts-restrictions/rules/no-restricted-cast-name.mts +11 -15
- package/src/rules/eslint-jest-rules.mts +3 -0
- package/src/rules/eslint-ts-data-forge-rules.mts +13 -0
- package/src/rules/eslint-vitest-rules.mts +2 -2
- package/src/rules/index.mts +1 -0
- package/src/types/define-known-rules.mts +2 -0
- package/src/types/rules/eslint-jest-rules.mts +122 -67
- package/src/types/rules/eslint-ts-data-forge-rules.mts +156 -0
- package/src/types/rules/eslint-vitest-rules.mts +46 -21
- 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
|
+
};
|