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
|
@@ -305,3 +305,102 @@ describe('prefer-canonical-length-guard', () => {
|
|
|
305
305
|
],
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
+
|
|
309
|
+
describe('prefer-canonical-length-guard through type wrappers', () => {
|
|
310
|
+
tester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
311
|
+
valid: [
|
|
312
|
+
{
|
|
313
|
+
name: 'a wrapped bound other than the degenerate one',
|
|
314
|
+
code: dedent`
|
|
315
|
+
import { Arr } from 'ts-data-forge';
|
|
316
|
+
|
|
317
|
+
declare const xs: readonly number[];
|
|
318
|
+
const ok = Arr.isFixedLengthArray(1 as const, xs);
|
|
319
|
+
`,
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
invalid: [
|
|
323
|
+
{
|
|
324
|
+
name: 'a bound wrapped in `as const` or `satisfies`',
|
|
325
|
+
code: dedent`
|
|
326
|
+
import { Arr } from 'ts-data-forge';
|
|
327
|
+
|
|
328
|
+
declare const xs: readonly number[];
|
|
329
|
+
const a = Arr.isFixedLengthArray(0 as const, xs);
|
|
330
|
+
const b = Arr.isBoundedLengthTuple(0 satisfies number, 0, xs!);
|
|
331
|
+
`,
|
|
332
|
+
output: dedent`
|
|
333
|
+
import { Arr } from 'ts-data-forge';
|
|
334
|
+
|
|
335
|
+
declare const xs: readonly number[];
|
|
336
|
+
const a = Arr.isEmpty(xs);
|
|
337
|
+
const b = Arr.isEmptyTuple(xs!);
|
|
338
|
+
`,
|
|
339
|
+
errors: [
|
|
340
|
+
{ messageId: 'useCanonicalGuard' },
|
|
341
|
+
{ messageId: 'useCanonicalGuard' },
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
typedTester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
348
|
+
valid: [
|
|
349
|
+
{
|
|
350
|
+
name: 'a computed index named `length` is not the length',
|
|
351
|
+
code: dedent`
|
|
352
|
+
declare const xs: readonly number[];
|
|
353
|
+
declare const length: number;
|
|
354
|
+
const ok = xs[length] > 0;
|
|
355
|
+
`,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
invalid: [
|
|
359
|
+
{
|
|
360
|
+
name: 'a folded-in comparison on a wrapped length',
|
|
361
|
+
code: dedent`
|
|
362
|
+
import { Arr } from 'ts-data-forge';
|
|
363
|
+
declare const xs: readonly number[];
|
|
364
|
+
const a = (xs.length satisfies number) > 0;
|
|
365
|
+
const b = xs.length >= 1 && (xs as readonly number[]).length <= 3;
|
|
366
|
+
`,
|
|
367
|
+
output: dedent`
|
|
368
|
+
import { Arr } from 'ts-data-forge';
|
|
369
|
+
declare const xs: readonly number[];
|
|
370
|
+
const a = Arr.isNonEmpty(xs);
|
|
371
|
+
const b = Arr.isBoundedLengthArray(1, 3, xs);
|
|
372
|
+
`,
|
|
373
|
+
errors: [
|
|
374
|
+
{ messageId: 'useIsNonEmpty' },
|
|
375
|
+
{ messageId: 'useIsBoundedLengthArray' },
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe('prefer-canonical-length-guard with parenthesized operands', () => {
|
|
383
|
+
tester.run('prefer-canonical-length-guard', preferCanonicalLengthGuard, {
|
|
384
|
+
valid: [],
|
|
385
|
+
invalid: [
|
|
386
|
+
{
|
|
387
|
+
name: 'a sequence array keeps its parentheses',
|
|
388
|
+
code: dedent`
|
|
389
|
+
import { Arr } from 'ts-data-forge';
|
|
390
|
+
|
|
391
|
+
declare const xs: readonly number[];
|
|
392
|
+
declare const log: () => void;
|
|
393
|
+
const ok = Arr.isFixedLengthArray(0, (log(), xs));
|
|
394
|
+
`,
|
|
395
|
+
output: dedent`
|
|
396
|
+
import { Arr } from 'ts-data-forge';
|
|
397
|
+
|
|
398
|
+
declare const xs: readonly number[];
|
|
399
|
+
declare const log: () => void;
|
|
400
|
+
const ok = Arr.isEmpty((log(), xs));
|
|
401
|
+
`,
|
|
402
|
+
errors: [{ messageId: 'useCanonicalGuard' }],
|
|
403
|
+
},
|
|
404
|
+
],
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
|
|
7
7
|
import { buildCalleeResolver, getTsDataForgeImport } from './import-utils.mjs';
|
|
8
8
|
|
|
9
9
|
type Options = readonly [];
|
|
@@ -29,7 +29,11 @@ const COMPARISON_GUARDS: ReadonlyRecord<string, ComparisonSpec> = {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Argument expression kinds whose precedence is lower than equality, so they
|
|
32
|
-
* must be parenthesized when placed on the left of `===` / `!==`.
|
|
32
|
+
* must be parenthesized when placed on the left of `===` / `!==`. A node's text
|
|
33
|
+
* never includes the parentheses the source wrote around it, so this is decided
|
|
34
|
+
* on the kind alone. `as` / `satisfies` and a nested equality would parse as
|
|
35
|
+
* intended, but are parenthesized so that the type or the grouping is not read
|
|
36
|
+
* as part of the comparison.
|
|
33
37
|
*/
|
|
34
38
|
const ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([
|
|
35
39
|
AST_NODE_TYPES.LogicalExpression,
|
|
@@ -38,11 +42,33 @@ const ARG_NEEDS_PARENS: ReadonlySet<string> = new Set([
|
|
|
38
42
|
AST_NODE_TYPES.SequenceExpression,
|
|
39
43
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
40
44
|
AST_NODE_TYPES.YieldExpression,
|
|
45
|
+
AST_NODE_TYPES.TSAsExpression,
|
|
46
|
+
AST_NODE_TYPES.TSSatisfiesExpression,
|
|
41
47
|
]);
|
|
42
48
|
|
|
49
|
+
/** Binary operators that bind no tighter than `===` / `!==`. */
|
|
50
|
+
const LOOSE_BINARY_OPERATORS: ReadonlySet<string> = new Set([
|
|
51
|
+
'|',
|
|
52
|
+
'^',
|
|
53
|
+
'&',
|
|
54
|
+
'==',
|
|
55
|
+
'!=',
|
|
56
|
+
'===',
|
|
57
|
+
'!==',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const argumentNeedsParens = (
|
|
61
|
+
argument: DeepReadonly<TSESTree.Expression>,
|
|
62
|
+
): boolean =>
|
|
63
|
+
ARG_NEEDS_PARENS.has(argument.type) ||
|
|
64
|
+
(argument.type === AST_NODE_TYPES.BinaryExpression &&
|
|
65
|
+
LOOSE_BINARY_OPERATORS.has(argument.operator));
|
|
66
|
+
|
|
43
67
|
/**
|
|
44
68
|
* Returns `true` when the comparison must be wrapped in parentheses to preserve
|
|
45
|
-
* the original grouping given the call expression's parent context.
|
|
69
|
+
* the original grouping given the call expression's parent context. A type
|
|
70
|
+
* wrapper around the call is one such context: `x === null satisfies boolean`
|
|
71
|
+
* would apply the wrapper to `null`.
|
|
46
72
|
*/
|
|
47
73
|
const needsWrappingInParent = (
|
|
48
74
|
node: DeepReadonly<TSESTree.CallExpression>,
|
|
@@ -56,6 +82,9 @@ const needsWrappingInParent = (
|
|
|
56
82
|
case AST_NODE_TYPES.AwaitExpression:
|
|
57
83
|
case AST_NODE_TYPES.BinaryExpression:
|
|
58
84
|
case AST_NODE_TYPES.TSNonNullExpression:
|
|
85
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
86
|
+
case AST_NODE_TYPES.TSSatisfiesExpression:
|
|
87
|
+
case AST_NODE_TYPES.TSTypeAssertion:
|
|
59
88
|
return true;
|
|
60
89
|
|
|
61
90
|
case AST_NODE_TYPES.MemberExpression:
|
|
@@ -97,7 +126,9 @@ export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
|
|
|
97
126
|
|
|
98
127
|
return {
|
|
99
128
|
CallExpression: (node) => {
|
|
100
|
-
if (node.arguments.length !== 1)
|
|
129
|
+
if (node.arguments.length !== 1) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
101
132
|
|
|
102
133
|
const argument = node.arguments[0];
|
|
103
134
|
|
|
@@ -110,17 +141,19 @@ export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
|
|
|
110
141
|
|
|
111
142
|
const resolved = resolveGuard(node.callee);
|
|
112
143
|
|
|
113
|
-
if (resolved === undefined)
|
|
144
|
+
if (resolved === undefined) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
114
147
|
|
|
115
148
|
const spec = COMPARISON_GUARDS[resolved.canonicalName];
|
|
116
149
|
|
|
117
|
-
if (spec === undefined)
|
|
150
|
+
if (spec === undefined) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
118
153
|
|
|
119
154
|
const argRaw = sourceCode.getText(argument);
|
|
120
155
|
|
|
121
|
-
const argText =
|
|
122
|
-
? `(${argRaw})`
|
|
123
|
-
: argRaw;
|
|
156
|
+
const argText = argumentNeedsParens(argument) ? `(${argRaw})` : argRaw;
|
|
124
157
|
|
|
125
158
|
const comparison = `${argText} ${spec.op} ${spec.literal}`;
|
|
126
159
|
|
|
@@ -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,
|
|
@@ -45,7 +45,9 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
45
45
|
LogicalExpression: (node) => {
|
|
46
46
|
const identifierName = getNonNullObjectIdentifierName(node);
|
|
47
47
|
|
|
48
|
-
if (identifierName === undefined)
|
|
48
|
+
if (identifierName === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
mut_nodesToFix.push({ node, identifierName });
|
|
51
53
|
},
|
|
@@ -86,60 +88,82 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
86
88
|
defaultOptions: [],
|
|
87
89
|
} as const;
|
|
88
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
|
+
*/
|
|
89
96
|
const getNonNullObjectIdentifierName = (
|
|
90
|
-
|
|
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,
|
|
91
100
|
): string | undefined => {
|
|
92
|
-
if (node.operator !== '&&')
|
|
101
|
+
if (node.operator !== '&&') {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
93
104
|
|
|
94
105
|
const leftIdentifierName = getTypeofObjectIdentifierName(node.left);
|
|
95
106
|
|
|
96
107
|
const rightIdentifierName = getNonNullCheckIdentifierName(node.right);
|
|
97
108
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return leftIdentifierName;
|
|
109
|
+
return leftIdentifierName === undefined ||
|
|
110
|
+
rightIdentifierName === undefined ||
|
|
111
|
+
leftIdentifierName !== rightIdentifierName
|
|
112
|
+
? undefined
|
|
113
|
+
: leftIdentifierName;
|
|
105
114
|
};
|
|
106
115
|
|
|
107
116
|
const getTypeofObjectIdentifierName = (
|
|
108
|
-
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
118
|
+
node: TSESTree.Expression,
|
|
109
119
|
): string | undefined => {
|
|
110
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression)
|
|
120
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
111
123
|
|
|
112
|
-
if (node.operator !== '===')
|
|
124
|
+
if (node.operator !== '===') {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
113
127
|
|
|
114
|
-
const
|
|
128
|
+
const left = skipTypeWrappers(node.left);
|
|
115
129
|
|
|
116
|
-
|
|
130
|
+
const right = skipTypeWrappers(node.right);
|
|
117
131
|
|
|
118
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
left.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
134
|
+
left.operator !== 'typeof' ||
|
|
135
|
+
right.type !== AST_NODE_TYPES.Literal ||
|
|
136
|
+
right.value !== 'object'
|
|
137
|
+
) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
119
140
|
|
|
120
|
-
|
|
141
|
+
const argument = skipTypeWrappers(left.argument);
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return left.argument.name;
|
|
143
|
+
return argument.type === AST_NODE_TYPES.Identifier
|
|
144
|
+
? argument.name
|
|
145
|
+
: undefined;
|
|
127
146
|
};
|
|
128
147
|
|
|
129
148
|
const getNonNullCheckIdentifierName = (
|
|
130
|
-
|
|
149
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
150
|
+
node: TSESTree.Expression,
|
|
131
151
|
): string | undefined => {
|
|
132
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const { left, right } = node;
|
|
152
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
137
155
|
|
|
138
|
-
if (
|
|
156
|
+
if (node.operator !== '!==') {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
const left = skipTypeWrappers(node.left);
|
|
141
161
|
|
|
142
|
-
|
|
162
|
+
const right = skipTypeWrappers(node.right);
|
|
143
163
|
|
|
144
|
-
return left.
|
|
164
|
+
return left.type !== AST_NODE_TYPES.Identifier ||
|
|
165
|
+
right.type !== AST_NODE_TYPES.Literal ||
|
|
166
|
+
right.value !== null
|
|
167
|
+
? undefined
|
|
168
|
+
: left.name;
|
|
145
169
|
};
|
|
@@ -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,
|
|
@@ -54,14 +55,20 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
54
55
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
55
56
|
expression: TSESTree.Expression,
|
|
56
57
|
): boolean => {
|
|
57
|
-
if (checker === undefined)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
if (checker === undefined) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
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);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
tsNode !== undefined &&
|
|
69
|
+
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
70
|
+
);
|
|
71
|
+
});
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
const mut_nodesToFix: {
|
|
@@ -84,13 +91,17 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// Check arguments: Object.hasOwn(obj, key)
|
|
87
|
-
if (node.arguments.length !== 2)
|
|
94
|
+
if (node.arguments.length !== 2) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
88
97
|
|
|
89
98
|
const objArg = node.arguments[0];
|
|
90
99
|
|
|
91
100
|
const keyArg = node.arguments[1];
|
|
92
101
|
|
|
93
|
-
if (objArg === undefined || keyArg === undefined)
|
|
102
|
+
if (objArg === undefined || keyArg === undefined) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
94
105
|
|
|
95
106
|
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
96
107
|
if (
|
|
@@ -109,14 +120,18 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
109
120
|
|
|
110
121
|
// Handle key in obj
|
|
111
122
|
BinaryExpression: (node) => {
|
|
112
|
-
if (node.operator !== 'in')
|
|
123
|
+
if (node.operator !== 'in') {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
113
126
|
|
|
114
127
|
const keyExpression = node.left;
|
|
115
128
|
|
|
116
129
|
const objExpression = node.right;
|
|
117
130
|
|
|
118
131
|
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
119
|
-
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier)
|
|
132
|
+
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
120
135
|
|
|
121
136
|
mut_nodesToFix.push({
|
|
122
137
|
node,
|
|
@@ -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
|
+
});
|