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.
Files changed (52) hide show
  1. package/README.md +80 -13
  2. package/dist/plugin.d.mts +8 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +3 -1
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +79 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rules/ast-utils.d.mts +2 -0
  9. package/dist/rules/ast-utils.d.mts.map +1 -1
  10. package/dist/rules/ast-utils.mjs +10 -3
  11. package/dist/rules/ast-utils.mjs.map +1 -1
  12. package/dist/rules/import-style.d.mts +21 -0
  13. package/dist/rules/import-style.d.mts.map +1 -0
  14. package/dist/rules/import-style.mjs +29 -0
  15. package/dist/rules/import-style.mjs.map +1 -0
  16. package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -1
  17. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -1
  18. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
  19. package/dist/rules/prefer-readonly-or-mutable-record.d.mts +10 -0
  20. package/dist/rules/prefer-readonly-or-mutable-record.d.mts.map +1 -0
  21. package/dist/rules/prefer-readonly-or-mutable-record.mjs +49 -0
  22. package/dist/rules/prefer-readonly-or-mutable-record.mjs.map +1 -0
  23. package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts +10 -0
  24. package/dist/rules/prefer-strict-or-relaxed-utility-type.d.mts.map +1 -0
  25. package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs +49 -0
  26. package/dist/rules/prefer-strict-or-relaxed-utility-type.mjs.map +1 -0
  27. package/dist/rules/rules.d.mts +6 -0
  28. package/dist/rules/rules.d.mts.map +1 -1
  29. package/dist/rules/rules.mjs +5 -1
  30. package/dist/rules/rules.mjs.map +1 -1
  31. package/dist/rules/std-type-rule-utils.d.mts +22 -0
  32. package/dist/rules/std-type-rule-utils.d.mts.map +1 -0
  33. package/dist/rules/std-type-rule-utils.mjs +40 -0
  34. package/dist/rules/std-type-rule-utils.mjs.map +1 -0
  35. package/dist/rules/tuple-rule-utils.d.mts +1 -19
  36. package/dist/rules/tuple-rule-utils.d.mts.map +1 -1
  37. package/dist/rules/tuple-rule-utils.mjs +7 -29
  38. package/dist/rules/tuple-rule-utils.mjs.map +1 -1
  39. package/package.json +4 -3
  40. package/src/plugin.mts +2 -0
  41. package/src/rule-types.mts +93 -1
  42. package/src/rules/ast-utils.mts +32 -3
  43. package/src/rules/import-style.mts +60 -0
  44. package/src/rules/prefer-canonical-length-constrained-tuple.mts +2 -3
  45. package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +84 -15
  46. package/src/rules/prefer-readonly-or-mutable-record.mts +74 -0
  47. package/src/rules/prefer-readonly-or-mutable-record.test.mts +160 -0
  48. package/src/rules/prefer-strict-or-relaxed-utility-type.mts +80 -0
  49. package/src/rules/prefer-strict-or-relaxed-utility-type.test.mts +279 -0
  50. package/src/rules/rules.mts +4 -0
  51. package/src/rules/std-type-rule-utils.mts +103 -0
  52. 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: 'type T = NonEmptyTuple<string>;',
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: 'type T = MutableNonEmptyTuple<string>;',
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: 'type T = NonEmptyTuple<string>;',
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: 'type T = FixedLengthTuple<3, string>;',
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: 'type T = MutableFixedLengthTuple<3, string>;',
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: 'type T = MinLengthTuple<2, number>;',
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: 'type T = MutableMinLengthTuple<2, number>;',
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: 'type T = MinLengthTuple<2, number>;',
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: 'type T = FixedLengthTuple<10, 1>;',
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: 'type T = FixedLengthTuple<2, string | number>;',
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: 'type T<V> = MutableMinLengthTuple<2, V>;',
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
- 'declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;',
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: 'type T = FixedLengthTuple<12, 0>;',
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;