eslint-plugin-ts-data-forge 0.8.2 → 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 +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 +3 -3
- 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
|
@@ -169,3 +169,139 @@ describe('prefer-comparison-over-nullish-guard', () => {
|
|
|
169
169
|
},
|
|
170
170
|
);
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
describe('prefer-comparison-over-nullish-guard through type wrappers', () => {
|
|
174
|
+
tester.run(
|
|
175
|
+
'prefer-comparison-over-nullish-guard',
|
|
176
|
+
preferComparisonOverNullishGuard,
|
|
177
|
+
{
|
|
178
|
+
valid: [],
|
|
179
|
+
invalid: [
|
|
180
|
+
{
|
|
181
|
+
name: 'the call wrapped in `satisfies`, `as` or `<T>`',
|
|
182
|
+
code: dedent`
|
|
183
|
+
import { isNull, isNotUndefined } from 'ts-data-forge';
|
|
184
|
+
declare const x: string | null | undefined;
|
|
185
|
+
const a = isNull(x) satisfies boolean;
|
|
186
|
+
const b = isNotUndefined(x) as boolean;
|
|
187
|
+
const c = <boolean>isNull(x);
|
|
188
|
+
`,
|
|
189
|
+
output: dedent`
|
|
190
|
+
import { isNull, isNotUndefined } from 'ts-data-forge';
|
|
191
|
+
declare const x: string | null | undefined;
|
|
192
|
+
const a = (x === null) satisfies boolean;
|
|
193
|
+
const b = (x !== undefined) as boolean;
|
|
194
|
+
const c = <boolean>(x === null);
|
|
195
|
+
`,
|
|
196
|
+
errors: [
|
|
197
|
+
{ messageId: 'preferComparison' },
|
|
198
|
+
{ messageId: 'preferComparison' },
|
|
199
|
+
{ messageId: 'preferComparison' },
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'a wrapped argument is parenthesized under `===`',
|
|
204
|
+
code: dedent`
|
|
205
|
+
import { isNull } from 'ts-data-forge';
|
|
206
|
+
declare const x: unknown;
|
|
207
|
+
const a = isNull(x as string | null);
|
|
208
|
+
const b = isNull(x satisfies unknown);
|
|
209
|
+
const c = isNull(x!);
|
|
210
|
+
`,
|
|
211
|
+
output: dedent`
|
|
212
|
+
import { isNull } from 'ts-data-forge';
|
|
213
|
+
declare const x: unknown;
|
|
214
|
+
const a = (x as string | null) === null;
|
|
215
|
+
const b = (x satisfies unknown) === null;
|
|
216
|
+
const c = x! === null;
|
|
217
|
+
`,
|
|
218
|
+
errors: [
|
|
219
|
+
{ messageId: 'preferComparison' },
|
|
220
|
+
{ messageId: 'preferComparison' },
|
|
221
|
+
{ messageId: 'preferComparison' },
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'the guard itself reached through a wrapper',
|
|
226
|
+
code: dedent`
|
|
227
|
+
import { isNull } from 'ts-data-forge';
|
|
228
|
+
declare const x: string | null | undefined;
|
|
229
|
+
const a = (isNull as (u: unknown) => boolean)(x);
|
|
230
|
+
`,
|
|
231
|
+
output: dedent`
|
|
232
|
+
import { isNull } from 'ts-data-forge';
|
|
233
|
+
declare const x: string | null | undefined;
|
|
234
|
+
const a = x === null;
|
|
235
|
+
`,
|
|
236
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: 'the namespace reached through a wrapper',
|
|
240
|
+
code: dedent`
|
|
241
|
+
import * as tf from 'ts-data-forge';
|
|
242
|
+
declare const x: string | null | undefined;
|
|
243
|
+
const b = (tf as typeof tf).isNotUndefined(x);
|
|
244
|
+
`,
|
|
245
|
+
output: dedent`
|
|
246
|
+
import * as tf from 'ts-data-forge';
|
|
247
|
+
declare const x: string | null | undefined;
|
|
248
|
+
const b = x !== undefined;
|
|
249
|
+
`,
|
|
250
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
251
|
+
},
|
|
252
|
+
],
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('prefer-comparison-over-nullish-guard with parenthesized operands', () => {
|
|
258
|
+
tester.run(
|
|
259
|
+
'prefer-comparison-over-nullish-guard',
|
|
260
|
+
preferComparisonOverNullishGuard,
|
|
261
|
+
{
|
|
262
|
+
valid: [],
|
|
263
|
+
invalid: [
|
|
264
|
+
{
|
|
265
|
+
name: 'bitwise operators bind looser than `===`',
|
|
266
|
+
code: dedent`
|
|
267
|
+
import { isNull } from 'ts-data-forge';
|
|
268
|
+
declare const a: number;
|
|
269
|
+
declare const b: number;
|
|
270
|
+
const p = isNull(a | b);
|
|
271
|
+
const q = isNull(a ^ b);
|
|
272
|
+
const r = isNull((a & b));
|
|
273
|
+
`,
|
|
274
|
+
output: dedent`
|
|
275
|
+
import { isNull } from 'ts-data-forge';
|
|
276
|
+
declare const a: number;
|
|
277
|
+
declare const b: number;
|
|
278
|
+
const p = (a | b) === null;
|
|
279
|
+
const q = (a ^ b) === null;
|
|
280
|
+
const r = (a & b) === null;
|
|
281
|
+
`,
|
|
282
|
+
errors: [
|
|
283
|
+
{ messageId: 'preferComparison' },
|
|
284
|
+
{ messageId: 'preferComparison' },
|
|
285
|
+
{ messageId: 'preferComparison' },
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'an equality argument is parenthesized for readability',
|
|
290
|
+
code: dedent`
|
|
291
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
292
|
+
declare const a: unknown;
|
|
293
|
+
declare const b: unknown;
|
|
294
|
+
const p = isNotUndefined(a === b);
|
|
295
|
+
`,
|
|
296
|
+
output: dedent`
|
|
297
|
+
import { isNotUndefined } from 'ts-data-forge';
|
|
298
|
+
declare const a: unknown;
|
|
299
|
+
declare const b: unknown;
|
|
300
|
+
const p = (a === b) !== undefined;
|
|
301
|
+
`,
|
|
302
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
7
|
import {
|
|
8
8
|
buildImportFixes,
|
|
9
9
|
getNamedImports,
|
|
@@ -88,8 +88,15 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
88
88
|
defaultOptions: [],
|
|
89
89
|
} as const;
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The name of the value checked by `typeof u === 'object' && u !== null`, or
|
|
93
|
+
* `undefined`. Every operand is read through type wrappers:
|
|
94
|
+
* `(u satisfies unknown) !== null` and `u! !== null` check the same `u`.
|
|
95
|
+
*/
|
|
91
96
|
const getNonNullObjectIdentifierName = (
|
|
92
|
-
|
|
97
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
99
|
+
node: TSESTree.LogicalExpression,
|
|
93
100
|
): string | undefined => {
|
|
94
101
|
if (node.operator !== '&&') {
|
|
95
102
|
return undefined;
|
|
@@ -107,7 +114,8 @@ const getNonNullObjectIdentifierName = (
|
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
const getTypeofObjectIdentifierName = (
|
|
110
|
-
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
118
|
+
node: TSESTree.Expression,
|
|
111
119
|
): string | undefined => {
|
|
112
120
|
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
113
121
|
return undefined;
|
|
@@ -117,19 +125,29 @@ const getTypeofObjectIdentifierName = (
|
|
|
117
125
|
return undefined;
|
|
118
126
|
}
|
|
119
127
|
|
|
120
|
-
const
|
|
128
|
+
const left = skipTypeWrappers(node.left);
|
|
121
129
|
|
|
122
|
-
|
|
130
|
+
const right = skipTypeWrappers(node.right);
|
|
131
|
+
|
|
132
|
+
if (
|
|
133
|
+
left.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
123
134
|
left.operator !== 'typeof' ||
|
|
124
135
|
right.type !== AST_NODE_TYPES.Literal ||
|
|
125
|
-
right.value !== 'object'
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
right.value !== 'object'
|
|
137
|
+
) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const argument = skipTypeWrappers(left.argument);
|
|
142
|
+
|
|
143
|
+
return argument.type === AST_NODE_TYPES.Identifier
|
|
144
|
+
? argument.name
|
|
145
|
+
: undefined;
|
|
129
146
|
};
|
|
130
147
|
|
|
131
148
|
const getNonNullCheckIdentifierName = (
|
|
132
|
-
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
150
|
+
node: TSESTree.Expression,
|
|
133
151
|
): string | undefined => {
|
|
134
152
|
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
135
153
|
return undefined;
|
|
@@ -139,7 +157,9 @@ const getNonNullCheckIdentifierName = (
|
|
|
139
157
|
return undefined;
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
const
|
|
160
|
+
const left = skipTypeWrappers(node.left);
|
|
161
|
+
|
|
162
|
+
const right = skipTypeWrappers(node.right);
|
|
143
163
|
|
|
144
164
|
return left.type !== AST_NODE_TYPES.Identifier ||
|
|
145
165
|
right.type !== AST_NODE_TYPES.Literal ||
|
|
@@ -154,3 +154,45 @@ describe('prefer-is-non-null-object', () => {
|
|
|
154
154
|
],
|
|
155
155
|
});
|
|
156
156
|
}, 20000);
|
|
157
|
+
|
|
158
|
+
describe('prefer-is-non-null-object through type wrappers', () => {
|
|
159
|
+
tester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
160
|
+
valid: [
|
|
161
|
+
{
|
|
162
|
+
name: 'checks on two different values, one wrapped',
|
|
163
|
+
code: dedent`
|
|
164
|
+
declare const u: unknown;
|
|
165
|
+
declare const v: unknown;
|
|
166
|
+
const ok = typeof u === 'object' && (v satisfies unknown) !== null;
|
|
167
|
+
`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
invalid: [
|
|
171
|
+
{
|
|
172
|
+
name: 'the value wrapped on either side',
|
|
173
|
+
code: dedent`
|
|
174
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
175
|
+
declare const u: unknown;
|
|
176
|
+
const a = typeof u === 'object' && (u satisfies unknown) !== null;
|
|
177
|
+
const b = typeof u === 'object' && u! !== null;
|
|
178
|
+
const c = typeof (u satisfies unknown) === 'object' && u !== null;
|
|
179
|
+
const d = typeof u === ('object' as const) && u !== (null as null);
|
|
180
|
+
`,
|
|
181
|
+
output: dedent`
|
|
182
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
183
|
+
declare const u: unknown;
|
|
184
|
+
const a = isNonNullObject(u);
|
|
185
|
+
const b = isNonNullObject(u);
|
|
186
|
+
const c = isNonNullObject(u);
|
|
187
|
+
const d = isNonNullObject(u);
|
|
188
|
+
`,
|
|
189
|
+
errors: [
|
|
190
|
+
{ messageId: 'useIsNonNullObject' },
|
|
191
|
+
{ messageId: 'useIsNonNullObject' },
|
|
192
|
+
{ messageId: 'useIsNonNullObject' },
|
|
193
|
+
{ messageId: 'useIsNonNullObject' },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
|
198
|
+
}, 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 { typeWrapperLayers } from './ast-utils.mjs';
|
|
8
9
|
import {
|
|
9
10
|
buildImportFixes,
|
|
10
11
|
getNamedImports,
|
|
@@ -58,12 +59,16 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
58
59
|
return false;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// At every layer of type wrappers (see typeWrapperLayers): an array
|
|
63
|
+
// cast to `Record<string, unknown>` still needs its `isRecord`.
|
|
64
|
+
return typeWrapperLayers(expression).every((layer) => {
|
|
65
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
return (
|
|
68
|
+
tsNode !== undefined &&
|
|
69
|
+
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
70
|
+
);
|
|
71
|
+
});
|
|
67
72
|
};
|
|
68
73
|
|
|
69
74
|
const mut_nodesToFix: {
|
|
@@ -289,3 +289,37 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
289
289
|
],
|
|
290
290
|
});
|
|
291
291
|
}, 20000);
|
|
292
|
+
|
|
293
|
+
describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
294
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
295
|
+
valid: [],
|
|
296
|
+
invalid: [
|
|
297
|
+
{
|
|
298
|
+
name: 'keeps isRecord for an array cast to a record: the value is still an array',
|
|
299
|
+
code: dedent`
|
|
300
|
+
declare const arr: readonly string[];
|
|
301
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
302
|
+
`,
|
|
303
|
+
output: dedent`
|
|
304
|
+
import { isRecord, hasKey } from 'ts-data-forge';
|
|
305
|
+
declare const arr: readonly string[];
|
|
306
|
+
const ok = (isRecord(arr as unknown as Record<string, unknown>) && hasKey(arr as unknown as Record<string, unknown>, '0'));
|
|
307
|
+
`,
|
|
308
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: 'keeps isRecord for a record widened to include an array: the author says it may be one',
|
|
312
|
+
code: dedent`
|
|
313
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
314
|
+
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly unknown[], 'a');
|
|
315
|
+
`,
|
|
316
|
+
output: dedent`
|
|
317
|
+
import { isRecord, hasKey } from 'ts-data-forge';
|
|
318
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
319
|
+
const ok = (isRecord(obj as Readonly<Record<string, unknown>> | readonly unknown[]) && hasKey(obj as Readonly<Record<string, unknown>> | readonly unknown[], 'a'));
|
|
320
|
+
`,
|
|
321
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -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
|
{
|
|
@@ -117,15 +120,25 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
117
120
|
|
|
118
121
|
const checker = services.program.getTypeChecker();
|
|
119
122
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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)) {
|
|
129
142
|
return;
|
|
130
143
|
}
|
|
131
144
|
|
|
@@ -142,7 +155,7 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
142
155
|
index,
|
|
143
156
|
{ node, argExpression },
|
|
144
157
|
] of mut_nodesToFix.entries()) {
|
|
145
|
-
const argText =
|
|
158
|
+
const argText = toArgumentText(argExpression, sourceCode);
|
|
146
159
|
|
|
147
160
|
const originalText = sourceCode.getText(node);
|
|
148
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,
|
|
@@ -101,9 +106,11 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
101
106
|
return;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
// 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`).
|
|
105
111
|
if (args.length >= 2) {
|
|
106
|
-
const radix =
|
|
112
|
+
const radix =
|
|
113
|
+
args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
|
|
107
114
|
|
|
108
115
|
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
109
116
|
return;
|
|
@@ -111,22 +118,29 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
111
118
|
}
|
|
112
119
|
|
|
113
120
|
// The argument must be a `string` so the autofix is type-safe against
|
|
114
|
-
// `Num.safeParseInt(s: string)`. Without type information, skip.
|
|
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.
|
|
115
124
|
if (services?.program == null) {
|
|
116
125
|
return;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
const checker = services.program.getTypeChecker();
|
|
120
129
|
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
136
|
|
|
127
|
-
|
|
137
|
+
return (
|
|
138
|
+
tsNode !== undefined &&
|
|
139
|
+
isStringType(checker.getTypeAtLocation(tsNode))
|
|
140
|
+
);
|
|
141
|
+
};
|
|
128
142
|
|
|
129
|
-
if (!
|
|
143
|
+
if (!typeWrapperLayers(firstArg).every(isString)) {
|
|
130
144
|
return;
|
|
131
145
|
}
|
|
132
146
|
|
|
@@ -143,7 +157,7 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
143
157
|
index,
|
|
144
158
|
{ node, argExpression },
|
|
145
159
|
] of mut_nodesToFix.entries()) {
|
|
146
|
-
const argText =
|
|
160
|
+
const argText = toArgumentText(argExpression, sourceCode);
|
|
147
161
|
|
|
148
162
|
const originalText = sourceCode.getText(node);
|
|
149
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,
|