eslint-plugin-ts-data-forge 0.8.5 → 0.9.0
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/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- 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 +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -34
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +53 -58
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +7 -5
- package/dist/rules/prefer-arr-is-array.mjs.map +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 +9 -6
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +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 +9 -6
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +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 -6
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +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 -6
- 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 -6
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +8 -11
- 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 +8 -6
- 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 +12 -166
- 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 +6 -8
- 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 +57 -34
- 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 +3 -3
- 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 +4 -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 +3 -2
- 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 +113 -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 +105 -48
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
- 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 +6 -6
- 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 +9 -12
- 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 +104 -23
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -10
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +73 -80
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/no-unnecessary-type-guard.mts +14 -10
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-array.mts +14 -7
- package/src/rules/prefer-arr-is-array.test.mts +14 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -13
- package/src/rules/prefer-arr-scan.mts +24 -19
- package/src/rules/prefer-arr-sum.mts +18 -14
- package/src/rules/prefer-arr-uniq.mts +25 -234
- package/src/rules/prefer-as-int.mts +19 -12
- package/src/rules/prefer-canonical-array-slicing.mts +79 -52
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-canonical-length-cast.mts +7 -3
- package/src/rules/prefer-canonical-length-guard.mts +9 -4
- package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
- package/src/rules/prefer-is-non-null-object.mts +171 -64
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +159 -74
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-num-safe-parse-float.mts +15 -10
- package/src/rules/prefer-num-safe-parse-int.mts +15 -13
- package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
- package/src/rules/prefer-range-for-loop.mts +157 -31
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
- package/dist/rules/import-utils.d.mts +0 -30
- package/dist/rules/import-utils.d.mts.map +0 -1
- package/dist/rules/import-utils.mjs +0 -98
- package/dist/rules/import-utils.mjs.map +0 -1
- package/src/rules/import-utils.mts +0 -175
|
@@ -675,6 +675,61 @@ describe('prefer-canonical-array-slicing through type wrappers', () => {
|
|
|
675
675
|
{ messageId: 'preferCanonicalArraySlicing' },
|
|
676
676
|
],
|
|
677
677
|
},
|
|
678
|
+
{
|
|
679
|
+
name: 'a filter callback whose body is wrapped',
|
|
680
|
+
code: dedent`
|
|
681
|
+
declare const xs: readonly number[];
|
|
682
|
+
const ys = xs.filter((_, i) => (i >= 1) satisfies boolean);
|
|
683
|
+
`,
|
|
684
|
+
output: dedent`
|
|
685
|
+
import { Arr } from 'ts-data-forge';
|
|
686
|
+
declare const xs: readonly number[];
|
|
687
|
+
const ys = Arr.tail(xs);
|
|
688
|
+
`,
|
|
689
|
+
errors: [{ messageId: 'preferCanonicalArraySlicing' }],
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: 'a wrapped `xs.length - N`, and a wrapped `.length` or receiver inside it',
|
|
693
|
+
code: dedent`
|
|
694
|
+
declare const xs: readonly number[];
|
|
695
|
+
const a = xs.slice(0, (xs.length - 1) as number);
|
|
696
|
+
const b = xs.slice(0, (xs as readonly number[]).length - 1);
|
|
697
|
+
const c = xs.filter((_, i) => i < (xs.length satisfies number) - 2);
|
|
698
|
+
const d = xs.toSpliced(0, xs!.length - 2);
|
|
699
|
+
`,
|
|
700
|
+
output: dedent`
|
|
701
|
+
import { Arr } from 'ts-data-forge';
|
|
702
|
+
declare const xs: readonly number[];
|
|
703
|
+
const a = Arr.butLast(xs);
|
|
704
|
+
const b = Arr.butLast(xs);
|
|
705
|
+
const c = Arr.skipLast(xs, 2);
|
|
706
|
+
const d = Arr.takeLast(xs, 2);
|
|
707
|
+
`,
|
|
708
|
+
errors: [
|
|
709
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
710
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
711
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
712
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
713
|
+
],
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: 'a wrapped receiver compared with its own `.length`',
|
|
717
|
+
code: dedent`
|
|
718
|
+
declare const xs: readonly number[];
|
|
719
|
+
const a = (xs satisfies readonly number[]).slice(0, xs.length - 1);
|
|
720
|
+
const b = xs.toSpliced((xs.length as number), 0, 1);
|
|
721
|
+
`,
|
|
722
|
+
output: dedent`
|
|
723
|
+
import { Arr } from 'ts-data-forge';
|
|
724
|
+
declare const xs: readonly number[];
|
|
725
|
+
const a = Arr.butLast(xs satisfies readonly number[]);
|
|
726
|
+
const b = Arr.toPushed(xs, 1);
|
|
727
|
+
`,
|
|
728
|
+
errors: [
|
|
729
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
730
|
+
{ messageId: 'preferCanonicalArraySlicing' },
|
|
731
|
+
],
|
|
732
|
+
},
|
|
678
733
|
],
|
|
679
734
|
});
|
|
680
735
|
});
|
|
@@ -4,8 +4,12 @@ import {
|
|
|
4
4
|
type TSESLint,
|
|
5
5
|
type TSESTree,
|
|
6
6
|
} from '@typescript-eslint/utils';
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
getImportDeclarations,
|
|
9
|
+
getImportedLocalName,
|
|
10
|
+
skipTypeWrappers,
|
|
11
|
+
} from 'eslint-rule-forge';
|
|
12
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
9
13
|
|
|
10
14
|
type Options = readonly [];
|
|
11
15
|
|
|
@@ -143,7 +147,7 @@ export const preferCanonicalLengthCast: TSESLint.RuleModule<
|
|
|
143
147
|
const sourceCode = context.sourceCode;
|
|
144
148
|
|
|
145
149
|
const arrLocalName = getImportedLocalName(
|
|
146
|
-
|
|
150
|
+
getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
|
|
147
151
|
'Arr',
|
|
148
152
|
);
|
|
149
153
|
|
|
@@ -3,9 +3,14 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import {
|
|
7
|
+
getArgumentText,
|
|
8
|
+
getImportDeclarations,
|
|
9
|
+
getImportedLocalName,
|
|
10
|
+
skipTypeWrappers,
|
|
11
|
+
} from 'eslint-rule-forge';
|
|
6
12
|
import type { MutableRecord, ReadonlyRecord } from 'ts-type-forge';
|
|
7
|
-
import {
|
|
8
|
-
import { getImportedLocalName, getTsDataForgeImport } from './import-utils.mjs';
|
|
13
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
9
14
|
import { preferArrIsBoundedLengthArray } from './prefer-arr-is-bounded-length-array.mjs';
|
|
10
15
|
import { preferArrIsFixedLengthArray } from './prefer-arr-is-fixed-length-array.mjs';
|
|
11
16
|
import { preferArrIsMaxLengthArray } from './prefer-arr-is-max-length-array.mjs';
|
|
@@ -117,7 +122,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
117
122
|
const sourceCode = context.sourceCode;
|
|
118
123
|
|
|
119
124
|
const arrLocalName = getImportedLocalName(
|
|
120
|
-
|
|
125
|
+
getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
|
|
121
126
|
'Arr',
|
|
122
127
|
);
|
|
123
128
|
|
|
@@ -154,7 +159,7 @@ export const preferCanonicalLengthGuard: TSESLint.RuleModule<
|
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
const arrayText =
|
|
162
|
+
const arrayText = getArgumentText(sourceCode, array);
|
|
158
163
|
|
|
159
164
|
context.report({
|
|
160
165
|
node,
|
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import { buildCalleeResolver, getImportDeclarations } from 'eslint-rule-forge';
|
|
6
7
|
import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
|
|
7
|
-
import {
|
|
8
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
8
9
|
|
|
9
10
|
type Options = readonly [];
|
|
10
11
|
|
|
@@ -121,7 +122,7 @@ export const preferComparisonOverNullishGuard: TSESLint.RuleModule<
|
|
|
121
122
|
const sourceCode = context.sourceCode;
|
|
122
123
|
|
|
123
124
|
const resolveGuard = buildCalleeResolver(
|
|
124
|
-
|
|
125
|
+
getImportDeclarations(sourceCode.ast, TS_DATA_FORGE_MODULE),
|
|
125
126
|
);
|
|
126
127
|
|
|
127
128
|
return {
|
|
@@ -49,6 +49,22 @@ describe('prefer-comparison-over-nullish-guard', () => {
|
|
|
49
49
|
},
|
|
50
50
|
],
|
|
51
51
|
invalid: [
|
|
52
|
+
{
|
|
53
|
+
name: 'the guard imported by a second ts-data-forge declaration',
|
|
54
|
+
code: dedent`
|
|
55
|
+
import type { Optional } from 'ts-data-forge';
|
|
56
|
+
import { isUndefined } from 'ts-data-forge';
|
|
57
|
+
declare const x: string | undefined;
|
|
58
|
+
const y = isUndefined(x);
|
|
59
|
+
`,
|
|
60
|
+
output: dedent`
|
|
61
|
+
import type { Optional } from 'ts-data-forge';
|
|
62
|
+
import { isUndefined } from 'ts-data-forge';
|
|
63
|
+
declare const x: string | undefined;
|
|
64
|
+
const y = x === undefined;
|
|
65
|
+
`,
|
|
66
|
+
errors: [{ messageId: 'preferComparison' }],
|
|
67
|
+
},
|
|
52
68
|
{
|
|
53
69
|
name: 'isNotUndefined with explicit argument',
|
|
54
70
|
code: dedent`
|
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
6
|
import {
|
|
8
|
-
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getImportDeclarations,
|
|
9
9
|
getNamedImports,
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
getRangeWithParens,
|
|
11
|
+
parenthesesDepth,
|
|
12
|
+
skipTypeWrappers,
|
|
13
|
+
} from 'eslint-rule-forge';
|
|
14
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
12
15
|
|
|
13
16
|
type Options = readonly [];
|
|
14
17
|
|
|
@@ -19,7 +22,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
19
22
|
type: 'suggestion',
|
|
20
23
|
docs: {
|
|
21
24
|
description:
|
|
22
|
-
'Replace `typeof u === "object" && u !== null
|
|
25
|
+
'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-data-forge.',
|
|
23
26
|
},
|
|
24
27
|
fixable: 'code',
|
|
25
28
|
schema: [],
|
|
@@ -34,51 +37,55 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
34
37
|
|
|
35
38
|
const program = sourceCode.ast;
|
|
36
39
|
|
|
37
|
-
const
|
|
40
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
41
|
+
program,
|
|
42
|
+
TS_DATA_FORGE_MODULE,
|
|
43
|
+
);
|
|
38
44
|
|
|
39
|
-
const
|
|
40
|
-
node: TSESTree.LogicalExpression;
|
|
41
|
-
identifierName: string;
|
|
42
|
-
}[] = [];
|
|
45
|
+
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
43
46
|
|
|
44
47
|
return {
|
|
45
48
|
LogicalExpression: (node) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (identifierName === undefined) {
|
|
49
|
+
// A chain is read once, from its outermost `&&`.
|
|
50
|
+
if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
|
|
53
55
|
},
|
|
54
56
|
'Program:exit': () => {
|
|
55
|
-
// Check if isNonNullObject is already imported
|
|
56
57
|
const hasIsNonNullObjectImport =
|
|
57
|
-
getNamedImports(
|
|
58
|
+
getNamedImports(tsDataForgeImports).includes('isNonNullObject');
|
|
58
59
|
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
60
|
+
// The import fix lives on the first report only:
|
|
61
|
+
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
62
|
+
// otherwise and only one of them would survive a pass. A first report
|
|
63
|
+
// silenced by an `eslint-disable` comment therefore takes the import
|
|
64
|
+
// with it.
|
|
63
65
|
for (const [
|
|
64
66
|
index,
|
|
65
|
-
{
|
|
66
|
-
] of
|
|
67
|
+
{ range, identifierName },
|
|
68
|
+
] of mut_pairsToFix.entries()) {
|
|
67
69
|
context.report({
|
|
68
|
-
|
|
70
|
+
loc: {
|
|
71
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
72
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
73
|
+
},
|
|
69
74
|
messageId: 'useIsNonNullObject',
|
|
70
75
|
fix: (fixer) => {
|
|
71
76
|
const replacement = `isNonNullObject(${identifierName})`;
|
|
72
77
|
|
|
73
|
-
// Add import only for the first node and only if not already imported
|
|
74
78
|
const importFixes =
|
|
75
79
|
index === 0 && !hasIsNonNullObjectImport
|
|
76
|
-
?
|
|
80
|
+
? buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
77
81
|
'isNonNullObject',
|
|
78
82
|
])
|
|
79
83
|
: [];
|
|
80
84
|
|
|
81
|
-
return [
|
|
85
|
+
return [
|
|
86
|
+
...importFixes,
|
|
87
|
+
fixer.replaceTextRange(range, replacement),
|
|
88
|
+
];
|
|
82
89
|
},
|
|
83
90
|
});
|
|
84
91
|
}
|
|
@@ -89,39 +96,123 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
89
96
|
} as const;
|
|
90
97
|
|
|
91
98
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
99
|
+
* A `typeof u === 'object'` and a `u !== null` next to each other in an `&&`
|
|
100
|
+
* chain, as the source range from the first to the second (parentheses
|
|
101
|
+
* included) and the name of `u`.
|
|
95
102
|
*/
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
type NonNullObjectPair = Readonly<{
|
|
104
|
+
range: TSESTree.Range;
|
|
105
|
+
identifierName: string;
|
|
106
|
+
}>;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether `node` is an operand of an enclosing `&&` chain: a `&&` written
|
|
110
|
+
* directly under another one, without parentheses.
|
|
111
|
+
*/
|
|
112
|
+
const isChainOperand = (
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
114
|
+
sourceCode: TSESLint.SourceCode,
|
|
98
115
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
99
116
|
node: TSESTree.LogicalExpression,
|
|
100
|
-
):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
): boolean =>
|
|
118
|
+
node.parent.type === AST_NODE_TYPES.LogicalExpression &&
|
|
119
|
+
node.parent.operator === '&&' &&
|
|
120
|
+
parenthesesDepth(sourceCode, node) === 0;
|
|
104
121
|
|
|
105
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
|
|
124
|
+
* two halves may come in either order, and the pair may have operands before
|
|
125
|
+
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
126
|
+
* be next to each other, so that the pair is one range of the source.
|
|
127
|
+
*/
|
|
128
|
+
const findNonNullObjectPairs = (
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
130
|
+
sourceCode: TSESLint.SourceCode,
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
132
|
+
node: TSESTree.LogicalExpression,
|
|
133
|
+
): readonly NonNullObjectPair[] => {
|
|
134
|
+
const operands = chainOperands(sourceCode, node);
|
|
135
|
+
|
|
136
|
+
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
137
|
+
const first = operands[index];
|
|
138
|
+
|
|
139
|
+
const second = operands[index + 1];
|
|
106
140
|
|
|
107
|
-
|
|
141
|
+
if (first === undefined || second === undefined) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
108
144
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
const identifierName =
|
|
146
|
+
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
147
|
+
|
|
148
|
+
return identifierName === undefined
|
|
149
|
+
? pairsFrom(index + 1)
|
|
150
|
+
: [
|
|
151
|
+
{
|
|
152
|
+
range: [
|
|
153
|
+
getRangeWithParens(sourceCode, first)[0],
|
|
154
|
+
getRangeWithParens(sourceCode, second)[1],
|
|
155
|
+
],
|
|
156
|
+
identifierName,
|
|
157
|
+
},
|
|
158
|
+
...pairsFrom(index + 2),
|
|
159
|
+
];
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return pairsFrom(0);
|
|
114
163
|
};
|
|
115
164
|
|
|
116
|
-
|
|
165
|
+
/**
|
|
166
|
+
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
|
|
167
|
+
* is one operand: its halves cannot be replaced without its parentheses.
|
|
168
|
+
*/
|
|
169
|
+
const chainOperands = (
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
171
|
+
sourceCode: TSESLint.SourceCode,
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
173
|
+
node: TSESTree.LogicalExpression,
|
|
174
|
+
): readonly TSESTree.Expression[] =>
|
|
175
|
+
[node.left, node.right].flatMap((operand) =>
|
|
176
|
+
operand.type === AST_NODE_TYPES.LogicalExpression &&
|
|
177
|
+
operand.operator === '&&' &&
|
|
178
|
+
parenthesesDepth(sourceCode, operand) === 0
|
|
179
|
+
? chainOperands(sourceCode, operand)
|
|
180
|
+
: [operand],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
184
|
+
// unknown) === 'object' && u! !== null` is the same check.
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and
|
|
188
|
+
* `nullCheck` is `u !== null` for the same `u`.
|
|
189
|
+
*/
|
|
190
|
+
const pairIdentifierName = (
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
192
|
+
typeofCheck: TSESTree.Expression,
|
|
117
193
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
118
|
-
|
|
194
|
+
nullCheck: TSESTree.Expression,
|
|
119
195
|
): string | undefined => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
196
|
+
const identifierName = getTypeofObjectIdentifierName(
|
|
197
|
+
skipTypeWrappers(typeofCheck),
|
|
198
|
+
);
|
|
123
199
|
|
|
124
|
-
|
|
200
|
+
return identifierName !== undefined &&
|
|
201
|
+
identifierName ===
|
|
202
|
+
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
203
|
+
? identifierName
|
|
204
|
+
: undefined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** `u` of `typeof u === 'object'`, in either operand order. */
|
|
208
|
+
const getTypeofObjectIdentifierName = (
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
210
|
+
node: TSESTree.Node,
|
|
211
|
+
): string | undefined => {
|
|
212
|
+
if (
|
|
213
|
+
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
214
|
+
node.operator !== '==='
|
|
215
|
+
) {
|
|
125
216
|
return undefined;
|
|
126
217
|
}
|
|
127
218
|
|
|
@@ -129,31 +220,39 @@ const getTypeofObjectIdentifierName = (
|
|
|
129
220
|
|
|
130
221
|
const right = skipTypeWrappers(node.right);
|
|
131
222
|
|
|
223
|
+
const typeofSide = isLiteralOf(right, 'object')
|
|
224
|
+
? left
|
|
225
|
+
: isLiteralOf(left, 'object')
|
|
226
|
+
? right
|
|
227
|
+
: undefined;
|
|
228
|
+
|
|
132
229
|
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
right.type !== AST_NODE_TYPES.Literal ||
|
|
136
|
-
right.value !== 'object'
|
|
230
|
+
typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
231
|
+
typeofSide.operator !== 'typeof'
|
|
137
232
|
) {
|
|
138
233
|
return undefined;
|
|
139
234
|
}
|
|
140
235
|
|
|
141
|
-
const argument = skipTypeWrappers(
|
|
236
|
+
const argument = skipTypeWrappers(typeofSide.argument);
|
|
142
237
|
|
|
143
238
|
return argument.type === AST_NODE_TYPES.Identifier
|
|
144
239
|
? argument.name
|
|
145
240
|
: undefined;
|
|
146
241
|
};
|
|
147
242
|
|
|
243
|
+
/**
|
|
244
|
+
* `u` of `u !== null`, in either operand order. `u != null` also excludes
|
|
245
|
+
* `undefined`, which `typeof u === 'object'` has already excluded, so it is
|
|
246
|
+
* the same check here.
|
|
247
|
+
*/
|
|
148
248
|
const getNonNullCheckIdentifierName = (
|
|
149
249
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
150
|
-
node: TSESTree.
|
|
250
|
+
node: TSESTree.Node,
|
|
151
251
|
): string | undefined => {
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (node.operator !== '!==') {
|
|
252
|
+
if (
|
|
253
|
+
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
254
|
+
(node.operator !== '!==' && node.operator !== '!=')
|
|
255
|
+
) {
|
|
157
256
|
return undefined;
|
|
158
257
|
}
|
|
159
258
|
|
|
@@ -161,9 +260,17 @@ const getNonNullCheckIdentifierName = (
|
|
|
161
260
|
|
|
162
261
|
const right = skipTypeWrappers(node.right);
|
|
163
262
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
const checked = isLiteralOf(right, null)
|
|
264
|
+
? left
|
|
265
|
+
: isLiteralOf(left, null)
|
|
266
|
+
? right
|
|
267
|
+
: undefined;
|
|
268
|
+
|
|
269
|
+
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
169
270
|
};
|
|
271
|
+
|
|
272
|
+
const isLiteralOf = (
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
274
|
+
node: TSESTree.Node,
|
|
275
|
+
value: string | null,
|
|
276
|
+
): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
@@ -193,6 +193,144 @@ describe('prefer-is-non-null-object through type wrappers', () => {
|
|
|
193
193
|
{ messageId: 'useIsNonNullObject' },
|
|
194
194
|
],
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: 'both halves wrapped as a whole',
|
|
198
|
+
code: dedent`
|
|
199
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
200
|
+
declare const u: unknown;
|
|
201
|
+
const ok =
|
|
202
|
+
((typeof u === 'object') satisfies boolean) &&
|
|
203
|
+
((u !== null) as boolean);
|
|
204
|
+
`,
|
|
205
|
+
output: dedent`
|
|
206
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
207
|
+
declare const u: unknown;
|
|
208
|
+
const ok =
|
|
209
|
+
isNonNullObject(u);
|
|
210
|
+
`,
|
|
211
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
212
|
+
},
|
|
196
213
|
],
|
|
197
214
|
});
|
|
198
215
|
}, 20000);
|
|
216
|
+
|
|
217
|
+
describe('prefer-is-non-null-object in other forms', () => {
|
|
218
|
+
tester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
219
|
+
valid: [
|
|
220
|
+
{
|
|
221
|
+
name: 'the two halves joined by `||`',
|
|
222
|
+
code: dedent`
|
|
223
|
+
declare const u: unknown;
|
|
224
|
+
const ok = typeof u === 'object' || u !== null;
|
|
225
|
+
`,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'the two halves not next to each other in the chain',
|
|
229
|
+
code: dedent`
|
|
230
|
+
declare const u: unknown;
|
|
231
|
+
declare const a: boolean;
|
|
232
|
+
const ok = typeof u === 'object' && a && u !== null;
|
|
233
|
+
`,
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: 'an undefined check is not a null check',
|
|
237
|
+
code: dedent`
|
|
238
|
+
declare const u: unknown;
|
|
239
|
+
const ok = u !== undefined && typeof u === 'object';
|
|
240
|
+
`,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'the halves split by parentheses',
|
|
244
|
+
code: dedent`
|
|
245
|
+
declare const u: unknown;
|
|
246
|
+
declare const b: boolean;
|
|
247
|
+
const ok = typeof u === 'object' && (u !== null && b);
|
|
248
|
+
`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
invalid: [
|
|
252
|
+
{
|
|
253
|
+
name: 'the halves in reverse order',
|
|
254
|
+
code: dedent`
|
|
255
|
+
declare const u: unknown;
|
|
256
|
+
const ok = u !== null && typeof u === 'object';
|
|
257
|
+
`,
|
|
258
|
+
output: dedent`
|
|
259
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
260
|
+
declare const u: unknown;
|
|
261
|
+
const ok = isNonNullObject(u);
|
|
262
|
+
`,
|
|
263
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
name: 'each comparison with its operands swapped',
|
|
267
|
+
code: dedent`
|
|
268
|
+
declare const u: unknown;
|
|
269
|
+
const ok = 'object' === typeof u && null !== u;
|
|
270
|
+
`,
|
|
271
|
+
output: dedent`
|
|
272
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
273
|
+
declare const u: unknown;
|
|
274
|
+
const ok = isNonNullObject(u);
|
|
275
|
+
`,
|
|
276
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: '`!= null`, which typeof has already made the same check',
|
|
280
|
+
code: dedent`
|
|
281
|
+
declare const u: unknown;
|
|
282
|
+
const ok = typeof u === 'object' && u != null;
|
|
283
|
+
`,
|
|
284
|
+
output: dedent`
|
|
285
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
286
|
+
declare const u: unknown;
|
|
287
|
+
const ok = isNonNullObject(u);
|
|
288
|
+
`,
|
|
289
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'the pair at the start, the end and the middle of a longer chain',
|
|
293
|
+
code: dedent`
|
|
294
|
+
declare const u: unknown;
|
|
295
|
+
declare const a: boolean;
|
|
296
|
+
declare const b: boolean;
|
|
297
|
+
const x = typeof u === 'object' && u !== null && 'k' in u;
|
|
298
|
+
const y = a && typeof u === 'object' && u !== null;
|
|
299
|
+
const z = a && u !== null && (typeof u === 'object') && b;
|
|
300
|
+
`,
|
|
301
|
+
output: dedent`
|
|
302
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
303
|
+
declare const u: unknown;
|
|
304
|
+
declare const a: boolean;
|
|
305
|
+
declare const b: boolean;
|
|
306
|
+
const x = isNonNullObject(u) && 'k' in u;
|
|
307
|
+
const y = a && isNonNullObject(u);
|
|
308
|
+
const z = a && isNonNullObject(u) && b;
|
|
309
|
+
`,
|
|
310
|
+
errors: [
|
|
311
|
+
{ messageId: 'useIsNonNullObject' },
|
|
312
|
+
{ messageId: 'useIsNonNullObject' },
|
|
313
|
+
{ messageId: 'useIsNonNullObject' },
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: 'two pairs in one chain',
|
|
318
|
+
code: dedent`
|
|
319
|
+
declare const u: unknown;
|
|
320
|
+
declare const v: unknown;
|
|
321
|
+
const ok = typeof u === 'object' && u !== null && v !== null && typeof v === 'object';
|
|
322
|
+
`,
|
|
323
|
+
output: dedent`
|
|
324
|
+
import { isNonNullObject } from 'ts-data-forge';
|
|
325
|
+
declare const u: unknown;
|
|
326
|
+
declare const v: unknown;
|
|
327
|
+
const ok = isNonNullObject(u) && isNonNullObject(v);
|
|
328
|
+
`,
|
|
329
|
+
errors: [
|
|
330
|
+
{ messageId: 'useIsNonNullObject' },
|
|
331
|
+
{ messageId: 'useIsNonNullObject' },
|
|
332
|
+
],
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
});
|