eslint-plugin-ts-type-forge 0.4.3 → 0.6.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 +80 -13
- package/dist/plugin.d.mts +8 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +3 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +79 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +2 -0
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +10 -3
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-style.d.mts +21 -0
- package/dist/rules/import-style.d.mts.map +1 -0
- package/dist/rules/import-style.mjs +29 -0
- package/dist/rules/import-style.mjs.map +1 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
- package/dist/rules/prefer-readonly-or-mutable-record.d.mts +10 -0
- package/dist/rules/prefer-readonly-or-mutable-record.d.mts.map +1 -0
- package/dist/rules/prefer-readonly-or-mutable-record.mjs +49 -0
- package/dist/rules/prefer-readonly-or-mutable-record.mjs.map +1 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts +10 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts.map +1 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs +49 -0
- package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs.map +1 -0
- package/dist/rules/rules.d.mts +6 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +5 -1
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/rules/std-type-rule-utils.d.mts +22 -0
- package/dist/rules/std-type-rule-utils.d.mts.map +1 -0
- package/dist/rules/std-type-rule-utils.mjs +40 -0
- package/dist/rules/std-type-rule-utils.mjs.map +1 -0
- package/dist/rules/tuple-rule-utils.d.mts +1 -19
- package/dist/rules/tuple-rule-utils.d.mts.map +1 -1
- package/dist/rules/tuple-rule-utils.mjs +7 -29
- package/dist/rules/tuple-rule-utils.mjs.map +1 -1
- package/package.json +4 -3
- package/src/plugin.mts +2 -0
- package/src/rule-types.mts +93 -1
- package/src/rules/ast-utils.mts +32 -3
- package/src/rules/import-style.mts +60 -0
- package/src/rules/prefer-canonical-length-constrained-tuple.mts +2 -3
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +84 -15
- package/src/rules/prefer-readonly-or-mutable-record.mts +74 -0
- package/src/rules/prefer-readonly-or-mutable-record.test.mts +160 -0
- package/src/rules/prefer-strict-or-relaxed-utility-type.mts +80 -0
- package/src/rules/prefer-strict-or-relaxed-utility-type.test.mts +279 -0
- package/src/rules/rules.mts +4 -0
- package/src/rules/std-type-rule-utils.mts +103 -0
- package/src/rules/tuple-rule-utils.mts +12 -66
|
@@ -45,6 +45,21 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
45
45
|
name: 'ignores plain arrays',
|
|
46
46
|
code: 'type T = readonly string[];',
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'ignores tuple patterns in a conditional type `extends` clause',
|
|
50
|
+
code: dedent`
|
|
51
|
+
type And<A extends boolean, B extends boolean> = [A, B] extends [
|
|
52
|
+
true,
|
|
53
|
+
true,
|
|
54
|
+
]
|
|
55
|
+
? true
|
|
56
|
+
: false;
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'ignores tuple patterns nested in an `extends` clause',
|
|
61
|
+
code: 'type T<A> = A extends readonly (readonly [string, string])[] ? 1 : 2;',
|
|
62
|
+
},
|
|
48
63
|
{
|
|
49
64
|
name: 'stays silent when the target name is bound locally',
|
|
50
65
|
code: dedent`
|
|
@@ -54,75 +69,116 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
54
69
|
},
|
|
55
70
|
],
|
|
56
71
|
invalid: [
|
|
72
|
+
{
|
|
73
|
+
name: 'still rewrites the branches of a conditional type',
|
|
74
|
+
code: 'type T<A> = A extends 1 ? [true, true] : 2;',
|
|
75
|
+
output: dedent`
|
|
76
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
77
|
+
type T<A> = A extends 1 ? MutableFixedLengthTuple<2, true> : 2;
|
|
78
|
+
`,
|
|
79
|
+
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
80
|
+
},
|
|
57
81
|
{
|
|
58
82
|
name: 'readonly non-empty tuple',
|
|
59
83
|
code: 'type T = readonly [string, ...string[]];',
|
|
60
|
-
output:
|
|
84
|
+
output: dedent`
|
|
85
|
+
import { type NonEmptyTuple } from 'ts-type-forge';
|
|
86
|
+
type T = NonEmptyTuple<string>;
|
|
87
|
+
`,
|
|
61
88
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
62
89
|
},
|
|
63
90
|
{
|
|
64
91
|
name: 'mutable non-empty tuple',
|
|
65
92
|
code: 'type T = [string, ...string[]];',
|
|
66
|
-
output:
|
|
93
|
+
output: dedent`
|
|
94
|
+
import { type MutableNonEmptyTuple } from 'ts-type-forge';
|
|
95
|
+
type T = MutableNonEmptyTuple<string>;
|
|
96
|
+
`,
|
|
67
97
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
68
98
|
},
|
|
69
99
|
{
|
|
70
100
|
name: 'readonly non-empty tuple with a readonly rest spelling',
|
|
71
101
|
code: 'type T = readonly [string, ...(readonly string[])];',
|
|
72
|
-
output:
|
|
102
|
+
output: dedent`
|
|
103
|
+
import { type NonEmptyTuple } from 'ts-type-forge';
|
|
104
|
+
type T = NonEmptyTuple<string>;
|
|
105
|
+
`,
|
|
73
106
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
74
107
|
},
|
|
75
108
|
{
|
|
76
109
|
name: 'readonly fixed-length tuple',
|
|
77
110
|
code: 'type T = readonly [string, string, string];',
|
|
78
|
-
output:
|
|
111
|
+
output: dedent`
|
|
112
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
113
|
+
type T = FixedLengthTuple<3, string>;
|
|
114
|
+
`,
|
|
79
115
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
80
116
|
},
|
|
81
117
|
{
|
|
82
118
|
name: 'mutable fixed-length tuple',
|
|
83
119
|
code: 'type T = [string, string, string];',
|
|
84
|
-
output:
|
|
120
|
+
output: dedent`
|
|
121
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
122
|
+
type T = MutableFixedLengthTuple<3, string>;
|
|
123
|
+
`,
|
|
85
124
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
86
125
|
},
|
|
87
126
|
{
|
|
88
127
|
name: 'readonly min-length tuple',
|
|
89
128
|
code: 'type T = readonly [number, number, ...number[]];',
|
|
90
|
-
output:
|
|
129
|
+
output: dedent`
|
|
130
|
+
import { type MinLengthTuple } from 'ts-type-forge';
|
|
131
|
+
type T = MinLengthTuple<2, number>;
|
|
132
|
+
`,
|
|
91
133
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
92
134
|
},
|
|
93
135
|
{
|
|
94
136
|
name: 'mutable min-length tuple',
|
|
95
137
|
code: 'type T = [number, number, ...number[]];',
|
|
96
|
-
output:
|
|
138
|
+
output: dedent`
|
|
139
|
+
import { type MutableMinLengthTuple } from 'ts-type-forge';
|
|
140
|
+
type T = MutableMinLengthTuple<2, number>;
|
|
141
|
+
`,
|
|
97
142
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
98
143
|
},
|
|
99
144
|
{
|
|
100
145
|
name: 'readonly rest spelling inside a readonly tuple',
|
|
101
146
|
code: 'type T = readonly [number, number, ...(readonly number[])];',
|
|
102
|
-
output:
|
|
147
|
+
output: dedent`
|
|
148
|
+
import { type MinLengthTuple } from 'ts-type-forge';
|
|
149
|
+
type T = MinLengthTuple<2, number>;
|
|
150
|
+
`,
|
|
103
151
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
104
152
|
},
|
|
105
153
|
{
|
|
106
154
|
name: 'handles the default maximum length of 10',
|
|
107
155
|
code: 'type T = readonly [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];',
|
|
108
|
-
output:
|
|
156
|
+
output: dedent`
|
|
157
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
158
|
+
type T = FixedLengthTuple<10, 1>;
|
|
159
|
+
`,
|
|
109
160
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
110
161
|
},
|
|
111
162
|
{
|
|
112
163
|
name: 'keeps a composite element type intact',
|
|
113
164
|
code: 'type T = readonly [string | number, string | number];',
|
|
114
|
-
output:
|
|
165
|
+
output: dedent`
|
|
166
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
167
|
+
type T = FixedLengthTuple<2, string | number>;
|
|
168
|
+
`,
|
|
115
169
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
116
170
|
},
|
|
117
171
|
{
|
|
118
172
|
name: 'handles generic element types',
|
|
119
173
|
code: 'type T<V> = [V, V, ...V[]];',
|
|
120
|
-
output:
|
|
174
|
+
output: dedent`
|
|
175
|
+
import { type MutableMinLengthTuple } from 'ts-type-forge';
|
|
176
|
+
type T<V> = MutableMinLengthTuple<2, V>;
|
|
177
|
+
`,
|
|
121
178
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
122
179
|
},
|
|
123
180
|
{
|
|
124
181
|
name: 'adds named imports once per target type',
|
|
125
|
-
options: [{ importStyle: 'named' }],
|
|
126
182
|
code: dedent`
|
|
127
183
|
type A = readonly [string, string];
|
|
128
184
|
type B = readonly [number, number, number];
|
|
@@ -141,6 +197,13 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
141
197
|
{ messageId: 'useCanonicalTuple' },
|
|
142
198
|
],
|
|
143
199
|
},
|
|
200
|
+
{
|
|
201
|
+
name: 'adds no import under importStyle: global',
|
|
202
|
+
options: [{ importStyle: 'global' }],
|
|
203
|
+
code: 'type T = readonly [string, string];',
|
|
204
|
+
output: 'type T = FixedLengthTuple<2, string>;',
|
|
205
|
+
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
206
|
+
},
|
|
144
207
|
{
|
|
145
208
|
name: 'reuses an aliased import',
|
|
146
209
|
code: dedent`
|
|
@@ -158,8 +221,11 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
158
221
|
{
|
|
159
222
|
name: 'rewrites parameter and return type positions',
|
|
160
223
|
code: 'declare const f: (xs: readonly [number, number]) => [string, string];',
|
|
161
|
-
output:
|
|
162
|
-
|
|
224
|
+
output: dedent`
|
|
225
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
226
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
227
|
+
declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;
|
|
228
|
+
`,
|
|
163
229
|
errors: [
|
|
164
230
|
{ messageId: 'useCanonicalTuple' },
|
|
165
231
|
{ messageId: 'useCanonicalTuple' },
|
|
@@ -169,7 +235,10 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
169
235
|
name: 'respects a raised maxLength',
|
|
170
236
|
options: [{ maxLength: 12 }],
|
|
171
237
|
code: 'type T = readonly [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];',
|
|
172
|
-
output:
|
|
238
|
+
output: dedent`
|
|
239
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
240
|
+
type T = FixedLengthTuple<12, 0>;
|
|
241
|
+
`,
|
|
173
242
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
174
243
|
},
|
|
175
244
|
],
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_IMPORT_STYLE,
|
|
4
|
+
IMPORT_STYLE_SCHEMA_PROPERTY,
|
|
5
|
+
} from './import-style.mjs';
|
|
6
|
+
import {
|
|
7
|
+
reportStdTypeReference,
|
|
8
|
+
type StdTypeAlternatives,
|
|
9
|
+
} from './std-type-rule-utils.mjs';
|
|
10
|
+
|
|
11
|
+
type Options = readonly [
|
|
12
|
+
Readonly<{
|
|
13
|
+
importStyle?: 'global' | 'named';
|
|
14
|
+
}>?,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
type MessageIds = 'useReadonlyOrMutableRecord' | 'replaceWith';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `Record` says nothing about whether the properties may be reassigned, so the
|
|
21
|
+
* mutability of every record type spelled with it is decided by whoever reads
|
|
22
|
+
* it. The ts-type-forge pair spells it out.
|
|
23
|
+
*/
|
|
24
|
+
const ALTERNATIVES: StdTypeAlternatives = new Map([
|
|
25
|
+
['Record', ['ReadonlyRecord', 'MutableRecord']],
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export const preferReadonlyOrMutableRecord: TSESLint.RuleModule<
|
|
29
|
+
MessageIds,
|
|
30
|
+
Options
|
|
31
|
+
> = {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'suggestion',
|
|
34
|
+
docs: {
|
|
35
|
+
description:
|
|
36
|
+
'Replace the built-in `Record` with the ts-type-forge `ReadonlyRecord` or `MutableRecord`, so that the mutability of the record is stated rather than implied.',
|
|
37
|
+
},
|
|
38
|
+
hasSuggestions: true,
|
|
39
|
+
schema: [
|
|
40
|
+
{
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,
|
|
44
|
+
},
|
|
45
|
+
additionalProperties: false,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
messages: {
|
|
49
|
+
useReadonlyOrMutableRecord:
|
|
50
|
+
'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',
|
|
51
|
+
replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
create: (context) => {
|
|
56
|
+
const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
TSTypeReference: (node) => {
|
|
60
|
+
reportStdTypeReference(
|
|
61
|
+
context,
|
|
62
|
+
node,
|
|
63
|
+
ALTERNATIVES,
|
|
64
|
+
{
|
|
65
|
+
report: 'useReadonlyOrMutableRecord',
|
|
66
|
+
suggestion: 'replaceWith',
|
|
67
|
+
},
|
|
68
|
+
importStyle,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],
|
|
74
|
+
} as const;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('prefer-readonly-or-mutable-record', () => {
|
|
14
|
+
tester.run(
|
|
15
|
+
'prefer-readonly-or-mutable-record',
|
|
16
|
+
preferReadonlyOrMutableRecord,
|
|
17
|
+
{
|
|
18
|
+
valid: [
|
|
19
|
+
{
|
|
20
|
+
name: 'accepts the ts-type-forge counterparts',
|
|
21
|
+
code: dedent`
|
|
22
|
+
type A = ReadonlyRecord<string, number>;
|
|
23
|
+
type B = MutableRecord<'a' | 'b', number>;
|
|
24
|
+
`,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'ignores unrelated standard-library types',
|
|
28
|
+
code: 'type A = Readonly<Partial<{ a: 1 }>>;',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'ignores a qualified name that merely ends in `Record`',
|
|
32
|
+
code: 'type A = Lib.Record<string, number>;',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ignores a locally declared type of the same name',
|
|
36
|
+
code: dedent`
|
|
37
|
+
type Record<K extends PropertyKey, V> = { readonly [P in K]: V };
|
|
38
|
+
type A = Record<string, number>;
|
|
39
|
+
`,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'ignores a name imported from another module',
|
|
43
|
+
code: dedent`
|
|
44
|
+
import { type Record } from './my-types.mjs';
|
|
45
|
+
type A = Record<string, number>;
|
|
46
|
+
`,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
invalid: [
|
|
50
|
+
{
|
|
51
|
+
name: 'reports `Record` and suggests both counterparts',
|
|
52
|
+
code: 'type A = Record<string, number>;',
|
|
53
|
+
errors: [
|
|
54
|
+
{
|
|
55
|
+
messageId: 'useReadonlyOrMutableRecord',
|
|
56
|
+
data: {
|
|
57
|
+
name: 'Record',
|
|
58
|
+
alternatives: '`ReadonlyRecord` or `MutableRecord`',
|
|
59
|
+
},
|
|
60
|
+
suggestions: [
|
|
61
|
+
{
|
|
62
|
+
messageId: 'replaceWith',
|
|
63
|
+
data: { name: 'Record', replacement: 'ReadonlyRecord' },
|
|
64
|
+
output: dedent`
|
|
65
|
+
import { type ReadonlyRecord } from 'ts-type-forge';
|
|
66
|
+
type A = ReadonlyRecord<string, number>;
|
|
67
|
+
`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
messageId: 'replaceWith',
|
|
71
|
+
data: { name: 'Record', replacement: 'MutableRecord' },
|
|
72
|
+
output: dedent`
|
|
73
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
74
|
+
type A = MutableRecord<string, number>;
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'touches no import under the `global` import style',
|
|
83
|
+
options: [{ importStyle: 'global' }],
|
|
84
|
+
code: 'type A = Record<string, number>;',
|
|
85
|
+
errors: [
|
|
86
|
+
{
|
|
87
|
+
messageId: 'useReadonlyOrMutableRecord',
|
|
88
|
+
suggestions: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'replaceWith',
|
|
91
|
+
output: 'type A = ReadonlyRecord<string, number>;',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
messageId: 'replaceWith',
|
|
95
|
+
output: 'type A = MutableRecord<string, number>;',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'reuses an existing ts-type-forge import, alias included',
|
|
103
|
+
code: dedent`
|
|
104
|
+
import { type ReadonlyRecord as RRecord } from 'ts-type-forge';
|
|
105
|
+
type A = Record<string, number>;
|
|
106
|
+
`,
|
|
107
|
+
errors: [
|
|
108
|
+
{
|
|
109
|
+
messageId: 'useReadonlyOrMutableRecord',
|
|
110
|
+
suggestions: [
|
|
111
|
+
{
|
|
112
|
+
messageId: 'replaceWith',
|
|
113
|
+
data: { name: 'Record', replacement: 'RRecord' },
|
|
114
|
+
output: dedent`
|
|
115
|
+
import { type ReadonlyRecord as RRecord } from 'ts-type-forge';
|
|
116
|
+
type A = RRecord<string, number>;
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
messageId: 'replaceWith',
|
|
121
|
+
data: { name: 'Record', replacement: 'MutableRecord' },
|
|
122
|
+
output: dedent`
|
|
123
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
124
|
+
import { type ReadonlyRecord as RRecord } from 'ts-type-forge';
|
|
125
|
+
type A = MutableRecord<string, number>;
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'reports `Record` in a type-parameter constraint too',
|
|
134
|
+
code: 'type A<T extends Record<string, unknown>> = keyof T;',
|
|
135
|
+
errors: [
|
|
136
|
+
{
|
|
137
|
+
messageId: 'useReadonlyOrMutableRecord',
|
|
138
|
+
suggestions: [
|
|
139
|
+
{
|
|
140
|
+
messageId: 'replaceWith',
|
|
141
|
+
output: dedent`
|
|
142
|
+
import { type ReadonlyRecord } from 'ts-type-forge';
|
|
143
|
+
type A<T extends ReadonlyRecord<string, unknown>> = keyof T;
|
|
144
|
+
`,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
messageId: 'replaceWith',
|
|
148
|
+
output: dedent`
|
|
149
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
150
|
+
type A<T extends MutableRecord<string, unknown>> = keyof T;
|
|
151
|
+
`,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
);
|
|
160
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_IMPORT_STYLE,
|
|
4
|
+
IMPORT_STYLE_SCHEMA_PROPERTY,
|
|
5
|
+
} from './import-style.mjs';
|
|
6
|
+
import {
|
|
7
|
+
reportStdTypeReference,
|
|
8
|
+
type StdTypeAlternatives,
|
|
9
|
+
} from './std-type-rule-utils.mjs';
|
|
10
|
+
|
|
11
|
+
type Options = readonly [
|
|
12
|
+
Readonly<{
|
|
13
|
+
importStyle?: 'global' | 'named';
|
|
14
|
+
}>?,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
type MessageIds = 'useStrictOrRelaxed' | 'replaceWith';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The standard-library types whose second argument is unchecked, and the
|
|
21
|
+
* ts-type-forge pair that makes the choice explicit.
|
|
22
|
+
*
|
|
23
|
+
* `Strict*` constrains the second argument (`U extends T` / `K extends keyof T`),
|
|
24
|
+
* so a key that does not exist — typically a leftover after a rename — is a
|
|
25
|
+
* compile error instead of a silently empty result. `Relaxed*` keeps the
|
|
26
|
+
* standard-library behavior for the cases that genuinely need it (a subtrahend
|
|
27
|
+
* that is not provably part of the union, a key set computed from a still
|
|
28
|
+
* deferred type parameter).
|
|
29
|
+
*/
|
|
30
|
+
const ALTERNATIVES: StdTypeAlternatives = new Map([
|
|
31
|
+
['Exclude', ['StrictExclude', 'RelaxedExclude']],
|
|
32
|
+
['Extract', ['StrictExtract', 'RelaxedExtract']],
|
|
33
|
+
['Omit', ['StrictOmit', 'RelaxedOmit']],
|
|
34
|
+
['Pick', ['StrictPick', 'RelaxedPick']],
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
export const preferStrictOrRelaxedUtilityType: TSESLint.RuleModule<
|
|
38
|
+
MessageIds,
|
|
39
|
+
Options
|
|
40
|
+
> = {
|
|
41
|
+
meta: {
|
|
42
|
+
type: 'suggestion',
|
|
43
|
+
docs: {
|
|
44
|
+
description:
|
|
45
|
+
'Replace the built-in `Exclude` / `Extract` / `Omit` / `Pick` with the ts-type-forge `Strict*` or `Relaxed*` counterpart, so that the unchecked second argument becomes an explicit choice.',
|
|
46
|
+
},
|
|
47
|
+
hasSuggestions: true,
|
|
48
|
+
schema: [
|
|
49
|
+
{
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,
|
|
53
|
+
},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
messages: {
|
|
58
|
+
useStrictOrRelaxed:
|
|
59
|
+
'Use {{alternatives}} from ts-type-forge instead of the built-in `{{name}}`.',
|
|
60
|
+
replaceWith: 'Replace `{{name}}` with `{{replacement}}`.',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
create: (context) => {
|
|
65
|
+
const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
TSTypeReference: (node) => {
|
|
69
|
+
reportStdTypeReference(
|
|
70
|
+
context,
|
|
71
|
+
node,
|
|
72
|
+
ALTERNATIVES,
|
|
73
|
+
{ report: 'useStrictOrRelaxed', suggestion: 'replaceWith' },
|
|
74
|
+
importStyle,
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],
|
|
80
|
+
} as const;
|