eslint-plugin-ts-data-forge 0.8.2 → 0.8.4
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/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +3 -3
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +59 -3
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +119 -8
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs +22 -11
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +1 -1
- package/dist/rules/no-side-effect-import.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +125 -79
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +11 -4
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +5 -19
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +11 -27
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -22
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -22
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +7 -22
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts +6 -5
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +33 -13
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +51 -37
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +33 -10
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +18 -12
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +29 -14
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +34 -13
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +10 -4
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs +25 -3
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +28 -10
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +8 -3
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +18 -7
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +15 -8
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +13 -7
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +7 -7
- package/src/index.mts +1 -1
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +3 -3
- package/src/rules/ast-utils.mts +156 -13
- package/src/rules/import-utils.mts +35 -15
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/no-unnecessary-type-guard.mts +192 -124
- package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
- package/src/rules/prefer-arr-is-array.mts +11 -6
- package/src/rules/prefer-arr-is-array.test.mts +40 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +13 -25
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +18 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +15 -26
- package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +15 -26
- package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -25
- package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
- package/src/rules/prefer-arr-scan.mts +55 -19
- package/src/rules/prefer-arr-scan.test.mts +149 -0
- package/src/rules/prefer-arr-sum.mts +78 -47
- package/src/rules/prefer-arr-sum.test.mts +97 -0
- package/src/rules/prefer-arr-uniq.mts +46 -12
- package/src/rules/prefer-arr-uniq.test.mts +71 -0
- package/src/rules/prefer-as-int.mts +24 -16
- package/src/rules/prefer-as-int.test.mts +51 -0
- package/src/rules/prefer-canonical-array-slicing.mts +38 -15
- package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
- package/src/rules/prefer-canonical-length-cast.mts +45 -15
- package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
- package/src/rules/prefer-canonical-length-guard.mts +13 -7
- package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +33 -6
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
- package/src/rules/prefer-is-non-null-object.mts +31 -11
- package/src/rules/prefer-is-non-null-object.test.mts +42 -0
- package/src/rules/prefer-is-record-and-has-key.mts +10 -5
- package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
- package/src/rules/prefer-num-safe-parse-float.mts +24 -11
- package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
- package/src/rules/prefer-num-safe-parse-int.mts +25 -11
- package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +1 -1
- package/src/rules/prefer-range-for-loop.mts +18 -8
- package/src/rules/prefer-range-for-loop.test.mts +75 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { Arr, pipe } from 'ts-data-forge';
|
|
7
|
-
import {
|
|
7
|
+
import type { TypeReference } from 'typescript';
|
|
8
|
+
import {
|
|
9
|
+
skipTypeWrappers,
|
|
10
|
+
toArgumentText,
|
|
11
|
+
typeWrapperLayers,
|
|
12
|
+
} from './ast-utils.mjs';
|
|
8
13
|
import {
|
|
9
14
|
buildImportFixes,
|
|
10
15
|
getNamedImports,
|
|
@@ -50,8 +55,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
50
55
|
return {
|
|
51
56
|
CallExpression: (node) => {
|
|
52
57
|
// Check for xs.reduce(...)
|
|
58
|
+
// (`xs[reduce]` calls whatever method the variable `reduce` names)
|
|
53
59
|
if (
|
|
54
60
|
node.callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
61
|
+
node.callee.computed ||
|
|
55
62
|
node.callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
56
63
|
node.callee.property.name !== 'reduce'
|
|
57
64
|
) {
|
|
@@ -67,7 +74,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
67
74
|
|
|
68
75
|
const reducer = node.arguments[0];
|
|
69
76
|
|
|
70
|
-
|
|
77
|
+
// The initial value and the operands of the body are read through
|
|
78
|
+
// type wrappers: `0 as number` is still 0, `(b satisfies number)` is
|
|
79
|
+
// still `b`. Types are asked of the values underneath too.
|
|
80
|
+
const initialValue = skipTypeWrappers(node.arguments[1]);
|
|
71
81
|
|
|
72
82
|
// Check initial value is 0
|
|
73
83
|
if (
|
|
@@ -102,25 +112,43 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
102
112
|
return;
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
// Case 1: (a, b) => a + b
|
|
106
115
|
if (
|
|
107
|
-
body.type
|
|
108
|
-
body.operator
|
|
109
|
-
body.left.type === AST_NODE_TYPES.Identifier &&
|
|
110
|
-
body.left.name === param1.name &&
|
|
111
|
-
body.right.type === AST_NODE_TYPES.Identifier &&
|
|
112
|
-
body.right.name === param2.name
|
|
116
|
+
body.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
117
|
+
body.operator !== '+'
|
|
113
118
|
) {
|
|
114
|
-
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
115
121
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
const left = skipTypeWrappers(body.left);
|
|
123
|
+
|
|
124
|
+
const right = skipTypeWrappers(body.right);
|
|
125
|
+
|
|
126
|
+
// Case 1: (a, b) => a + b
|
|
127
|
+
if (
|
|
128
|
+
left.type === AST_NODE_TYPES.Identifier &&
|
|
129
|
+
left.name === param1.name &&
|
|
130
|
+
right.type === AST_NODE_TYPES.Identifier &&
|
|
131
|
+
right.name === param2.name
|
|
132
|
+
) {
|
|
133
|
+
// Check if arrayExpression type is number[] or compatible at every
|
|
134
|
+
// layer of type wrappers: a cast does not turn strings, which `+`
|
|
135
|
+
// concatenates, into numbers.
|
|
136
|
+
const isNumberArray = (
|
|
137
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
138
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
139
|
+
expression: TSESTree.Node,
|
|
140
|
+
): boolean => {
|
|
141
|
+
const type = pipe(
|
|
142
|
+
services?.esTreeNodeToTSNodeMap?.get(expression),
|
|
143
|
+
).mapNullable((tsNode) => checker.getTypeAtLocation(tsNode)).value;
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
type === undefined || // Check if it's an array type or tuple type
|
|
147
|
+
(!checker.isArrayType(type) && !checker.isTupleType(type))
|
|
148
|
+
) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
119
151
|
|
|
120
|
-
if (
|
|
121
|
-
type !== undefined && // Check if it's an array type or tuple type
|
|
122
|
-
(checker.isArrayType(type) || checker.isTupleType(type))
|
|
123
|
-
) {
|
|
124
152
|
// Get type arguments using the official API
|
|
125
153
|
// TypeReference has typeArguments that we can access
|
|
126
154
|
const typeArguments = checker.getTypeArguments(
|
|
@@ -128,20 +156,22 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
128
156
|
type as TypeReference,
|
|
129
157
|
);
|
|
130
158
|
|
|
131
|
-
if
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
159
|
+
// Check if element type is assignable to number
|
|
160
|
+
return (
|
|
161
|
+
Arr.isNonEmpty(typeArguments) &&
|
|
162
|
+
checker.isTypeAssignableTo(
|
|
163
|
+
typeArguments[0],
|
|
164
|
+
checker.getNumberType(),
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
if (typeWrapperLayers(arrayExpression).every(isNumberArray)) {
|
|
170
|
+
mut_nodesToFix.push({
|
|
171
|
+
node,
|
|
172
|
+
arrayExpression,
|
|
173
|
+
messageId: 'useArrSum',
|
|
174
|
+
});
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
return;
|
|
@@ -149,14 +179,12 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
149
179
|
|
|
150
180
|
// Case 2: (a, b) => a.prop + b.prop or a['prop'] + b['prop']
|
|
151
181
|
if (
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
body.left.type === AST_NODE_TYPES.MemberExpression &&
|
|
155
|
-
body.right.type === AST_NODE_TYPES.MemberExpression
|
|
182
|
+
left.type === AST_NODE_TYPES.MemberExpression &&
|
|
183
|
+
right.type === AST_NODE_TYPES.MemberExpression
|
|
156
184
|
) {
|
|
157
|
-
const leftObj =
|
|
185
|
+
const leftObj = skipTypeWrappers(left.object);
|
|
158
186
|
|
|
159
|
-
const rightObj =
|
|
187
|
+
const rightObj = skipTypeWrappers(right.object);
|
|
160
188
|
|
|
161
189
|
if (
|
|
162
190
|
leftObj.type !== AST_NODE_TYPES.Identifier ||
|
|
@@ -168,25 +196,25 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
// Check if both access the same property
|
|
171
|
-
const leftProp =
|
|
199
|
+
const leftProp = left.property;
|
|
172
200
|
|
|
173
|
-
const rightProp =
|
|
201
|
+
const rightProp = right.property;
|
|
174
202
|
|
|
175
203
|
let mut_propName: string | undefined;
|
|
176
204
|
|
|
177
205
|
if (
|
|
178
206
|
leftProp.type === AST_NODE_TYPES.Identifier &&
|
|
179
|
-
!
|
|
207
|
+
!left.computed &&
|
|
180
208
|
rightProp.type === AST_NODE_TYPES.Identifier &&
|
|
181
|
-
!
|
|
209
|
+
!right.computed &&
|
|
182
210
|
leftProp.name === rightProp.name
|
|
183
211
|
) {
|
|
184
212
|
mut_propName = leftProp.name;
|
|
185
213
|
} else if (
|
|
186
214
|
leftProp.type === AST_NODE_TYPES.Literal &&
|
|
187
|
-
|
|
215
|
+
left.computed &&
|
|
188
216
|
rightProp.type === AST_NODE_TYPES.Literal &&
|
|
189
|
-
|
|
217
|
+
right.computed &&
|
|
190
218
|
leftProp.value === rightProp.value &&
|
|
191
219
|
typeof leftProp.value === 'string'
|
|
192
220
|
) {
|
|
@@ -199,9 +227,9 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
199
227
|
|
|
200
228
|
// Check if property type is number
|
|
201
229
|
if (services?.program !== undefined && services.program !== null) {
|
|
202
|
-
const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(
|
|
230
|
+
const tsLeftProp = services.esTreeNodeToTSNodeMap?.get(left);
|
|
203
231
|
|
|
204
|
-
const tsRightProp = services.esTreeNodeToTSNodeMap?.get(
|
|
232
|
+
const tsRightProp = services.esTreeNodeToTSNodeMap?.get(right);
|
|
205
233
|
|
|
206
234
|
if (tsLeftProp !== undefined && tsRightProp !== undefined) {
|
|
207
235
|
const leftPropType = checker.getTypeAtLocation(tsLeftProp);
|
|
@@ -223,7 +251,7 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
223
251
|
// Generate mapper function
|
|
224
252
|
const mapperParam = param1.name;
|
|
225
253
|
|
|
226
|
-
const mapper =
|
|
254
|
+
const mapper = left.computed
|
|
227
255
|
? `${mapperParam} => ${mapperParam}['${mut_propName}']`
|
|
228
256
|
: `${mapperParam} => ${mapperParam}.${mut_propName}`;
|
|
229
257
|
|
|
@@ -241,7 +269,10 @@ export const preferArrSum: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
241
269
|
const hasArrImport = namedImports.includes('Arr');
|
|
242
270
|
|
|
243
271
|
for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
|
|
244
|
-
const arrayText =
|
|
272
|
+
const arrayText = toArgumentText(
|
|
273
|
+
nodeInfo.arrayExpression,
|
|
274
|
+
sourceCode,
|
|
275
|
+
);
|
|
245
276
|
|
|
246
277
|
context.report({
|
|
247
278
|
node: nodeInfo.node,
|
|
@@ -169,3 +169,100 @@ describe('prefer-arr-sum', () => {
|
|
|
169
169
|
],
|
|
170
170
|
});
|
|
171
171
|
}, 20000);
|
|
172
|
+
|
|
173
|
+
describe('prefer-arr-sum through type wrappers', () => {
|
|
174
|
+
tester.run('prefer-arr-sum', preferArrSum, {
|
|
175
|
+
valid: [
|
|
176
|
+
{
|
|
177
|
+
name: 'strings cast to numbers are concatenated, not summed',
|
|
178
|
+
code: dedent`
|
|
179
|
+
declare const xs: readonly string[];
|
|
180
|
+
const sum = (xs as unknown as readonly number[]).reduce(
|
|
181
|
+
(a, b) => a + b,
|
|
182
|
+
0,
|
|
183
|
+
);
|
|
184
|
+
`,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'a string property cast to number',
|
|
188
|
+
code: dedent`
|
|
189
|
+
declare const xs: readonly Readonly<{ n: string }>[];
|
|
190
|
+
const sum = xs.reduce(
|
|
191
|
+
(a, b) => (a.n as unknown as number) + (b.n as unknown as number),
|
|
192
|
+
0,
|
|
193
|
+
);
|
|
194
|
+
`,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: 'a computed `reduce` is some other method',
|
|
198
|
+
code: dedent`
|
|
199
|
+
declare const xs: readonly number[];
|
|
200
|
+
declare const reduce: 'reduce';
|
|
201
|
+
const sum = xs[reduce]((a, b) => a + b, 0);
|
|
202
|
+
`,
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
invalid: [
|
|
206
|
+
{
|
|
207
|
+
name: 'the initial value or an operand wrapped',
|
|
208
|
+
code: dedent`
|
|
209
|
+
import { Arr } from 'ts-data-forge';
|
|
210
|
+
declare const xs: readonly number[];
|
|
211
|
+
const a = xs.reduce((a, b) => a + b, 0 as number);
|
|
212
|
+
const b = xs.reduce((a, b) => a + (b satisfies number), 0);
|
|
213
|
+
const c = xs!.reduce((a, b) => a! + b, 0);
|
|
214
|
+
`,
|
|
215
|
+
output: dedent`
|
|
216
|
+
import { Arr } from 'ts-data-forge';
|
|
217
|
+
declare const xs: readonly number[];
|
|
218
|
+
const a = Arr.sum(xs);
|
|
219
|
+
const b = Arr.sum(xs);
|
|
220
|
+
const c = Arr.sum(xs!);
|
|
221
|
+
`,
|
|
222
|
+
errors: [
|
|
223
|
+
{ messageId: 'useArrSum' },
|
|
224
|
+
{ messageId: 'useArrSum' },
|
|
225
|
+
{ messageId: 'useArrSum' },
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: 'a property read through wrappers',
|
|
230
|
+
code: dedent`
|
|
231
|
+
import { Arr } from 'ts-data-forge';
|
|
232
|
+
declare const xs: readonly Readonly<{ n: number }>[];
|
|
233
|
+
const sum = xs.reduce((a, b) => (a.n satisfies number) + b!.n, 0);
|
|
234
|
+
`,
|
|
235
|
+
output: dedent`
|
|
236
|
+
import { Arr } from 'ts-data-forge';
|
|
237
|
+
declare const xs: readonly Readonly<{ n: number }>[];
|
|
238
|
+
const sum = Arr.sumBy(xs, a => a.n);
|
|
239
|
+
`,
|
|
240
|
+
errors: [{ messageId: 'useArrSumBy' }],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
}, 20000);
|
|
245
|
+
|
|
246
|
+
describe('prefer-arr-sum with parenthesized operands', () => {
|
|
247
|
+
tester.run('prefer-arr-sum', preferArrSum, {
|
|
248
|
+
valid: [],
|
|
249
|
+
invalid: [
|
|
250
|
+
{
|
|
251
|
+
name: 'a sequence array keeps its parentheses',
|
|
252
|
+
code: dedent`
|
|
253
|
+
import { Arr } from 'ts-data-forge';
|
|
254
|
+
declare const xs: readonly number[];
|
|
255
|
+
declare const log: () => void;
|
|
256
|
+
const sum = (log(), xs).reduce((a, b) => a + b, 0);
|
|
257
|
+
`,
|
|
258
|
+
output: dedent`
|
|
259
|
+
import { Arr } from 'ts-data-forge';
|
|
260
|
+
declare const xs: readonly number[];
|
|
261
|
+
declare const log: () => void;
|
|
262
|
+
const sum = Arr.sum((log(), xs));
|
|
263
|
+
`,
|
|
264
|
+
errors: [{ messageId: 'useArrSum' }],
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
}, 20000);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { Arr } from 'ts-data-forge';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
|
+
import { skipTypeWrappers, typeWrapperLayers } from './ast-utils.mjs';
|
|
8
9
|
import {
|
|
9
10
|
buildImportFixes,
|
|
10
11
|
getImportedLocalName,
|
|
@@ -87,15 +88,19 @@ export const preferArrUniq: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
87
88
|
return;
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
// At every layer of type wrappers (see typeWrapperLayers).
|
|
92
|
+
const isPrimitiveArray = typeWrapperLayers(arrayExpression).every(
|
|
93
|
+
(layer) => {
|
|
94
|
+
const tsNode = getTsNode(layer);
|
|
91
95
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
return (
|
|
97
|
+
tsNode !== undefined &&
|
|
98
|
+
isPrimitiveArrayType(checker, checker.getTypeAtLocation(tsNode))
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
95
102
|
|
|
96
|
-
if (
|
|
97
|
-
!isPrimitiveArrayType(checker, checker.getTypeAtLocation(arrayTsNode))
|
|
98
|
-
) {
|
|
103
|
+
if (!isPrimitiveArray) {
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
106
|
|
|
@@ -221,8 +226,11 @@ const mutatingMethods: ReadonlySet<string> = new Set([
|
|
|
221
226
|
*/
|
|
222
227
|
const getSetSourceArray = (
|
|
223
228
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
224
|
-
|
|
229
|
+
wrapped: TSESTree.Node,
|
|
225
230
|
): TSESTree.Expression | undefined => {
|
|
231
|
+
// `new Set(xs) as ReadonlySet<string>` is still `new Set(xs)`.
|
|
232
|
+
const node = skipTypeWrappers(wrapped);
|
|
233
|
+
|
|
226
234
|
if (
|
|
227
235
|
node.type !== AST_NODE_TYPES.NewExpression ||
|
|
228
236
|
node.callee.type !== AST_NODE_TYPES.Identifier ||
|
|
@@ -348,15 +356,17 @@ const needsMutableResult = (
|
|
|
348
356
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
349
357
|
node: TSESTree.Node,
|
|
350
358
|
): boolean => {
|
|
351
|
-
|
|
359
|
+
const outer = outermostTypePreservingWrapper(node);
|
|
360
|
+
|
|
361
|
+
if (isUsedAsMutable(checker, getTsNode, outer)) {
|
|
352
362
|
return true;
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
const parent =
|
|
365
|
+
const parent = outer.parent;
|
|
356
366
|
|
|
357
367
|
if (
|
|
358
368
|
parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
|
|
359
|
-
parent.init !==
|
|
369
|
+
parent.init !== outer ||
|
|
360
370
|
parent.id.type !== AST_NODE_TYPES.Identifier ||
|
|
361
371
|
parent.id.typeAnnotation !== undefined
|
|
362
372
|
) {
|
|
@@ -369,11 +379,35 @@ const needsMutableResult = (
|
|
|
369
379
|
variable?.references.some(
|
|
370
380
|
(reference) =>
|
|
371
381
|
reference.init !== true &&
|
|
372
|
-
isUsedAsMutable(
|
|
382
|
+
isUsedAsMutable(
|
|
383
|
+
checker,
|
|
384
|
+
getTsNode,
|
|
385
|
+
outermostTypePreservingWrapper(reference.identifier),
|
|
386
|
+
),
|
|
373
387
|
) ?? false
|
|
374
388
|
);
|
|
375
389
|
};
|
|
376
390
|
|
|
391
|
+
/**
|
|
392
|
+
* `node`, or the outermost `!` / `satisfies` around it. Neither changes the
|
|
393
|
+
* type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
|
|
394
|
+
* `ys!.push(4)` still need a mutable array. An `as` does change it, and a
|
|
395
|
+
* readonly array may be cast to a mutable one, so the walk stops there.
|
|
396
|
+
*/
|
|
397
|
+
const outermostTypePreservingWrapper = (
|
|
398
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
399
|
+
node: TSESTree.Node,
|
|
400
|
+
): TSESTree.Node => {
|
|
401
|
+
const { parent } = node;
|
|
402
|
+
|
|
403
|
+
return parent !== undefined &&
|
|
404
|
+
(parent.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
405
|
+
parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
|
|
406
|
+
parent.expression === node
|
|
407
|
+
? outermostTypePreservingWrapper(parent)
|
|
408
|
+
: node;
|
|
409
|
+
};
|
|
410
|
+
|
|
377
411
|
const isUsedAsMutable = (
|
|
378
412
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
379
413
|
checker: ts.TypeChecker,
|
|
@@ -453,3 +453,74 @@ describe('prefer-arr-uniq', () => {
|
|
|
453
453
|
],
|
|
454
454
|
});
|
|
455
455
|
});
|
|
456
|
+
|
|
457
|
+
describe('prefer-arr-uniq through type wrappers', () => {
|
|
458
|
+
tester.run('prefer-arr-uniq', preferArrUniq, {
|
|
459
|
+
valid: [],
|
|
460
|
+
invalid: [
|
|
461
|
+
{
|
|
462
|
+
name: 'a Set wrapped in `as` or `satisfies`',
|
|
463
|
+
code: dedent`
|
|
464
|
+
declare const xs: readonly string[];
|
|
465
|
+
const a = Array.from(new Set(xs) as ReadonlySet<string>);
|
|
466
|
+
const b = [...(new Set(xs) satisfies ReadonlySet<string>)];
|
|
467
|
+
`,
|
|
468
|
+
output: dedent`
|
|
469
|
+
import { Arr } from 'ts-data-forge';
|
|
470
|
+
declare const xs: readonly string[];
|
|
471
|
+
const a = Arr.uniq(xs);
|
|
472
|
+
const b = Arr.uniq(xs);
|
|
473
|
+
`,
|
|
474
|
+
errors: [{ messageId: 'useArrUniq' }, { messageId: 'useArrUniq' }],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'only suggests when the result is mutated through `!`',
|
|
478
|
+
code: dedent`
|
|
479
|
+
const xs = [3, 1, 3];
|
|
480
|
+
Array.from(new Set(xs))!.push(4);
|
|
481
|
+
`,
|
|
482
|
+
output: null,
|
|
483
|
+
errors: [
|
|
484
|
+
{
|
|
485
|
+
messageId: 'useArrUniq',
|
|
486
|
+
suggestions: [
|
|
487
|
+
{
|
|
488
|
+
messageId: 'suggestArrUniq',
|
|
489
|
+
output: dedent`
|
|
490
|
+
import { Arr } from 'ts-data-forge';
|
|
491
|
+
const xs = [3, 1, 3];
|
|
492
|
+
Arr.uniq(xs)!.push(4);
|
|
493
|
+
`,
|
|
494
|
+
},
|
|
495
|
+
],
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'only suggests when a variable initialized through `!` is mutated',
|
|
501
|
+
code: dedent`
|
|
502
|
+
const xs = [3, 1, 3];
|
|
503
|
+
const ys = Array.from(new Set(xs))!;
|
|
504
|
+
ys.push(4);
|
|
505
|
+
`,
|
|
506
|
+
output: null,
|
|
507
|
+
errors: [
|
|
508
|
+
{
|
|
509
|
+
messageId: 'useArrUniq',
|
|
510
|
+
suggestions: [
|
|
511
|
+
{
|
|
512
|
+
messageId: 'suggestArrUniq',
|
|
513
|
+
output: dedent`
|
|
514
|
+
import { Arr } from 'ts-data-forge';
|
|
515
|
+
const xs = [3, 1, 3];
|
|
516
|
+
const ys = Arr.uniq(xs)!;
|
|
517
|
+
ys.push(4);
|
|
518
|
+
`,
|
|
519
|
+
},
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
});
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import { toArgumentText } from './ast-utils.mjs';
|
|
7
8
|
import { brandedNumberTypeNameToFunctionName } from './branded-number-types.mjs';
|
|
8
9
|
import {
|
|
9
10
|
buildImportFixes,
|
|
@@ -38,25 +39,32 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
38
39
|
const tsDataForgeImport = getTsDataForgeImport(program);
|
|
39
40
|
|
|
40
41
|
const mut_nodesToFix: {
|
|
41
|
-
node: TSESTree.TSAsExpression;
|
|
42
|
+
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion;
|
|
42
43
|
typeName: string;
|
|
43
44
|
functionName: string;
|
|
44
45
|
}[] = [];
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// `x as Int` and `<Int>x` are the same assertion.
|
|
48
|
+
const collect = (
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
50
|
+
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion,
|
|
51
|
+
): void => {
|
|
52
|
+
const typeInfo = getBrandedNumberTypeInfo(node.typeAnnotation);
|
|
53
|
+
|
|
54
|
+
if (typeInfo === undefined) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
mut_nodesToFix.push({
|
|
59
|
+
node,
|
|
60
|
+
typeName: typeInfo.typeName,
|
|
61
|
+
functionName: typeInfo.functionName,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
53
64
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
functionName: typeInfo.functionName,
|
|
58
|
-
});
|
|
59
|
-
},
|
|
65
|
+
return {
|
|
66
|
+
TSAsExpression: collect,
|
|
67
|
+
TSTypeAssertion: collect,
|
|
60
68
|
'Program:exit': () => {
|
|
61
69
|
const namedImports = getNamedImports(tsDataForgeImport);
|
|
62
70
|
|
|
@@ -79,7 +87,7 @@ export const preferAsInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
79
87
|
functionName,
|
|
80
88
|
},
|
|
81
89
|
fix: (fixer) => {
|
|
82
|
-
const replacement = `${functionName}(${
|
|
90
|
+
const replacement = `${functionName}(${toArgumentText(node.expression, sourceCode)})`;
|
|
83
91
|
|
|
84
92
|
// Add import only for the first node of this function and only if not already imported
|
|
85
93
|
const importFixes =
|
|
@@ -265,3 +265,54 @@ describe('prefer-as-int', () => {
|
|
|
265
265
|
],
|
|
266
266
|
});
|
|
267
267
|
}, 20000);
|
|
268
|
+
|
|
269
|
+
describe('prefer-as-int through type wrappers', () => {
|
|
270
|
+
tester.run('prefer-as-int', preferAsInt, {
|
|
271
|
+
valid: [],
|
|
272
|
+
invalid: [
|
|
273
|
+
{
|
|
274
|
+
name: 'an angle-bracket assertion',
|
|
275
|
+
code: dedent`
|
|
276
|
+
import { asInt } from 'ts-data-forge';
|
|
277
|
+
declare const n: number;
|
|
278
|
+
const a = <Int>n;
|
|
279
|
+
`,
|
|
280
|
+
output: dedent`
|
|
281
|
+
import { asInt } from 'ts-data-forge';
|
|
282
|
+
declare const n: number;
|
|
283
|
+
const a = asInt(n);
|
|
284
|
+
`,
|
|
285
|
+
errors: [{ messageId: 'useBrandedNumberCastFunction' }],
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
});
|
|
289
|
+
}, 20000);
|
|
290
|
+
|
|
291
|
+
describe('prefer-as-int with parenthesized operands', () => {
|
|
292
|
+
tester.run('prefer-as-int', preferAsInt, {
|
|
293
|
+
valid: [],
|
|
294
|
+
invalid: [
|
|
295
|
+
{
|
|
296
|
+
name: 'a sequence operand keeps its parentheses',
|
|
297
|
+
code: dedent`
|
|
298
|
+
import { asInt } from 'ts-data-forge';
|
|
299
|
+
declare const log: () => void;
|
|
300
|
+
declare const n: number;
|
|
301
|
+
const a = (log(), n) as Int;
|
|
302
|
+
const b = <Int>(log(), n);
|
|
303
|
+
`,
|
|
304
|
+
output: dedent`
|
|
305
|
+
import { asInt } from 'ts-data-forge';
|
|
306
|
+
declare const log: () => void;
|
|
307
|
+
declare const n: number;
|
|
308
|
+
const a = asInt((log(), n));
|
|
309
|
+
const b = asInt((log(), n));
|
|
310
|
+
`,
|
|
311
|
+
errors: [
|
|
312
|
+
{ messageId: 'useBrandedNumberCastFunction' },
|
|
313
|
+
{ messageId: 'useBrandedNumberCastFunction' },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
});
|
|
318
|
+
}, 20000);
|