eslint-plugin-ts-data-forge 0.8.1 → 0.8.3
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/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 +142 -31
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/branded-number-types.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 +26 -13
- 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 +161 -101
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +25 -11
- 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 +11 -21
- 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 +16 -28
- 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 +16 -24
- 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 +16 -24
- 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 +9 -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 +68 -31
- 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 +57 -40
- 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 +59 -30
- 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 +27 -24
- 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 +59 -28
- 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 +41 -16
- 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 +18 -8
- 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 +31 -6
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +56 -40
- 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 +18 -8
- 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 +27 -10
- 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 +24 -11
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +15 -8
- 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 +25 -23
- 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 +192 -37
- package/src/rules/branded-number-types.mts +1 -1
- package/src/rules/import-utils.mts +41 -17
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/no-unnecessary-type-guard.mts +249 -148
- package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
- package/src/rules/prefer-arr-is-array.mts +32 -13
- package/src/rules/prefer-arr-is-array.test.mts +40 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
- package/src/rules/prefer-arr-is-non-empty.mts +19 -24
- package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
- package/src/rules/prefer-arr-scan.mts +107 -37
- package/src/rules/prefer-arr-scan.test.mts +149 -0
- package/src/rules/prefer-arr-sum.mts +87 -50
- package/src/rules/prefer-arr-sum.test.mts +97 -0
- package/src/rules/prefer-arr-uniq.mts +83 -33
- package/src/rules/prefer-arr-uniq.test.mts +71 -0
- package/src/rules/prefer-as-int.mts +38 -38
- package/src/rules/prefer-as-int.test.mts +51 -0
- package/src/rules/prefer-canonical-array-slicing.mts +84 -29
- package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
- package/src/rules/prefer-canonical-length-cast.mts +56 -18
- package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
- package/src/rules/prefer-canonical-length-guard.mts +25 -11
- package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
- package/src/rules/prefer-is-non-null-object.mts +57 -33
- package/src/rules/prefer-is-non-null-object.test.mts +42 -0
- package/src/rules/prefer-is-record-and-has-key.mts +27 -12
- package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
- package/src/rules/prefer-num-safe-parse-float.mts +37 -13
- package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
- package/src/rules/prefer-num-safe-parse-int.mts +41 -15
- package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
- package/src/rules/prefer-range-for-loop.mts +21 -9
- 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,6 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
+
import { toArgumentText, typeWrapperLayers } from './ast-utils.mjs';
|
|
7
8
|
import {
|
|
8
9
|
buildImportFixes,
|
|
9
10
|
getNamedImports,
|
|
@@ -36,7 +37,9 @@ const isStringType = (type: ts.Type): boolean => {
|
|
|
36
37
|
* `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` (ts-data-forge).
|
|
37
38
|
*
|
|
38
39
|
* `Number(x)` is only flagged when x is purely a string type to avoid false
|
|
39
|
-
* positives for `Number(someBoolean)` or `Number(someNumber)` uses.
|
|
40
|
+
* positives for `Number(someBoolean)` or `Number(someNumber)` uses. The type is
|
|
41
|
+
* asked of the value under any `as` / `!` / `satisfies` too, so a cast does not
|
|
42
|
+
* vouch for a string that is not one.
|
|
40
43
|
*/
|
|
41
44
|
export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
42
45
|
{
|
|
@@ -91,32 +94,53 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
91
94
|
callee.type === AST_NODE_TYPES.Identifier &&
|
|
92
95
|
callee.name === 'Number';
|
|
93
96
|
|
|
94
|
-
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall)
|
|
97
|
+
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
|
|
95
98
|
return;
|
|
99
|
+
}
|
|
96
100
|
|
|
97
101
|
const args = node.arguments;
|
|
98
102
|
|
|
99
103
|
const firstArg = args[0];
|
|
100
104
|
|
|
101
|
-
if (firstArg === undefined)
|
|
105
|
+
if (firstArg === undefined) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
102
108
|
|
|
103
109
|
// Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
|
|
104
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement)
|
|
110
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
105
113
|
|
|
106
114
|
// The argument must be purely `string` so the autofix is type-safe
|
|
107
115
|
// against `Num.safeParseFloat(s: string)`. Without type information,
|
|
108
116
|
// skip.
|
|
109
|
-
if (services?.program == null)
|
|
117
|
+
if (services?.program == null) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
110
120
|
|
|
111
121
|
const checker = services.program.getTypeChecker();
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
123
|
+
// Every layer of type wrappers must be `string` (see
|
|
124
|
+
// typeWrapperLayers): the value underneath is what `Number` sees at
|
|
125
|
+
// run time (`Number(v as string)` may still be handed `true`), and
|
|
126
|
+
// the type a wrapper states is what the fix, which keeps the
|
|
127
|
+
// wrapper, passes on.
|
|
128
|
+
const isString = (
|
|
129
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
131
|
+
expression: TSESTree.Node,
|
|
132
|
+
): boolean => {
|
|
133
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(expression);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
tsNode !== undefined &&
|
|
137
|
+
isStringType(checker.getTypeAtLocation(tsNode))
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
if (!typeWrapperLayers(firstArg).every(isString)) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
120
144
|
|
|
121
145
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
122
146
|
},
|
|
@@ -131,7 +155,7 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
131
155
|
index,
|
|
132
156
|
{ node, argExpression },
|
|
133
157
|
] of mut_nodesToFix.entries()) {
|
|
134
|
-
const argText =
|
|
158
|
+
const argText = toArgumentText(argExpression, sourceCode);
|
|
135
159
|
|
|
136
160
|
const originalText = sourceCode.getText(node);
|
|
137
161
|
|
|
@@ -161,3 +161,82 @@ describe('prefer-num-safe-parse-float', () => {
|
|
|
161
161
|
],
|
|
162
162
|
});
|
|
163
163
|
}, 20000);
|
|
164
|
+
|
|
165
|
+
describe('prefer-num-safe-parse-float through type wrappers', () => {
|
|
166
|
+
tester.run('prefer-num-safe-parse-float', preferNumSafeParseFloat, {
|
|
167
|
+
valid: [
|
|
168
|
+
{
|
|
169
|
+
name: 'an unknown value cast to string',
|
|
170
|
+
code: dedent`
|
|
171
|
+
declare const v: unknown;
|
|
172
|
+
const a = Number(v as string);
|
|
173
|
+
const b = parseFloat(<string>v);
|
|
174
|
+
`,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'a number cast to string',
|
|
178
|
+
code: dedent`
|
|
179
|
+
declare const n: number;
|
|
180
|
+
const a = Number(n as unknown as string);
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: 'a string whose type a middle layer does not vouch for',
|
|
185
|
+
code: dedent`
|
|
186
|
+
declare const s: string;
|
|
187
|
+
const a = Number(s as unknown as string);
|
|
188
|
+
`,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'a `!` that leaves null in place (`Number(null)` is 0)',
|
|
192
|
+
code: dedent`
|
|
193
|
+
declare const s: string | null;
|
|
194
|
+
const a = Number(s!);
|
|
195
|
+
`,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
invalid: [
|
|
199
|
+
{
|
|
200
|
+
name: 'a string argument keeps its wrapper',
|
|
201
|
+
code: dedent`
|
|
202
|
+
declare const s: string;
|
|
203
|
+
const a = Number(s!);
|
|
204
|
+
const b = parseFloat(s satisfies string);
|
|
205
|
+
`,
|
|
206
|
+
output: dedent`
|
|
207
|
+
import { Num, Result } from 'ts-data-forge';
|
|
208
|
+
declare const s: string;
|
|
209
|
+
const a = Result.unwrapOkOr(Num.safeParseFloat(s!), Number.NaN);
|
|
210
|
+
const b = Result.unwrapOkOr(Num.safeParseFloat(s satisfies string), Number.NaN);
|
|
211
|
+
`,
|
|
212
|
+
errors: [
|
|
213
|
+
{ messageId: 'useSafeParseFloat' },
|
|
214
|
+
{ messageId: 'useSafeParseFloat' },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
});
|
|
219
|
+
}, 20000);
|
|
220
|
+
|
|
221
|
+
describe('prefer-num-safe-parse-float with parenthesized operands', () => {
|
|
222
|
+
tester.run('prefer-num-safe-parse-float', preferNumSafeParseFloat, {
|
|
223
|
+
valid: [],
|
|
224
|
+
invalid: [
|
|
225
|
+
{
|
|
226
|
+
name: 'a sequence argument keeps its parentheses',
|
|
227
|
+
code: dedent`
|
|
228
|
+
declare const s: string;
|
|
229
|
+
declare const log: () => void;
|
|
230
|
+
const a = Number((log(), s));
|
|
231
|
+
`,
|
|
232
|
+
output: dedent`
|
|
233
|
+
import { Num, Result } from 'ts-data-forge';
|
|
234
|
+
declare const s: string;
|
|
235
|
+
declare const log: () => void;
|
|
236
|
+
const a = Result.unwrapOkOr(Num.safeParseFloat((log(), s)), Number.NaN);
|
|
237
|
+
`,
|
|
238
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
}, 20000);
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
skipTypeWrappers,
|
|
9
|
+
toArgumentText,
|
|
10
|
+
typeWrapperLayers,
|
|
11
|
+
} from './ast-utils.mjs';
|
|
7
12
|
import {
|
|
8
13
|
buildImportFixes,
|
|
9
14
|
getNamedImports,
|
|
@@ -84,20 +89,28 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
84
89
|
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
85
90
|
callee.property.name === 'parseInt';
|
|
86
91
|
|
|
87
|
-
if (!isGlobalParseInt && !isNumberParseInt)
|
|
92
|
+
if (!isGlobalParseInt && !isNumberParseInt) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
const args = node.arguments;
|
|
90
97
|
|
|
91
98
|
const firstArg = args[0];
|
|
92
99
|
|
|
93
|
-
if (firstArg === undefined)
|
|
100
|
+
if (firstArg === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
94
103
|
|
|
95
104
|
// Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
|
|
96
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement)
|
|
105
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
97
108
|
|
|
98
|
-
// Only base 10: accept a missing radix or an explicit literal `10
|
|
109
|
+
// Only base 10: accept a missing radix or an explicit literal `10`,
|
|
110
|
+
// under any type wrapper (`10 as const`).
|
|
99
111
|
if (args.length >= 2) {
|
|
100
|
-
const radix =
|
|
112
|
+
const radix =
|
|
113
|
+
args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
|
|
101
114
|
|
|
102
115
|
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
103
116
|
return;
|
|
@@ -105,18 +118,31 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
// The argument must be a `string` so the autofix is type-safe against
|
|
108
|
-
// `Num.safeParseInt(s: string)`. Without type information, skip.
|
|
109
|
-
|
|
121
|
+
// `Num.safeParseInt(s: string)`. Without type information, skip. Every
|
|
122
|
+
// layer of type wrappers is asked (see typeWrapperLayers): a cast
|
|
123
|
+
// does not make a number a string.
|
|
124
|
+
if (services?.program == null) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
110
127
|
|
|
111
128
|
const checker = services.program.getTypeChecker();
|
|
112
129
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
const isString = (
|
|
131
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
133
|
+
expression: TSESTree.Node,
|
|
134
|
+
): boolean => {
|
|
135
|
+
const tsNode = services.esTreeNodeToTSNodeMap?.get(expression);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
tsNode !== undefined &&
|
|
139
|
+
isStringType(checker.getTypeAtLocation(tsNode))
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (!typeWrapperLayers(firstArg).every(isString)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
120
146
|
|
|
121
147
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
122
148
|
},
|
|
@@ -131,7 +157,7 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
131
157
|
index,
|
|
132
158
|
{ node, argExpression },
|
|
133
159
|
] of mut_nodesToFix.entries()) {
|
|
134
|
-
const argText =
|
|
160
|
+
const argText = toArgumentText(argExpression, sourceCode);
|
|
135
161
|
|
|
136
162
|
const originalText = sourceCode.getText(node);
|
|
137
163
|
|
|
@@ -135,3 +135,67 @@ describe('prefer-num-safe-parse-int', () => {
|
|
|
135
135
|
],
|
|
136
136
|
});
|
|
137
137
|
}, 20000);
|
|
138
|
+
|
|
139
|
+
describe('prefer-num-safe-parse-int through type wrappers', () => {
|
|
140
|
+
tester.run('prefer-num-safe-parse-int', preferNumSafeParseInt, {
|
|
141
|
+
valid: [
|
|
142
|
+
{
|
|
143
|
+
name: 'a number cast to string',
|
|
144
|
+
code: dedent`
|
|
145
|
+
declare const n: number;
|
|
146
|
+
const a = parseInt(n as unknown as string, 10);
|
|
147
|
+
`,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'a wrapped radix other than 10',
|
|
151
|
+
code: dedent`
|
|
152
|
+
declare const s: string;
|
|
153
|
+
const a = parseInt(s, 16 as const);
|
|
154
|
+
`,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
invalid: [
|
|
158
|
+
{
|
|
159
|
+
name: 'a radix wrapped in `as const` or `satisfies`',
|
|
160
|
+
code: dedent`
|
|
161
|
+
declare const s: string;
|
|
162
|
+
const a = parseInt(s, 10 as const);
|
|
163
|
+
const b = Number.parseInt(s!, 10 satisfies number);
|
|
164
|
+
`,
|
|
165
|
+
output: dedent`
|
|
166
|
+
import { Num, Result } from 'ts-data-forge';
|
|
167
|
+
declare const s: string;
|
|
168
|
+
const a = Result.unwrapOkOr(Num.safeParseInt(s), Number.NaN);
|
|
169
|
+
const b = Result.unwrapOkOr(Num.safeParseInt(s!), Number.NaN);
|
|
170
|
+
`,
|
|
171
|
+
errors: [
|
|
172
|
+
{ messageId: 'useSafeParseInt' },
|
|
173
|
+
{ messageId: 'useSafeParseInt' },
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
}, 20000);
|
|
179
|
+
|
|
180
|
+
describe('prefer-num-safe-parse-int with parenthesized operands', () => {
|
|
181
|
+
tester.run('prefer-num-safe-parse-int', preferNumSafeParseInt, {
|
|
182
|
+
valid: [],
|
|
183
|
+
invalid: [
|
|
184
|
+
{
|
|
185
|
+
name: 'a sequence argument keeps its parentheses',
|
|
186
|
+
code: dedent`
|
|
187
|
+
declare const s: string;
|
|
188
|
+
declare const log: () => void;
|
|
189
|
+
const a = parseInt((log(), s), 10);
|
|
190
|
+
`,
|
|
191
|
+
output: dedent`
|
|
192
|
+
import { Num, Result } from 'ts-data-forge';
|
|
193
|
+
declare const s: string;
|
|
194
|
+
declare const log: () => void;
|
|
195
|
+
const a = Result.unwrapOkOr(Num.safeParseInt((log(), s)), Number.NaN);
|
|
196
|
+
`,
|
|
197
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}, 20000);
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { Arr } from 'ts-data-forge';
|
|
7
|
-
import {
|
|
7
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
8
8
|
import {
|
|
9
9
|
buildImportFixes,
|
|
10
10
|
getImportedLocalName,
|
|
@@ -159,7 +159,9 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
|
|
|
159
159
|
CallExpression: (node) => {
|
|
160
160
|
const roundTrip = parseEntriesRoundTrip(node);
|
|
161
161
|
|
|
162
|
-
if (roundTrip === undefined)
|
|
162
|
+
if (roundTrip === undefined) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
163
165
|
|
|
164
166
|
mut_roundTrips.push({ node, roundTrip });
|
|
165
167
|
},
|
|
@@ -225,13 +227,19 @@ export const preferObjOverEntriesRoundTrip: TSESLint.RuleModule<
|
|
|
225
227
|
const parseEntriesRoundTrip = (
|
|
226
228
|
node: TSESTree.CallExpression,
|
|
227
229
|
): RoundTrip | undefined => {
|
|
228
|
-
if (!isObjectStaticCall(node, 'fromEntries'))
|
|
230
|
+
if (!isObjectStaticCall(node, 'fromEntries')) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
229
233
|
|
|
230
|
-
if (!Arr.isFixedLengthTuple(1, node.arguments))
|
|
234
|
+
if (!Arr.isFixedLengthTuple(1, node.arguments)) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
231
237
|
|
|
232
238
|
const [transform] = node.arguments;
|
|
233
239
|
|
|
234
|
-
if (transform.type !== AST_NODE_TYPES.CallExpression)
|
|
240
|
+
if (transform.type !== AST_NODE_TYPES.CallExpression) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
235
243
|
|
|
236
244
|
const { callee } = transform;
|
|
237
245
|
|
|
@@ -265,11 +273,15 @@ const parseEntriesRoundTrip = (
|
|
|
265
273
|
|
|
266
274
|
const [record] = entriesCall.arguments;
|
|
267
275
|
|
|
268
|
-
if (record.type === AST_NODE_TYPES.SpreadElement)
|
|
276
|
+
if (record.type === AST_NODE_TYPES.SpreadElement) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
269
279
|
|
|
270
280
|
// A second argument (`thisArg`) or a callback that also takes the entry index
|
|
271
281
|
// has no counterpart in `Obj`.
|
|
272
|
-
if (!Arr.isFixedLengthTuple(1, transform.arguments))
|
|
282
|
+
if (!Arr.isFixedLengthTuple(1, transform.arguments)) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
273
285
|
|
|
274
286
|
const [callback] = transform.arguments;
|
|
275
287
|
|
|
@@ -287,11 +299,15 @@ const parseEntriesRoundTrip = (
|
|
|
287
299
|
|
|
288
300
|
const [entryPattern] = callback.params;
|
|
289
301
|
|
|
290
|
-
if (entryPattern.type !== AST_NODE_TYPES.ArrayPattern)
|
|
302
|
+
if (entryPattern.type !== AST_NODE_TYPES.ArrayPattern) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
291
305
|
|
|
292
306
|
const entryParams = parseEntryPattern(entryPattern);
|
|
293
307
|
|
|
294
|
-
if (entryParams === undefined)
|
|
308
|
+
if (entryParams === undefined) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
295
311
|
|
|
296
312
|
const body = callback.body;
|
|
297
313
|
|
|
@@ -320,7 +336,9 @@ const parseEntriesRoundTrip = (
|
|
|
320
336
|
}
|
|
321
337
|
|
|
322
338
|
case 'flatMap': {
|
|
323
|
-
if (body.type !== AST_NODE_TYPES.ConditionalExpression)
|
|
339
|
+
if (body.type !== AST_NODE_TYPES.ConditionalExpression) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
324
342
|
|
|
325
343
|
const onTrue = parseSingletonEntry(body.consequent, entryParams.keyParam);
|
|
326
344
|
|
|
@@ -365,7 +383,9 @@ type EntryParams = Readonly<{
|
|
|
365
383
|
const parseEntryPattern = (
|
|
366
384
|
pattern: TSESTree.ArrayPattern,
|
|
367
385
|
): EntryParams | undefined => {
|
|
368
|
-
if (pattern.elements.length > 2)
|
|
386
|
+
if (pattern.elements.length > 2) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
369
389
|
|
|
370
390
|
const [keyElement, valueElement] = pattern.elements;
|
|
371
391
|
|
|
@@ -399,7 +419,9 @@ const parseKeptEntry = (
|
|
|
399
419
|
node: TSESTree.Expression,
|
|
400
420
|
keyParam: TSESTree.Identifier | undefined,
|
|
401
421
|
): KeptEntry | undefined => {
|
|
402
|
-
if (keyParam === undefined)
|
|
422
|
+
if (keyParam === undefined) {
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
403
425
|
|
|
404
426
|
const tuple = stripTypeWrappers(node);
|
|
405
427
|
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { Arr } from 'ts-data-forge';
|
|
7
|
+
import { skipTypeWrappers, toArgumentText } from './ast-utils.mjs';
|
|
7
8
|
import {
|
|
8
9
|
buildImportFixes,
|
|
9
10
|
getNamedImports,
|
|
@@ -68,12 +69,21 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
68
69
|
|
|
69
70
|
const beginExpr = declaration.init;
|
|
70
71
|
|
|
71
|
-
// Check test: i < end
|
|
72
|
+
// Check test: i < end. The loop variable and the step are read
|
|
73
|
+
// through type wrappers: `(i satisfies number) < n` tests `i`, and
|
|
74
|
+
// `i += 0 as number` still steps by 0.
|
|
72
75
|
if (
|
|
73
76
|
node.test?.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
74
|
-
node.test.operator !== '<'
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
node.test.operator !== '<'
|
|
78
|
+
) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const testedVariable = skipTypeWrappers(node.test.left);
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
testedVariable.type !== AST_NODE_TYPES.Identifier ||
|
|
86
|
+
testedVariable.name !== varName
|
|
77
87
|
) {
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
@@ -81,7 +91,9 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
81
91
|
const endExpr = node.test.right;
|
|
82
92
|
|
|
83
93
|
// Check update: ++i, i++, i += 1, or i += step
|
|
84
|
-
if (node.update === null)
|
|
94
|
+
if (node.update === null) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
85
97
|
|
|
86
98
|
let mut_step: string | undefined;
|
|
87
99
|
|
|
@@ -103,7 +115,7 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
103
115
|
node.update.left.name === varName
|
|
104
116
|
) {
|
|
105
117
|
// i += step
|
|
106
|
-
const stepValue = node.update.right;
|
|
118
|
+
const stepValue = skipTypeWrappers(node.update.right);
|
|
107
119
|
|
|
108
120
|
if (stepValue.type === AST_NODE_TYPES.Literal) {
|
|
109
121
|
const stepNum = stepValue.value;
|
|
@@ -116,15 +128,15 @@ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
116
128
|
}
|
|
117
129
|
} else {
|
|
118
130
|
// Variable step like i += step
|
|
119
|
-
mut_step = sourceCode
|
|
131
|
+
mut_step = toArgumentText(node.update.right, sourceCode);
|
|
120
132
|
}
|
|
121
133
|
} else {
|
|
122
134
|
return;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
|
-
const beginText =
|
|
137
|
+
const beginText = toArgumentText(beginExpr, sourceCode);
|
|
126
138
|
|
|
127
|
-
const endText =
|
|
139
|
+
const endText = toArgumentText(endExpr, sourceCode);
|
|
128
140
|
|
|
129
141
|
mut_nodesToFix.push({
|
|
130
142
|
node,
|
|
@@ -154,3 +154,78 @@ describe('prefer-range-for-loop', () => {
|
|
|
154
154
|
],
|
|
155
155
|
});
|
|
156
156
|
}, 20000);
|
|
157
|
+
|
|
158
|
+
describe('prefer-range-for-loop through type wrappers', () => {
|
|
159
|
+
tester.run('prefer-range-for-loop', preferRangeForLoop, {
|
|
160
|
+
valid: [
|
|
161
|
+
{
|
|
162
|
+
name: 'a wrapped non-positive step',
|
|
163
|
+
code: dedent`
|
|
164
|
+
declare const n: number;
|
|
165
|
+
for (let i = 0; i < n; i += 0 as number) {
|
|
166
|
+
console.log(i);
|
|
167
|
+
}
|
|
168
|
+
`,
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
invalid: [
|
|
172
|
+
{
|
|
173
|
+
name: 'the loop variable or the step wrapped',
|
|
174
|
+
code: dedent`
|
|
175
|
+
import { range } from 'ts-data-forge';
|
|
176
|
+
declare const n: number;
|
|
177
|
+
for (let i = 0; (i satisfies number) < n; ++i) {
|
|
178
|
+
console.log(i);
|
|
179
|
+
}
|
|
180
|
+
for (let j = 0; j! < n; j += 2 as number) {
|
|
181
|
+
console.log(j);
|
|
182
|
+
}
|
|
183
|
+
`,
|
|
184
|
+
output: dedent`
|
|
185
|
+
import { range } from 'ts-data-forge';
|
|
186
|
+
declare const n: number;
|
|
187
|
+
for (const i of range(0, n)) {
|
|
188
|
+
console.log(i);
|
|
189
|
+
}
|
|
190
|
+
for (const j of range(0, n, 2)) {
|
|
191
|
+
console.log(j);
|
|
192
|
+
}
|
|
193
|
+
`,
|
|
194
|
+
errors: [
|
|
195
|
+
{ messageId: 'useRangeForLoop' },
|
|
196
|
+
{ messageId: 'useRangeForLoop' },
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}, 20000);
|
|
202
|
+
|
|
203
|
+
describe('prefer-range-for-loop with parenthesized operands', () => {
|
|
204
|
+
tester.run('prefer-range-for-loop', preferRangeForLoop, {
|
|
205
|
+
valid: [],
|
|
206
|
+
invalid: [
|
|
207
|
+
{
|
|
208
|
+
name: 'sequence bounds and step keep their parentheses',
|
|
209
|
+
code: dedent`
|
|
210
|
+
import { range } from 'ts-data-forge';
|
|
211
|
+
declare const n: number;
|
|
212
|
+
declare const step: number;
|
|
213
|
+
declare const log: () => void;
|
|
214
|
+
for (let i = (log(), 0); i < (log(), n); i += (log(), step)) {
|
|
215
|
+
console.log(i);
|
|
216
|
+
}
|
|
217
|
+
`,
|
|
218
|
+
output: dedent`
|
|
219
|
+
import { range } from 'ts-data-forge';
|
|
220
|
+
declare const n: number;
|
|
221
|
+
declare const step: number;
|
|
222
|
+
declare const log: () => void;
|
|
223
|
+
for (const i of range((log(), 0), (log(), n), (log(), step))) {
|
|
224
|
+
console.log(i);
|
|
225
|
+
}
|
|
226
|
+
`,
|
|
227
|
+
errors: [{ messageId: 'useRangeForLoop' }],
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
}, 20000);
|
package/src/rules/rules.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ESLintPlugin } from '../types.mjs';
|
|
2
2
|
import { noSideEffectImport } from './no-side-effect-import.mjs';
|
|
3
3
|
import { noUnnecessaryTypeGuard } from './no-unnecessary-type-guard.mjs';
|
|
4
4
|
import { preferArrIsArray } from './prefer-arr-is-array.mjs';
|
package/src/types.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
3
3
|
|
|
4
4
|
export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
|
|
5
5
|
|