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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import dedent from 'dedent';
|
|
4
|
+
import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
describe('prefer-strict-or-relaxed-utility-type', () => {
|
|
14
|
+
tester.run(
|
|
15
|
+
'prefer-strict-or-relaxed-utility-type',
|
|
16
|
+
preferStrictOrRelaxedUtilityType,
|
|
17
|
+
{
|
|
18
|
+
valid: [
|
|
19
|
+
{
|
|
20
|
+
name: 'accepts the ts-type-forge counterparts',
|
|
21
|
+
code: dedent`
|
|
22
|
+
type A = StrictExclude<'a' | 'b', 'a'>;
|
|
23
|
+
type B = RelaxedExtract<string | number, number>;
|
|
24
|
+
type C = StrictOmit<{ a: 1; b: 2 }, 'a'>;
|
|
25
|
+
type D = RelaxedPick<{ a: 1; b: 2 }, 'a' | 'z'>;
|
|
26
|
+
`,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'ignores unrelated standard-library types',
|
|
30
|
+
code: 'type A = Partial<Required<{ a: 1 }>>;',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'ignores a qualified name that merely ends in a banned name',
|
|
34
|
+
code: "type A = Utils.Pick<{ a: 1 }, 'a'>;",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'ignores a locally declared type of the same name',
|
|
38
|
+
code: dedent`
|
|
39
|
+
type Pick<T, K extends keyof T> = { readonly [P in K]: T[P] };
|
|
40
|
+
type A = Pick<{ a: 1 }, 'a'>;
|
|
41
|
+
`,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'ignores a name imported from another module',
|
|
45
|
+
code: dedent`
|
|
46
|
+
import { type Omit } from './my-types.mjs';
|
|
47
|
+
type A = Omit<{ a: 1; b: 2 }, 'a'>;
|
|
48
|
+
`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
invalid: [
|
|
52
|
+
{
|
|
53
|
+
name: 'reports `Exclude` and suggests both counterparts',
|
|
54
|
+
code: "type A = Exclude<'a' | 'b', 'a'>;",
|
|
55
|
+
errors: [
|
|
56
|
+
{
|
|
57
|
+
messageId: 'useStrictOrRelaxed',
|
|
58
|
+
data: {
|
|
59
|
+
name: 'Exclude',
|
|
60
|
+
alternatives: '`StrictExclude` or `RelaxedExclude`',
|
|
61
|
+
},
|
|
62
|
+
suggestions: [
|
|
63
|
+
{
|
|
64
|
+
messageId: 'replaceWith',
|
|
65
|
+
data: { name: 'Exclude', replacement: 'StrictExclude' },
|
|
66
|
+
output: dedent`
|
|
67
|
+
import { type StrictExclude } from 'ts-type-forge';
|
|
68
|
+
type A = StrictExclude<'a' | 'b', 'a'>;
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
messageId: 'replaceWith',
|
|
73
|
+
data: { name: 'Exclude', replacement: 'RelaxedExclude' },
|
|
74
|
+
output: dedent`
|
|
75
|
+
import { type RelaxedExclude } from 'ts-type-forge';
|
|
76
|
+
type A = RelaxedExclude<'a' | 'b', 'a'>;
|
|
77
|
+
`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'reports `Extract`',
|
|
85
|
+
code: 'type A = Extract<string | number, number>;',
|
|
86
|
+
errors: [
|
|
87
|
+
{
|
|
88
|
+
messageId: 'useStrictOrRelaxed',
|
|
89
|
+
suggestions: [
|
|
90
|
+
{
|
|
91
|
+
messageId: 'replaceWith',
|
|
92
|
+
output: dedent`
|
|
93
|
+
import { type StrictExtract } from 'ts-type-forge';
|
|
94
|
+
type A = StrictExtract<string | number, number>;
|
|
95
|
+
`,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
messageId: 'replaceWith',
|
|
99
|
+
output: dedent`
|
|
100
|
+
import { type RelaxedExtract } from 'ts-type-forge';
|
|
101
|
+
type A = RelaxedExtract<string | number, number>;
|
|
102
|
+
`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'reports `Omit`',
|
|
110
|
+
code: "type A = Omit<{ a: 1; b: 2 }, 'a'>;",
|
|
111
|
+
errors: [
|
|
112
|
+
{
|
|
113
|
+
messageId: 'useStrictOrRelaxed',
|
|
114
|
+
suggestions: [
|
|
115
|
+
{
|
|
116
|
+
messageId: 'replaceWith',
|
|
117
|
+
output: dedent`
|
|
118
|
+
import { type StrictOmit } from 'ts-type-forge';
|
|
119
|
+
type A = StrictOmit<{ a: 1; b: 2 }, 'a'>;
|
|
120
|
+
`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
messageId: 'replaceWith',
|
|
124
|
+
output: dedent`
|
|
125
|
+
import { type RelaxedOmit } from 'ts-type-forge';
|
|
126
|
+
type A = RelaxedOmit<{ a: 1; b: 2 }, 'a'>;
|
|
127
|
+
`,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'reports `Pick`',
|
|
135
|
+
code: "type A = Pick<{ a: 1; b: 2 }, 'a'>;",
|
|
136
|
+
errors: [
|
|
137
|
+
{
|
|
138
|
+
messageId: 'useStrictOrRelaxed',
|
|
139
|
+
suggestions: [
|
|
140
|
+
{
|
|
141
|
+
messageId: 'replaceWith',
|
|
142
|
+
output: dedent`
|
|
143
|
+
import { type StrictPick } from 'ts-type-forge';
|
|
144
|
+
type A = StrictPick<{ a: 1; b: 2 }, 'a'>;
|
|
145
|
+
`,
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
messageId: 'replaceWith',
|
|
149
|
+
output: dedent`
|
|
150
|
+
import { type RelaxedPick } from 'ts-type-forge';
|
|
151
|
+
type A = RelaxedPick<{ a: 1; b: 2 }, 'a'>;
|
|
152
|
+
`,
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'touches no import under the `global` import style',
|
|
160
|
+
options: [{ importStyle: 'global' }],
|
|
161
|
+
code: "type A = Pick<{ a: 1 }, 'a'>;",
|
|
162
|
+
errors: [
|
|
163
|
+
{
|
|
164
|
+
messageId: 'useStrictOrRelaxed',
|
|
165
|
+
suggestions: [
|
|
166
|
+
{
|
|
167
|
+
messageId: 'replaceWith',
|
|
168
|
+
output: "type A = StrictPick<{ a: 1 }, 'a'>;",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
messageId: 'replaceWith',
|
|
172
|
+
output: "type A = RelaxedPick<{ a: 1 }, 'a'>;",
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'reuses an existing ts-type-forge import, alias included',
|
|
180
|
+
code: dedent`
|
|
181
|
+
import { type StrictPick as SPick } from 'ts-type-forge';
|
|
182
|
+
type A = Pick<{ a: 1 }, 'a'>;
|
|
183
|
+
`,
|
|
184
|
+
errors: [
|
|
185
|
+
{
|
|
186
|
+
messageId: 'useStrictOrRelaxed',
|
|
187
|
+
suggestions: [
|
|
188
|
+
{
|
|
189
|
+
messageId: 'replaceWith',
|
|
190
|
+
data: { name: 'Pick', replacement: 'SPick' },
|
|
191
|
+
output: dedent`
|
|
192
|
+
import { type StrictPick as SPick } from 'ts-type-forge';
|
|
193
|
+
type A = SPick<{ a: 1 }, 'a'>;
|
|
194
|
+
`,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
messageId: 'replaceWith',
|
|
198
|
+
data: { name: 'Pick', replacement: 'RelaxedPick' },
|
|
199
|
+
output: dedent`
|
|
200
|
+
import { type RelaxedPick } from 'ts-type-forge';
|
|
201
|
+
import { type StrictPick as SPick } from 'ts-type-forge';
|
|
202
|
+
type A = RelaxedPick<{ a: 1 }, 'a'>;
|
|
203
|
+
`,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'drops the suggestion whose target name is bound locally',
|
|
211
|
+
code: dedent`
|
|
212
|
+
type StrictOmit<T, K extends keyof T> = { readonly [P in K]: T[P] };
|
|
213
|
+
type A = Omit<{ a: 1; b: 2 }, 'a'>;
|
|
214
|
+
`,
|
|
215
|
+
errors: [
|
|
216
|
+
{
|
|
217
|
+
messageId: 'useStrictOrRelaxed',
|
|
218
|
+
suggestions: [
|
|
219
|
+
{
|
|
220
|
+
messageId: 'replaceWith',
|
|
221
|
+
data: { name: 'Omit', replacement: 'RelaxedOmit' },
|
|
222
|
+
output: dedent`
|
|
223
|
+
import { type RelaxedOmit } from 'ts-type-forge';
|
|
224
|
+
type StrictOmit<T, K extends keyof T> = { readonly [P in K]: T[P] };
|
|
225
|
+
type A = RelaxedOmit<{ a: 1; b: 2 }, 'a'>;
|
|
226
|
+
`,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
name: 'reports every occurrence, nesting included',
|
|
234
|
+
code: "type A = Pick<{ a: 1; b: 2 }, Exclude<'a' | 'b', 'b'>>;",
|
|
235
|
+
errors: [
|
|
236
|
+
{
|
|
237
|
+
messageId: 'useStrictOrRelaxed',
|
|
238
|
+
suggestions: [
|
|
239
|
+
{
|
|
240
|
+
messageId: 'replaceWith',
|
|
241
|
+
output: dedent`
|
|
242
|
+
import { type StrictPick } from 'ts-type-forge';
|
|
243
|
+
type A = StrictPick<{ a: 1; b: 2 }, Exclude<'a' | 'b', 'b'>>;
|
|
244
|
+
`,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
messageId: 'replaceWith',
|
|
248
|
+
output: dedent`
|
|
249
|
+
import { type RelaxedPick } from 'ts-type-forge';
|
|
250
|
+
type A = RelaxedPick<{ a: 1; b: 2 }, Exclude<'a' | 'b', 'b'>>;
|
|
251
|
+
`,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
messageId: 'useStrictOrRelaxed',
|
|
257
|
+
suggestions: [
|
|
258
|
+
{
|
|
259
|
+
messageId: 'replaceWith',
|
|
260
|
+
output: dedent`
|
|
261
|
+
import { type StrictExclude } from 'ts-type-forge';
|
|
262
|
+
type A = Pick<{ a: 1; b: 2 }, StrictExclude<'a' | 'b', 'b'>>;
|
|
263
|
+
`,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
messageId: 'replaceWith',
|
|
267
|
+
output: dedent`
|
|
268
|
+
import { type RelaxedExclude } from 'ts-type-forge';
|
|
269
|
+
type A = Pick<{ a: 1; b: 2 }, RelaxedExclude<'a' | 'b', 'b'>>;
|
|
270
|
+
`,
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
});
|
package/src/rules/rules.mts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { type ESLintPlugin } from '../types.mjs';
|
|
2
2
|
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
3
|
+
import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
|
|
4
|
+
import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
|
|
3
5
|
|
|
4
6
|
export const tsTypeForgeRules = {
|
|
5
7
|
'prefer-canonical-length-constrained-tuple':
|
|
6
8
|
preferCanonicalLengthConstrainedTuple,
|
|
9
|
+
'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,
|
|
10
|
+
'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,
|
|
7
11
|
} as const satisfies ESLintPlugin['rules'];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
ASTUtils,
|
|
4
|
+
type TSESLint,
|
|
5
|
+
type TSESTree,
|
|
6
|
+
} from '@typescript-eslint/utils';
|
|
7
|
+
import { Arr } from 'ts-data-forge';
|
|
8
|
+
import { resolveTypeName, type ImportStyle } from './import-style.mjs';
|
|
9
|
+
import { buildTypeImportFix } from './import-utils.mjs';
|
|
10
|
+
|
|
11
|
+
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The ts-type-forge replacements offered for one standard-library type, in the
|
|
15
|
+
* order they are suggested.
|
|
16
|
+
*/
|
|
17
|
+
export type StdTypeAlternatives = ReadonlyMap<string, readonly string[]>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reports a reference to a banned standard-library type and offers one
|
|
21
|
+
* suggestion per ts-type-forge alternative.
|
|
22
|
+
*
|
|
23
|
+
* Choosing between the alternatives is a semantic decision — `StrictExclude`
|
|
24
|
+
* additionally checks that the subtrahend is part of the union, `ReadonlyRecord`
|
|
25
|
+
* and `MutableRecord` describe different types — so this is deliberately *not*
|
|
26
|
+
* an autofix. Each suggestion carries its own import fix, which is safe because
|
|
27
|
+
* ESLint applies suggestions one at a time.
|
|
28
|
+
*/
|
|
29
|
+
export const reportStdTypeReference = <MessageIds extends string>(
|
|
30
|
+
context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,
|
|
31
|
+
node: TSESTree.TSTypeReference,
|
|
32
|
+
alternatives: StdTypeAlternatives,
|
|
33
|
+
messageIds: Readonly<{ report: MessageIds; suggestion: MessageIds }>,
|
|
34
|
+
importStyle: ImportStyle,
|
|
35
|
+
): void => {
|
|
36
|
+
const typeName = node.typeName;
|
|
37
|
+
|
|
38
|
+
// `ns.Pick` / `import('…').Pick` never denote the standard-library type.
|
|
39
|
+
if (typeName.type !== AST_NODE_TYPES.Identifier) return;
|
|
40
|
+
|
|
41
|
+
const stdName = typeName.name;
|
|
42
|
+
|
|
43
|
+
const candidates = alternatives.get(stdName);
|
|
44
|
+
|
|
45
|
+
if (candidates === undefined) return;
|
|
46
|
+
|
|
47
|
+
// A file that declares — or imports — its own `Pick` does not mean the
|
|
48
|
+
// standard-library one, so there is nothing to steer.
|
|
49
|
+
if (isLocallyBound(context.sourceCode, node, stdName)) return;
|
|
50
|
+
|
|
51
|
+
const program = context.sourceCode.ast;
|
|
52
|
+
|
|
53
|
+
context.report({
|
|
54
|
+
node: typeName,
|
|
55
|
+
messageId: messageIds.report,
|
|
56
|
+
data: { name: stdName, alternatives: formatAlternatives(candidates) },
|
|
57
|
+
suggest: candidates.flatMap((candidate) => {
|
|
58
|
+
const target = resolveTypeName(program, candidate, importStyle);
|
|
59
|
+
|
|
60
|
+
// The replacement name is already bound to something else in this file,
|
|
61
|
+
// so rewriting to it would resolve to the wrong declaration.
|
|
62
|
+
return target === undefined
|
|
63
|
+
? []
|
|
64
|
+
: [
|
|
65
|
+
{
|
|
66
|
+
messageId: messageIds.suggestion,
|
|
67
|
+
data: { name: stdName, replacement: target.localName },
|
|
68
|
+
fix: (fixer: TSESLint.RuleFixer): readonly TSESLint.RuleFix[] =>
|
|
69
|
+
Arr.toPushed(
|
|
70
|
+
target.needsImport
|
|
71
|
+
? buildTypeImportFix(fixer, program, candidate)
|
|
72
|
+
: [],
|
|
73
|
+
fixer.replaceText(typeName, target.localName),
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
}),
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** `['StrictPick', 'RelaxedPick']` → `` '`StrictPick` or `RelaxedPick`' ``. */
|
|
82
|
+
const formatAlternatives = (candidates: readonly string[]): string =>
|
|
83
|
+
candidates.map((candidate) => `\`${candidate}\``).join(' or ');
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether `name` resolves to a declaration in this file (a type alias, an
|
|
87
|
+
* interface, a class, an import, …) rather than to the ambient standard-library
|
|
88
|
+
* type of the same name.
|
|
89
|
+
*
|
|
90
|
+
* The `defs` check is what separates the two: ESLint materializes an *implicit*
|
|
91
|
+
* global variable for every unresolved reference, so `findVariable` also
|
|
92
|
+
* succeeds for `Exclude` in a file that never declares it. Only a variable with
|
|
93
|
+
* at least one definition was actually written down somewhere in this file.
|
|
94
|
+
*/
|
|
95
|
+
const isLocallyBound = (
|
|
96
|
+
sourceCode: TSESLint.SourceCode,
|
|
97
|
+
node: TSESTree.Node,
|
|
98
|
+
name: string,
|
|
99
|
+
): boolean => {
|
|
100
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);
|
|
101
|
+
|
|
102
|
+
return variable !== null && Arr.isNonEmpty(variable.defs);
|
|
103
|
+
};
|
|
@@ -1,65 +1,10 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
getImportedLocalName,
|
|
6
|
-
getTsTypeForgeImports,
|
|
7
|
-
hasConflictingDeclaration,
|
|
8
|
-
} from './import-utils.mjs';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import { resolveTypeName, type ImportStyle } from './import-style.mjs';
|
|
4
|
+
import { buildTypeImportFix } from './import-utils.mjs';
|
|
9
5
|
|
|
10
6
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
11
7
|
|
|
12
|
-
/** `JSONSchema4`, reached through the public rule-module types. */
|
|
13
|
-
type JsonSchema = Exclude<
|
|
14
|
-
TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],
|
|
15
|
-
readonly unknown[]
|
|
16
|
-
>;
|
|
17
|
-
|
|
18
|
-
export type ImportStyle = 'global' | 'named';
|
|
19
|
-
|
|
20
|
-
export const DEFAULT_IMPORT_STYLE: ImportStyle = 'global';
|
|
21
|
-
|
|
22
|
-
export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
|
|
23
|
-
type: 'string',
|
|
24
|
-
enum: ['global', 'named'],
|
|
25
|
-
description: [
|
|
26
|
-
'How the ts-type-forge type is brought into scope.',
|
|
27
|
-
"'global' (default) assumes the ambient globals of",
|
|
28
|
-
'`ts-type-forge/global` and never touches imports;',
|
|
29
|
-
"'named' makes the autofix add the corresponding",
|
|
30
|
-
`\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\``,
|
|
31
|
-
'when the name is not imported yet.',
|
|
32
|
-
].join(' '),
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Resolves how a ts-type-forge type name should be written in this file, and
|
|
37
|
-
* whether the autofix has to add an import for it.
|
|
38
|
-
*
|
|
39
|
-
* Returns `undefined` when the name is already bound to something else (a local
|
|
40
|
-
* alias, or an import from another module) — rewriting to it would then silently
|
|
41
|
-
* resolve to the wrong declaration, which is exactly what happens inside
|
|
42
|
-
* ts-type-forge's own sources.
|
|
43
|
-
*/
|
|
44
|
-
export const resolveTypeName = (
|
|
45
|
-
program: TSESTree.Program,
|
|
46
|
-
canonicalName: string,
|
|
47
|
-
importStyle: ImportStyle,
|
|
48
|
-
): Readonly<{ localName: string; needsImport: boolean }> | undefined => {
|
|
49
|
-
const importedLocalName = getImportedLocalName(
|
|
50
|
-
getTsTypeForgeImports(program),
|
|
51
|
-
canonicalName,
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
if (importedLocalName !== undefined) {
|
|
55
|
-
return { localName: importedLocalName, needsImport: false };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return hasConflictingDeclaration(program, canonicalName)
|
|
59
|
-
? undefined
|
|
60
|
-
: { localName: canonicalName, needsImport: importStyle === 'named' };
|
|
61
|
-
};
|
|
62
|
-
|
|
63
8
|
export type TupleRewrite = Readonly<{
|
|
64
9
|
node: TSESTree.Node;
|
|
65
10
|
canonicalName: string;
|
|
@@ -109,14 +54,15 @@ export const reportTupleRewrites = <MessageIds extends string>(
|
|
|
109
54
|
node: rewrite.node,
|
|
110
55
|
messageId,
|
|
111
56
|
data: { original: sourceCode.getText(rewrite.node), replacement },
|
|
112
|
-
fix: (fixer) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
57
|
+
fix: (fixer) =>
|
|
58
|
+
Arr.toPushed(
|
|
59
|
+
index === 0
|
|
60
|
+
? importNames.flatMap((name) =>
|
|
61
|
+
buildTypeImportFix(fixer, program, name),
|
|
62
|
+
)
|
|
63
|
+
: [],
|
|
64
|
+
fixer.replaceText(rewrite.node, replacement),
|
|
65
|
+
),
|
|
120
66
|
});
|
|
121
67
|
}
|
|
122
68
|
};
|