eslint-plugin-ts-std-forge 0.1.3 → 0.1.5
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 +1 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +41 -0
- package/dist/rules/ast-utils.d.mts.map +1 -0
- package/dist/rules/ast-utils.mjs +101 -0
- package/dist/rules/ast-utils.mjs.map +1 -0
- 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 +2 -1
- package/dist/rules/import-utils.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 +55 -43
- 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 +22 -17
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.mjs +23 -14
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.mjs +19 -15
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.mjs +16 -17
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse.mjs +14 -16
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +24 -22
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +2 -2
- package/src/rules/ast-utils.mts +147 -0
- package/src/rules/import-utils.mts +4 -2
- package/src/rules/prefer-is-non-null-object.mts +63 -38
- package/src/rules/prefer-is-non-null-object.test.mts +54 -0
- package/src/rules/prefer-is-record-and-has-key.mts +34 -21
- package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
- package/src/rules/prefer-safe-array-is-array.mts +34 -16
- package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
- package/src/rules/prefer-safe-array-length-guard.mts +32 -19
- package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
- package/src/rules/prefer-safe-number-parse-integer.mts +29 -23
- package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
- package/src/rules/prefer-safe-number-parse.mts +25 -21
- package/src/rules/prefer-safe-number-parse.test.mts +97 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -3,7 +3,12 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
7
|
+
import {
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getValueType,
|
|
10
|
+
skipTypeWrappers,
|
|
11
|
+
} from './ast-utils.mjs';
|
|
7
12
|
import {
|
|
8
13
|
buildImportFixes,
|
|
9
14
|
getNamedImports,
|
|
@@ -79,28 +84,28 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
79
84
|
|
|
80
85
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
81
86
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const checker = services?.program?.getTypeChecker();
|
|
87
|
+
const checker = sourceCode.parserServices?.program?.getTypeChecker();
|
|
85
88
|
|
|
86
89
|
/**
|
|
87
90
|
* Whether `expression` is an array or tuple. Without type information the
|
|
88
91
|
* answer is no: `.length` is also a string's, a function's and a
|
|
89
|
-
* `TypedArray`'s, and none of those can be handed to the guard.
|
|
92
|
+
* `TypedArray`'s, and none of those can be handed to the guard. The type is
|
|
93
|
+
* the value's, not a cast's: a string cast to an array is still a string.
|
|
90
94
|
*/
|
|
91
95
|
const isArrayLike = (
|
|
92
96
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
93
97
|
expression: TSESTree.Expression,
|
|
94
98
|
): boolean => {
|
|
95
|
-
if (checker === undefined)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (tsNode === undefined) return false;
|
|
99
|
+
if (checker === undefined) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
100
102
|
|
|
101
|
-
const type =
|
|
103
|
+
const type = getValueType(sourceCode, expression);
|
|
102
104
|
|
|
103
|
-
return
|
|
105
|
+
return (
|
|
106
|
+
type !== undefined &&
|
|
107
|
+
(checker.isArrayType(type) || checker.isTupleType(type))
|
|
108
|
+
);
|
|
104
109
|
};
|
|
105
110
|
|
|
106
111
|
const mut_nodesToFix: {
|
|
@@ -113,9 +118,13 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
113
118
|
BinaryExpression: (node) => {
|
|
114
119
|
const matched = matchLengthComparison(node);
|
|
115
120
|
|
|
116
|
-
if (matched === undefined)
|
|
121
|
+
if (matched === undefined) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
if (!isArrayLike(matched.arrayExpression))
|
|
125
|
+
if (!isArrayLike(matched.arrayExpression)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
119
128
|
|
|
120
129
|
mut_nodesToFix.push({ node, ...matched });
|
|
121
130
|
},
|
|
@@ -132,7 +141,7 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
132
141
|
index,
|
|
133
142
|
{ node, arrayExpression, guard },
|
|
134
143
|
] of mut_nodesToFix.entries()) {
|
|
135
|
-
const arrayText = sourceCode
|
|
144
|
+
const arrayText = getArgumentText(sourceCode, arrayExpression);
|
|
136
145
|
|
|
137
146
|
context.report({
|
|
138
147
|
node,
|
|
@@ -172,9 +181,11 @@ const matchLengthComparison = (
|
|
|
172
181
|
):
|
|
173
182
|
| Readonly<{ arrayExpression: TSESTree.Expression; guard: Guard }>
|
|
174
183
|
| undefined => {
|
|
175
|
-
|
|
184
|
+
// Both operands are read through their type wrappers, which change neither
|
|
185
|
+
// the length nor the bound: `(xs.length as number) > (0 as const)`.
|
|
186
|
+
const left = skipTypeWrappers(node.left);
|
|
176
187
|
|
|
177
|
-
|
|
188
|
+
const right = skipTypeWrappers(node.right);
|
|
178
189
|
|
|
179
190
|
const lengthOnLeft = isLengthAccess(left);
|
|
180
191
|
|
|
@@ -182,7 +193,9 @@ const matchLengthComparison = (
|
|
|
182
193
|
|
|
183
194
|
const boundSide = lengthOnLeft ? right : left;
|
|
184
195
|
|
|
185
|
-
if (!isLengthAccess(lengthSide))
|
|
196
|
+
if (!isLengthAccess(lengthSide)) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
186
199
|
|
|
187
200
|
// `xs.length === ys.length` has `.length` on both sides and no literal
|
|
188
201
|
// bound, so the bound check below rejects it.
|
|
@@ -206,7 +219,7 @@ const matchLengthComparison = (
|
|
|
206
219
|
|
|
207
220
|
const isLengthAccess = (
|
|
208
221
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
209
|
-
node: TSESTree.
|
|
222
|
+
node: TSESTree.Node,
|
|
210
223
|
): node is TSESTree.MemberExpression =>
|
|
211
224
|
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
212
225
|
!node.computed &&
|
|
@@ -150,3 +150,101 @@ describe('prefer-safe-array-length-guard', () => {
|
|
|
150
150
|
],
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
|
+
|
|
154
|
+
describe('prefer-safe-array-length-guard through type wrappers', () => {
|
|
155
|
+
tester.run('prefer-safe-array-length-guard', preferSafeArrayLengthGuard, {
|
|
156
|
+
valid: [
|
|
157
|
+
{
|
|
158
|
+
name: 'a string cast to an array is still a string',
|
|
159
|
+
code: dedent`
|
|
160
|
+
declare const s: string;
|
|
161
|
+
const ok = (s as unknown as readonly string[]).length > 0;
|
|
162
|
+
`,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: 'a bound with no named guard behind `as const` is left alone',
|
|
166
|
+
code: dedent`
|
|
167
|
+
declare const xs: readonly number[];
|
|
168
|
+
const ok = xs.length > (2 as const);
|
|
169
|
+
`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
invalid: [
|
|
173
|
+
{
|
|
174
|
+
name: 'reads the bound through `as const`',
|
|
175
|
+
code: dedent`
|
|
176
|
+
declare const xs: readonly number[];
|
|
177
|
+
const ok = xs.length > (0 as const);
|
|
178
|
+
`,
|
|
179
|
+
output: dedent`
|
|
180
|
+
import { SafeArray } from 'ts-std-forge';
|
|
181
|
+
declare const xs: readonly number[];
|
|
182
|
+
const ok = SafeArray.isNonEmpty(xs);
|
|
183
|
+
`,
|
|
184
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: 'reads the length through `as`',
|
|
188
|
+
code: dedent`
|
|
189
|
+
declare const xs: readonly number[];
|
|
190
|
+
const ok = (xs.length as number) > 0;
|
|
191
|
+
`,
|
|
192
|
+
output: dedent`
|
|
193
|
+
import { SafeArray } from 'ts-std-forge';
|
|
194
|
+
declare const xs: readonly number[];
|
|
195
|
+
const ok = SafeArray.isNonEmpty(xs);
|
|
196
|
+
`,
|
|
197
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'reads the length through `!`, with the bound on the left',
|
|
201
|
+
code: dedent`
|
|
202
|
+
declare const xs: readonly number[];
|
|
203
|
+
const ok = (0 satisfies number) === xs.length!;
|
|
204
|
+
`,
|
|
205
|
+
output: dedent`
|
|
206
|
+
import { SafeArray } from 'ts-std-forge';
|
|
207
|
+
declare const xs: readonly number[];
|
|
208
|
+
const ok = SafeArray.isEmpty(xs);
|
|
209
|
+
`,
|
|
210
|
+
errors: [{ messageId: 'useIsEmpty' }],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'keeps a wrapper on the array and reads `!` as the array it asserts',
|
|
214
|
+
code: dedent`
|
|
215
|
+
declare const xs: readonly number[] | undefined;
|
|
216
|
+
const ok = xs!.length !== 0;
|
|
217
|
+
`,
|
|
218
|
+
output: dedent`
|
|
219
|
+
import { SafeArray } from 'ts-std-forge';
|
|
220
|
+
declare const xs: readonly number[] | undefined;
|
|
221
|
+
const ok = SafeArray.isNonEmpty(xs!);
|
|
222
|
+
`,
|
|
223
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('prefer-safe-array-length-guard with parenthesized operands', () => {
|
|
230
|
+
tester.run('prefer-safe-array-length-guard', preferSafeArrayLengthGuard, {
|
|
231
|
+
valid: [],
|
|
232
|
+
invalid: [
|
|
233
|
+
{
|
|
234
|
+
name: 'keeps the parentheses of a sequence array',
|
|
235
|
+
code: dedent`
|
|
236
|
+
declare const a: number;
|
|
237
|
+
declare const xs: readonly number[];
|
|
238
|
+
const ok = (a, xs).length > 0;
|
|
239
|
+
`,
|
|
240
|
+
output: dedent`
|
|
241
|
+
import { SafeArray } from 'ts-std-forge';
|
|
242
|
+
declare const a: number;
|
|
243
|
+
declare const xs: readonly number[];
|
|
244
|
+
const ok = SafeArray.isNonEmpty((a, xs));
|
|
245
|
+
`,
|
|
246
|
+
errors: [{ messageId: 'useIsNonEmpty' }],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getValueType,
|
|
10
|
+
isIdentifierNamed,
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from './ast-utils.mjs';
|
|
7
13
|
import {
|
|
8
14
|
buildImportFixes,
|
|
9
15
|
getNamedImports,
|
|
@@ -62,8 +68,6 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
62
68
|
|
|
63
69
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
64
70
|
|
|
65
|
-
const services = sourceCode.parserServices;
|
|
66
|
-
|
|
67
71
|
const mut_nodesToFix: {
|
|
68
72
|
node: TSESTree.CallExpression;
|
|
69
73
|
argExpression: TSESTree.Expression;
|
|
@@ -71,7 +75,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
71
75
|
|
|
72
76
|
return {
|
|
73
77
|
CallExpression: (node) => {
|
|
74
|
-
const
|
|
78
|
+
const callee = skipTypeWrappers(node.callee);
|
|
75
79
|
|
|
76
80
|
// Match `parseInt(...)` or `Number.parseInt(...)`.
|
|
77
81
|
const isGlobalParseInt =
|
|
@@ -81,25 +85,32 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
81
85
|
const isNumberParseInt =
|
|
82
86
|
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
83
87
|
!callee.computed &&
|
|
84
|
-
callee.object
|
|
85
|
-
callee.object.name === 'Number' &&
|
|
88
|
+
isIdentifierNamed(callee.object, 'Number') &&
|
|
86
89
|
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
87
90
|
callee.property.name === 'parseInt';
|
|
88
91
|
|
|
89
|
-
if (!isGlobalParseInt && !isNumberParseInt)
|
|
92
|
+
if (!isGlobalParseInt && !isNumberParseInt) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
90
95
|
|
|
91
96
|
const args = node.arguments;
|
|
92
97
|
|
|
93
98
|
const firstArg = args[0];
|
|
94
99
|
|
|
95
|
-
if (firstArg === undefined)
|
|
100
|
+
if (firstArg === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
96
103
|
|
|
97
104
|
// Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
|
|
98
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement)
|
|
105
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
99
108
|
|
|
100
|
-
// 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
|
+
// read through its type wrappers, which leave the value as it is.
|
|
101
111
|
if (args.length >= 2) {
|
|
102
|
-
const radix =
|
|
112
|
+
const radix =
|
|
113
|
+
args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
|
|
103
114
|
|
|
104
115
|
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
105
116
|
return;
|
|
@@ -107,19 +118,14 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
// The argument must be a `string` so the autofix is type-safe against
|
|
110
|
-
// `SafeNumber.parseInteger(value: string)
|
|
111
|
-
//
|
|
112
|
-
|
|
121
|
+
// `SafeNumber.parseInteger(value: string)`, and that of the value, not
|
|
122
|
+
// of a cast: `parseInt(1e21 as unknown as string)` is 1, the rewrite
|
|
123
|
+
// 1e21. Without type information, skip.
|
|
124
|
+
const argType = getValueType(sourceCode, firstArg);
|
|
113
125
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (tsNode === undefined) return;
|
|
119
|
-
|
|
120
|
-
const argType = checker.getTypeAtLocation(tsNode);
|
|
121
|
-
|
|
122
|
-
if (!isStringType(argType)) return;
|
|
126
|
+
if (argType === undefined || !isStringType(argType)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
123
129
|
|
|
124
130
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
125
131
|
},
|
|
@@ -134,7 +140,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
134
140
|
index,
|
|
135
141
|
{ node, argExpression },
|
|
136
142
|
] of mut_nodesToFix.entries()) {
|
|
137
|
-
const argText = sourceCode
|
|
143
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
138
144
|
|
|
139
145
|
context.report({
|
|
140
146
|
node,
|
|
@@ -65,3 +65,107 @@ describe('prefer-safe-number-parse-integer', () => {
|
|
|
65
65
|
],
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
70
|
+
tester.run('prefer-safe-number-parse-integer', preferSafeNumberParseInteger, {
|
|
71
|
+
valid: [
|
|
72
|
+
{
|
|
73
|
+
// `parseInt(1e21)` is 1, `SafeNumber.parseInteger(1e21)` is 1e21.
|
|
74
|
+
name: 'ignores a number cast to string',
|
|
75
|
+
code: dedent`
|
|
76
|
+
declare const x: number;
|
|
77
|
+
const n = parseInt(x as unknown as string);
|
|
78
|
+
`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'ignores a radix other than 10 behind `as const`',
|
|
82
|
+
code: dedent`
|
|
83
|
+
declare const s: string;
|
|
84
|
+
const n = parseInt(s, 16 as const);
|
|
85
|
+
`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
invalid: [
|
|
89
|
+
{
|
|
90
|
+
name: 'reads a radix of 10 through `as const`',
|
|
91
|
+
code: dedent`
|
|
92
|
+
declare const s: string;
|
|
93
|
+
const n = parseInt(s, 10 as const);
|
|
94
|
+
`,
|
|
95
|
+
output: dedent`
|
|
96
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
97
|
+
declare const s: string;
|
|
98
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
99
|
+
`,
|
|
100
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'reads a radix of 10 through `satisfies` and keeps `satisfies` on the argument',
|
|
104
|
+
code: dedent`
|
|
105
|
+
declare const s: string;
|
|
106
|
+
const n = Number.parseInt(s satisfies string, 10 satisfies number);
|
|
107
|
+
`,
|
|
108
|
+
output: dedent`
|
|
109
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
110
|
+
declare const s: string;
|
|
111
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s satisfies string), Number.NaN);
|
|
112
|
+
`,
|
|
113
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'reads the callee through `as` and `!`',
|
|
117
|
+
code: dedent`
|
|
118
|
+
declare const s: string;
|
|
119
|
+
const a = (parseInt as typeof parseInt)(s);
|
|
120
|
+
const b = (Number as NumberConstructor)!.parseInt(s, 10);
|
|
121
|
+
`,
|
|
122
|
+
output: dedent`
|
|
123
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
124
|
+
declare const s: string;
|
|
125
|
+
const a = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
126
|
+
const b = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
127
|
+
`,
|
|
128
|
+
errors: [
|
|
129
|
+
{ messageId: 'useSafeNumberParseInteger' },
|
|
130
|
+
{ messageId: 'useSafeNumberParseInteger' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'reads `!` on an optional string as the string it asserts',
|
|
135
|
+
code: dedent`
|
|
136
|
+
declare const s: string | undefined;
|
|
137
|
+
const n = parseInt(s!);
|
|
138
|
+
`,
|
|
139
|
+
output: dedent`
|
|
140
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
141
|
+
declare const s: string | undefined;
|
|
142
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s!), Number.NaN);
|
|
143
|
+
`,
|
|
144
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('prefer-safe-number-parse-integer with parenthesized operands', () => {
|
|
151
|
+
tester.run('prefer-safe-number-parse-integer', preferSafeNumberParseInteger, {
|
|
152
|
+
valid: [],
|
|
153
|
+
invalid: [
|
|
154
|
+
{
|
|
155
|
+
name: 'keeps the parentheses of a sequence argument',
|
|
156
|
+
code: dedent`
|
|
157
|
+
declare const a: number;
|
|
158
|
+
declare const s: string;
|
|
159
|
+
const n = parseInt((a, s), (10));
|
|
160
|
+
`,
|
|
161
|
+
output: dedent`
|
|
162
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
163
|
+
declare const a: number;
|
|
164
|
+
declare const s: string;
|
|
165
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger((a, s)), Number.NaN);
|
|
166
|
+
`,
|
|
167
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getValueType,
|
|
10
|
+
isIdentifierNamed,
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from './ast-utils.mjs';
|
|
7
13
|
import {
|
|
8
14
|
buildImportFixes,
|
|
9
15
|
getNamedImports,
|
|
@@ -65,8 +71,6 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
65
71
|
|
|
66
72
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
67
73
|
|
|
68
|
-
const services = sourceCode.parserServices;
|
|
69
|
-
|
|
70
74
|
const mut_nodesToFix: {
|
|
71
75
|
node: TSESTree.CallExpression;
|
|
72
76
|
argExpression: TSESTree.Expression;
|
|
@@ -74,7 +78,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
74
78
|
|
|
75
79
|
return {
|
|
76
80
|
CallExpression: (node) => {
|
|
77
|
-
const
|
|
81
|
+
const callee = skipTypeWrappers(node.callee);
|
|
78
82
|
|
|
79
83
|
// Match `parseFloat(...)` (global function).
|
|
80
84
|
const isGlobalParseFloat =
|
|
@@ -85,8 +89,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
85
89
|
const isNumberParseFloat =
|
|
86
90
|
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
87
91
|
!callee.computed &&
|
|
88
|
-
callee.object
|
|
89
|
-
callee.object.name === 'Number' &&
|
|
92
|
+
isIdentifierNamed(callee.object, 'Number') &&
|
|
90
93
|
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
91
94
|
callee.property.name === 'parseFloat';
|
|
92
95
|
|
|
@@ -94,31 +97,32 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
94
97
|
const isNumberCall =
|
|
95
98
|
callee.type === AST_NODE_TYPES.Identifier && callee.name === 'Number';
|
|
96
99
|
|
|
97
|
-
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall)
|
|
100
|
+
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
98
103
|
|
|
99
104
|
const args = node.arguments;
|
|
100
105
|
|
|
101
106
|
const firstArg = args[0];
|
|
102
107
|
|
|
103
|
-
if (firstArg === undefined)
|
|
108
|
+
if (firstArg === undefined) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
104
111
|
|
|
105
112
|
// Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
|
|
106
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement)
|
|
113
|
+
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
107
116
|
|
|
108
117
|
// The argument must be purely `string` so the autofix is type-safe
|
|
109
|
-
// against `SafeNumber.parse(value: string)
|
|
110
|
-
//
|
|
111
|
-
|
|
118
|
+
// against `SafeNumber.parse(value: string)`, and that of the value, not
|
|
119
|
+
// of a cast: `Number(true as unknown as string)` is 1, the rewrite NaN.
|
|
120
|
+
// Without type information, skip.
|
|
121
|
+
const argType = getValueType(sourceCode, firstArg);
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (tsNode === undefined) return;
|
|
118
|
-
|
|
119
|
-
const argType = checker.getTypeAtLocation(tsNode);
|
|
120
|
-
|
|
121
|
-
if (!isStringType(argType)) return;
|
|
123
|
+
if (argType === undefined || !isStringType(argType)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
122
126
|
|
|
123
127
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
124
128
|
},
|
|
@@ -133,7 +137,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
133
137
|
index,
|
|
134
138
|
{ node, argExpression },
|
|
135
139
|
] of mut_nodesToFix.entries()) {
|
|
136
|
-
const argText = sourceCode
|
|
140
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
137
141
|
|
|
138
142
|
context.report({
|
|
139
143
|
node,
|
|
@@ -107,3 +107,100 @@ describe('prefer-safe-number-parse', () => {
|
|
|
107
107
|
],
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
describe('prefer-safe-number-parse through type wrappers', () => {
|
|
112
|
+
tester.run('prefer-safe-number-parse', preferSafeNumberParse, {
|
|
113
|
+
valid: [
|
|
114
|
+
{
|
|
115
|
+
name: 'ignores a boolean cast to string, which Number() reads as 0 or 1',
|
|
116
|
+
code: dedent`
|
|
117
|
+
declare const b: boolean;
|
|
118
|
+
const n = Number(b as unknown as string);
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'ignores a number cast to string with `<T>`',
|
|
123
|
+
code: dedent`
|
|
124
|
+
declare const x: number;
|
|
125
|
+
const n = Number(<string>(<unknown>x));
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'ignores a union made to look like a string with `!`',
|
|
130
|
+
code: dedent`
|
|
131
|
+
declare const x: string | number | undefined;
|
|
132
|
+
const n = parseFloat(x! as string);
|
|
133
|
+
`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
invalid: [
|
|
137
|
+
{
|
|
138
|
+
name: 'keeps `satisfies` on a string argument',
|
|
139
|
+
code: dedent`
|
|
140
|
+
declare const s: string;
|
|
141
|
+
const n = Number(s satisfies string);
|
|
142
|
+
`,
|
|
143
|
+
output: dedent`
|
|
144
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
145
|
+
declare const s: string;
|
|
146
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s satisfies string), Number.NaN);
|
|
147
|
+
`,
|
|
148
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'reads the callee through `as` and `!`',
|
|
152
|
+
code: dedent`
|
|
153
|
+
declare const s: string;
|
|
154
|
+
const a = (Number as NumberConstructor)(s);
|
|
155
|
+
const b = Number!.parseFloat(s);
|
|
156
|
+
`,
|
|
157
|
+
output: dedent`
|
|
158
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
159
|
+
declare const s: string;
|
|
160
|
+
const a = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
161
|
+
const b = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
162
|
+
`,
|
|
163
|
+
errors: [
|
|
164
|
+
{ messageId: 'useSafeNumberParse' },
|
|
165
|
+
{ messageId: 'useSafeNumberParse' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'reads `!` on an optional string as the string it asserts',
|
|
170
|
+
code: dedent`
|
|
171
|
+
declare const s: string | undefined;
|
|
172
|
+
const n = parseFloat(s!);
|
|
173
|
+
`,
|
|
174
|
+
output: dedent`
|
|
175
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
176
|
+
declare const s: string | undefined;
|
|
177
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s!), Number.NaN);
|
|
178
|
+
`,
|
|
179
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('prefer-safe-number-parse with parenthesized operands', () => {
|
|
186
|
+
tester.run('prefer-safe-number-parse', preferSafeNumberParse, {
|
|
187
|
+
valid: [],
|
|
188
|
+
invalid: [
|
|
189
|
+
{
|
|
190
|
+
name: 'keeps the parentheses of a sequence argument',
|
|
191
|
+
code: dedent`
|
|
192
|
+
declare const a: number;
|
|
193
|
+
declare const s: string;
|
|
194
|
+
const n = parseFloat((a, s));
|
|
195
|
+
`,
|
|
196
|
+
output: dedent`
|
|
197
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
198
|
+
declare const a: number;
|
|
199
|
+
declare const s: string;
|
|
200
|
+
const n = Result.unwrapOkOr(SafeNumber.parse((a, s)), Number.NaN);
|
|
201
|
+
`,
|
|
202
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
});
|
package/src/rules/rules.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ESLintPlugin } from '../types.mjs';
|
|
2
2
|
import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
3
3
|
import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
|
|
4
4
|
import { preferSafeArrayIsArray } from './prefer-safe-array-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
|
|