eslint-plugin-ts-type-forge 0.8.15 → 0.8.17
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 +5 -2
- 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 +7 -7
- package/dist/rules/ast-utils.mjs +71 -71
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-style.d.mts +3 -2
- package/dist/rules/import-style.d.mts.map +1 -1
- package/dist/rules/import-style.mjs +4 -3
- package/dist/rules/import-style.mjs.map +1 -1
- package/dist/rules/import-utils.d.mts +12 -2
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs +16 -3
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/rewrite-rule-utils.d.mts +8 -7
- package/dist/rules/rewrite-rule-utils.d.mts.map +1 -1
- package/dist/rules/rewrite-rule-utils.mjs +9 -7
- package/dist/rules/rewrite-rule-utils.mjs.map +1 -1
- package/dist/rules/std-type-rule-utils.d.mts.map +1 -1
- package/dist/rules/std-type-rule-utils.mjs +3 -5
- package/dist/rules/std-type-rule-utils.mjs.map +1 -1
- package/package.json +6 -6
- package/src/plugin.mts +4 -3
- package/src/rule-types.mts +7 -7
- package/src/rules/ast-utils.mts +108 -108
- package/src/rules/import-style.mts +6 -5
- package/src/rules/import-utils.mts +31 -5
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +49 -12
- package/src/rules/prefer-canonical-mutable-record.test.mts +120 -5
- package/src/rules/prefer-readonly-or-mutable-record.test.mts +29 -2
- package/src/rules/prefer-strict-or-relaxed-utility-type.test.mts +1 -2
- package/src/rules/rewrite-rule-utils.mts +21 -16
- package/src/rules/std-type-rule-utils.mts +3 -5
package/src/rule-types.mts
CHANGED
|
@@ -15,9 +15,9 @@ type SpreadOptionsIfIsArray<
|
|
|
15
15
|
: T;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Extracts the options tuple type of a rule from its TypeScript
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* Extracts the options tuple type of a rule from its TypeScript implementation,
|
|
19
|
+
* so that option types stay in sync with the rule source without converting
|
|
20
|
+
* `meta.schema` (JSON Schema) back into TypeScript types.
|
|
21
21
|
*/
|
|
22
22
|
type RuleOptionsOf<Name extends keyof typeof tsTypeForgeRules> =
|
|
23
23
|
(typeof tsTypeForgeRules)[Name] extends TSESLint.RuleModule<
|
|
@@ -68,7 +68,7 @@ namespace PreferCanonicalLengthConstrainedTuple {
|
|
|
68
68
|
* "global",
|
|
69
69
|
* "named"
|
|
70
70
|
* ],
|
|
71
|
-
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix
|
|
71
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix import the type from `ts-type-forge` when the name is not imported yet, adding it to an existing import of the module where there is one; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
72
72
|
* },
|
|
73
73
|
* "maxLength": {
|
|
74
74
|
* "type": "integer",
|
|
@@ -117,7 +117,7 @@ namespace PreferCanonicalMutableRecord {
|
|
|
117
117
|
* "global",
|
|
118
118
|
* "named"
|
|
119
119
|
* ],
|
|
120
|
-
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix
|
|
120
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix import the type from `ts-type-forge` when the name is not imported yet, adding it to an existing import of the module where there is one; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
121
121
|
* }
|
|
122
122
|
* },
|
|
123
123
|
* "additionalProperties": false
|
|
@@ -161,7 +161,7 @@ namespace PreferReadonlyOrMutableRecord {
|
|
|
161
161
|
* "global",
|
|
162
162
|
* "named"
|
|
163
163
|
* ],
|
|
164
|
-
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix
|
|
164
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix import the type from `ts-type-forge` when the name is not imported yet, adding it to an existing import of the module where there is one; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
165
165
|
* }
|
|
166
166
|
* },
|
|
167
167
|
* "additionalProperties": false
|
|
@@ -205,7 +205,7 @@ namespace PreferStrictOrRelaxedUtilityType {
|
|
|
205
205
|
* "global",
|
|
206
206
|
* "named"
|
|
207
207
|
* ],
|
|
208
|
-
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix
|
|
208
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix import the type from `ts-type-forge` when the name is not imported yet, adding it to an existing import of the module where there is one; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
209
209
|
* }
|
|
210
210
|
* },
|
|
211
211
|
* "additionalProperties": false
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -143,6 +143,114 @@ const isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {
|
|
|
143
143
|
);
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
+
/** The `type X = …` declaration `node` sits in, if any. */
|
|
147
|
+
const enclosingTypeAlias = (
|
|
148
|
+
node: TSESTree.Node,
|
|
149
|
+
): TSESTree.TSTypeAliasDeclaration | undefined =>
|
|
150
|
+
node.type === AST_NODE_TYPES.TSTypeAliasDeclaration
|
|
151
|
+
? node
|
|
152
|
+
: node.type === AST_NODE_TYPES.Program
|
|
153
|
+
? undefined
|
|
154
|
+
: enclosingTypeAlias(node.parent);
|
|
155
|
+
|
|
156
|
+
const isNode = (value: unknown): value is TSESTree.Node =>
|
|
157
|
+
isNonNullObject(value) &&
|
|
158
|
+
typeof (value as Readonly<{ type?: unknown }>).type === 'string';
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Visits `node` and its descendants.
|
|
162
|
+
*
|
|
163
|
+
* Children are found through the parser's visitor keys, so only the properties
|
|
164
|
+
* that actually hold one are read. Reading every own property instead works
|
|
165
|
+
* too, and is the fallback for a parser that declares no keys for a node type,
|
|
166
|
+
* but it allocates an entry pair per property and walks `loc` / `range` as well
|
|
167
|
+
* — on the generated rule-type files that alone was most of the cost.
|
|
168
|
+
*/
|
|
169
|
+
const walk = (
|
|
170
|
+
node: TSESTree.Node,
|
|
171
|
+
sourceCode: TSESLint.SourceCode,
|
|
172
|
+
visit: (visited: TSESTree.Node) => void,
|
|
173
|
+
): void => {
|
|
174
|
+
visit(node);
|
|
175
|
+
|
|
176
|
+
if (!isRecord(node)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const keys =
|
|
181
|
+
sourceCode.visitorKeys[node.type] ??
|
|
182
|
+
Object.keys(node).filter((key) => key !== 'parent');
|
|
183
|
+
|
|
184
|
+
for (const key of keys) {
|
|
185
|
+
const value: unknown = hasKey(node, key) ? node[key] : undefined;
|
|
186
|
+
|
|
187
|
+
if (Arr.isArray(value)) {
|
|
188
|
+
for (const element of value) {
|
|
189
|
+
if (isNode(element)) {
|
|
190
|
+
walk(element, sourceCode, visit);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} else if (isNode(value)) {
|
|
194
|
+
walk(value, sourceCode, visit);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/** The names every `X` / `X<…>` reference in the subtree points at. */
|
|
200
|
+
const referencedTypeNames = (
|
|
201
|
+
node: TSESTree.Node,
|
|
202
|
+
sourceCode: TSESLint.SourceCode,
|
|
203
|
+
): ReadonlySet<string> => {
|
|
204
|
+
const mut_names = new Set<string>();
|
|
205
|
+
|
|
206
|
+
walk(node, sourceCode, (visited) => {
|
|
207
|
+
if (
|
|
208
|
+
visited.type === AST_NODE_TYPES.TSTypeReference &&
|
|
209
|
+
visited.typeName.type === AST_NODE_TYPES.Identifier
|
|
210
|
+
) {
|
|
211
|
+
mut_names.add(visited.typeName.name);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return mut_names;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Which other type names each `type X = …` in the file refers to, keyed by `X`.
|
|
220
|
+
*
|
|
221
|
+
* Built once per file and cached on the `Program` node: the search below runs
|
|
222
|
+
* per candidate tuple, and rebuilding this for each of them made the rule cost
|
|
223
|
+
* `O(tuples × file size)` — a full second on the largest generated files here.
|
|
224
|
+
*/
|
|
225
|
+
const aliasReferenceGraph = (
|
|
226
|
+
program: TSESTree.Program,
|
|
227
|
+
sourceCode: TSESLint.SourceCode,
|
|
228
|
+
): ReadonlyMap<string, ReadonlySet<string>> => {
|
|
229
|
+
const cached = aliasReferenceGraphCache.get(program);
|
|
230
|
+
|
|
231
|
+
if (cached !== undefined) {
|
|
232
|
+
return cached;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const mut_graph = new Map<string, ReadonlySet<string>>();
|
|
236
|
+
|
|
237
|
+
walk(program, sourceCode, (visited) => {
|
|
238
|
+
if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
239
|
+
mut_graph.set(
|
|
240
|
+
visited.id.name,
|
|
241
|
+
referencedTypeNames(visited.typeAnnotation, sourceCode),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
aliasReferenceGraphCache.set(program, mut_graph);
|
|
247
|
+
|
|
248
|
+
return mut_graph;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const programOf = (node: TSESTree.Node): TSESTree.Program =>
|
|
252
|
+
node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);
|
|
253
|
+
|
|
146
254
|
/**
|
|
147
255
|
* Whether the tuple spells out an alias that its own element type leads back
|
|
148
256
|
* to.
|
|
@@ -225,119 +333,11 @@ const definesRecursiveTypeAlias = (
|
|
|
225
333
|
}
|
|
226
334
|
};
|
|
227
335
|
|
|
228
|
-
/** The `type X = …` declaration `node` sits in, if any. */
|
|
229
|
-
const enclosingTypeAlias = (
|
|
230
|
-
node: TSESTree.Node,
|
|
231
|
-
): TSESTree.TSTypeAliasDeclaration | undefined =>
|
|
232
|
-
node.type === AST_NODE_TYPES.TSTypeAliasDeclaration
|
|
233
|
-
? node
|
|
234
|
-
: node.type === AST_NODE_TYPES.Program
|
|
235
|
-
? undefined
|
|
236
|
-
: enclosingTypeAlias(node.parent);
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Which other type names each `type X = …` in the file refers to, keyed by `X`.
|
|
240
|
-
*
|
|
241
|
-
* Built once per file and cached on the `Program` node: the search below runs
|
|
242
|
-
* per candidate tuple, and rebuilding this for each of them made the rule cost
|
|
243
|
-
* `O(tuples × file size)` — a full second on the largest generated files here.
|
|
244
|
-
*/
|
|
245
|
-
const aliasReferenceGraph = (
|
|
246
|
-
program: TSESTree.Program,
|
|
247
|
-
sourceCode: TSESLint.SourceCode,
|
|
248
|
-
): ReadonlyMap<string, ReadonlySet<string>> => {
|
|
249
|
-
const cached = aliasReferenceGraphCache.get(program);
|
|
250
|
-
|
|
251
|
-
if (cached !== undefined) {
|
|
252
|
-
return cached;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const mut_graph = new Map<string, ReadonlySet<string>>();
|
|
256
|
-
|
|
257
|
-
walk(program, sourceCode, (visited) => {
|
|
258
|
-
if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
259
|
-
mut_graph.set(
|
|
260
|
-
visited.id.name,
|
|
261
|
-
referencedTypeNames(visited.typeAnnotation, sourceCode),
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
aliasReferenceGraphCache.set(program, mut_graph);
|
|
267
|
-
|
|
268
|
-
return mut_graph;
|
|
269
|
-
};
|
|
270
|
-
|
|
271
336
|
const aliasReferenceGraphCache = new WeakMap<
|
|
272
337
|
TSESTree.Program,
|
|
273
338
|
ReadonlyMap<string, ReadonlySet<string>>
|
|
274
339
|
>();
|
|
275
340
|
|
|
276
|
-
const programOf = (node: TSESTree.Node): TSESTree.Program =>
|
|
277
|
-
node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);
|
|
278
|
-
|
|
279
|
-
/** The names every `X` / `X<…>` reference in the subtree points at. */
|
|
280
|
-
const referencedTypeNames = (
|
|
281
|
-
node: TSESTree.Node,
|
|
282
|
-
sourceCode: TSESLint.SourceCode,
|
|
283
|
-
): ReadonlySet<string> => {
|
|
284
|
-
const mut_names = new Set<string>();
|
|
285
|
-
|
|
286
|
-
walk(node, sourceCode, (visited) => {
|
|
287
|
-
if (
|
|
288
|
-
visited.type === AST_NODE_TYPES.TSTypeReference &&
|
|
289
|
-
visited.typeName.type === AST_NODE_TYPES.Identifier
|
|
290
|
-
) {
|
|
291
|
-
mut_names.add(visited.typeName.name);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
return mut_names;
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Visits `node` and its descendants.
|
|
300
|
-
*
|
|
301
|
-
* Children are found through the parser's visitor keys, so only the properties
|
|
302
|
-
* that actually hold one are read. Reading every own property instead works
|
|
303
|
-
* too, and is the fallback for a parser that declares no keys for a node type,
|
|
304
|
-
* but it allocates an entry pair per property and walks `loc` / `range` as
|
|
305
|
-
* well — on the generated rule-type files that alone was most of the cost.
|
|
306
|
-
*/
|
|
307
|
-
const walk = (
|
|
308
|
-
node: TSESTree.Node,
|
|
309
|
-
sourceCode: TSESLint.SourceCode,
|
|
310
|
-
visit: (visited: TSESTree.Node) => void,
|
|
311
|
-
): void => {
|
|
312
|
-
visit(node);
|
|
313
|
-
|
|
314
|
-
if (!isRecord(node)) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const keys =
|
|
319
|
-
sourceCode.visitorKeys[node.type] ??
|
|
320
|
-
Object.keys(node).filter((key) => key !== 'parent');
|
|
321
|
-
|
|
322
|
-
for (const key of keys) {
|
|
323
|
-
const value: unknown = hasKey(node, key) ? node[key] : undefined;
|
|
324
|
-
|
|
325
|
-
if (Arr.isArray(value)) {
|
|
326
|
-
for (const element of value) {
|
|
327
|
-
if (isNode(element)) {
|
|
328
|
-
walk(element, sourceCode, visit);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
} else if (isNode(value)) {
|
|
332
|
-
walk(value, sourceCode, visit);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const isNode = (value: unknown): value is TSESTree.Node =>
|
|
338
|
-
isNonNullObject(value) &&
|
|
339
|
-
typeof (value as Readonly<{ type?: unknown }>).type === 'string';
|
|
340
|
-
|
|
341
341
|
/** Unwraps `V[]` and `readonly V[]` to `V`. */
|
|
342
342
|
const getArrayElementType = (
|
|
343
343
|
node: TSESTree.TypeNode,
|
|
@@ -20,9 +20,9 @@ export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
|
|
|
20
20
|
enum: ['global', 'named'],
|
|
21
21
|
description: [
|
|
22
22
|
'How the ts-type-forge type is brought into scope.',
|
|
23
|
-
"'named' (default) makes the autofix
|
|
24
|
-
|
|
25
|
-
"
|
|
23
|
+
"'named' (default) makes the autofix import the type from",
|
|
24
|
+
`\`${TS_TYPE_FORGE_MODULE}\` when the name is not imported yet,`,
|
|
25
|
+
"adding it to an existing import of the module where there is one; 'global' assumes the",
|
|
26
26
|
'ambient globals of `ts-type-forge/global` and never touches',
|
|
27
27
|
'imports.',
|
|
28
28
|
].join(' '),
|
|
@@ -33,8 +33,9 @@ export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
|
|
|
33
33
|
* whether the fix has to add an import for it.
|
|
34
34
|
*
|
|
35
35
|
* Returns `undefined` when the name is already bound to something else (a local
|
|
36
|
-
* alias,
|
|
37
|
-
*
|
|
36
|
+
* alias, an import from another module, or another ts-type-forge export
|
|
37
|
+
* imported under the name) — rewriting to it would then silently resolve to the
|
|
38
|
+
* wrong declaration, which is exactly what happens inside
|
|
38
39
|
* ts-type-forge's own sources.
|
|
39
40
|
*/
|
|
40
41
|
export const resolveTypeName = (
|
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
6
|
+
import { addTypeImports, importedNameOf } from 'eslint-rule-forge';
|
|
2
7
|
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
3
8
|
|
|
4
9
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
5
10
|
|
|
11
|
+
/**
|
|
12
|
+
* The edits that import the ts-type-forge types `names` the file does not
|
|
13
|
+
* import yet, as `addTypeImports` places them.
|
|
14
|
+
*
|
|
15
|
+
* Fixes that add an import overlap, so `--fix` applies one per pass, and the
|
|
16
|
+
* rest in later passes, by when the file imports their names (see
|
|
17
|
+
* `resolveTypeName`).
|
|
18
|
+
*/
|
|
19
|
+
export const buildTypeImportFix = (
|
|
20
|
+
fixer: TSESLint.RuleFixer,
|
|
21
|
+
sourceCode: Readonly<TSESLint.SourceCode>,
|
|
22
|
+
names: readonly string[],
|
|
23
|
+
): readonly TSESLint.RuleFix[] =>
|
|
24
|
+
addTypeImports(fixer, sourceCode, TS_TYPE_FORGE_MODULE, names);
|
|
25
|
+
|
|
6
26
|
/**
|
|
7
27
|
* Whether the file already binds `name` to something that is not the
|
|
8
28
|
* ts-type-forge export of the same name — a local type alias / interface /
|
|
9
|
-
* class / enum,
|
|
29
|
+
* class / enum, an import from another module, or another ts-type-forge
|
|
30
|
+
* export imported under that name. Autofixes that introduce
|
|
10
31
|
* `name` must bail out in that case, because the rewritten type would resolve
|
|
11
32
|
* to the wrong declaration. (ts-type-forge's own sources, which declare
|
|
12
33
|
* `NonEmptyArray` themselves, are the motivating example.)
|
|
@@ -20,9 +41,14 @@ export const hasConflictingDeclaration = (
|
|
|
20
41
|
): boolean =>
|
|
21
42
|
program.body.some((node) => {
|
|
22
43
|
if (node.type === AST_NODE_TYPES.ImportDeclaration) {
|
|
23
|
-
return (
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
return node.specifiers.some(
|
|
45
|
+
(specifier) =>
|
|
46
|
+
specifier.local.name === name &&
|
|
47
|
+
!(
|
|
48
|
+
node.source.value === TS_TYPE_FORGE_MODULE &&
|
|
49
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
50
|
+
importedNameOf(specifier) === name
|
|
51
|
+
),
|
|
26
52
|
);
|
|
27
53
|
}
|
|
28
54
|
|
|
@@ -255,13 +255,20 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
255
255
|
type B = readonly [number, number, number];
|
|
256
256
|
type C = [number, number, ...number[]];
|
|
257
257
|
`,
|
|
258
|
-
output:
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
output: [
|
|
259
|
+
dedent`
|
|
260
|
+
import { type FixedLengthTuple, type MutableMinLengthTuple } from 'ts-type-forge';
|
|
261
|
+
type A = FixedLengthTuple<2, string>;
|
|
262
|
+
type B = readonly [number, number, number];
|
|
263
|
+
type C = [number, number, ...number[]];
|
|
264
|
+
`,
|
|
265
|
+
dedent`
|
|
266
|
+
import { type FixedLengthTuple, type MutableMinLengthTuple } from 'ts-type-forge';
|
|
267
|
+
type A = FixedLengthTuple<2, string>;
|
|
268
|
+
type B = FixedLengthTuple<3, number>;
|
|
269
|
+
type C = MutableMinLengthTuple<2, number>;
|
|
270
|
+
`,
|
|
271
|
+
],
|
|
265
272
|
errors: [
|
|
266
273
|
{ messageId: 'useCanonicalTuple' },
|
|
267
274
|
{ messageId: 'useCanonicalTuple' },
|
|
@@ -290,13 +297,43 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
290
297
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
291
298
|
},
|
|
292
299
|
{
|
|
293
|
-
name: '
|
|
294
|
-
code:
|
|
295
|
-
output: dedent`
|
|
300
|
+
name: 'gives a report that needs no import no import edit',
|
|
301
|
+
code: dedent`
|
|
296
302
|
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
type A = readonly [number, number];
|
|
304
|
+
type C = [number, number, ...number[]];
|
|
299
305
|
`,
|
|
306
|
+
// C's import edit spans A, so A follows in the next pass.
|
|
307
|
+
output: [
|
|
308
|
+
dedent`
|
|
309
|
+
import { type FixedLengthTuple, type MutableMinLengthTuple } from 'ts-type-forge';
|
|
310
|
+
type A = readonly [number, number];
|
|
311
|
+
type C = MutableMinLengthTuple<2, number>;
|
|
312
|
+
`,
|
|
313
|
+
dedent`
|
|
314
|
+
import { type FixedLengthTuple, type MutableMinLengthTuple } from 'ts-type-forge';
|
|
315
|
+
type A = FixedLengthTuple<2, number>;
|
|
316
|
+
type C = MutableMinLengthTuple<2, number>;
|
|
317
|
+
`,
|
|
318
|
+
],
|
|
319
|
+
errors: [
|
|
320
|
+
{ messageId: 'useCanonicalTuple' },
|
|
321
|
+
{ messageId: 'useCanonicalTuple' },
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'rewrites parameter and return type positions',
|
|
326
|
+
code: 'declare const f: (xs: readonly [number, number]) => [string, string];',
|
|
327
|
+
output: [
|
|
328
|
+
dedent`
|
|
329
|
+
import { type FixedLengthTuple, type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
330
|
+
declare const f: (xs: FixedLengthTuple<2, number>) => [string, string];
|
|
331
|
+
`,
|
|
332
|
+
dedent`
|
|
333
|
+
import { type FixedLengthTuple, type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
334
|
+
declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;
|
|
335
|
+
`,
|
|
336
|
+
],
|
|
300
337
|
errors: [
|
|
301
338
|
{ messageId: 'useCanonicalTuple' },
|
|
302
339
|
{ messageId: 'useCanonicalTuple' },
|
|
@@ -50,6 +50,13 @@ describe('prefer-canonical-mutable-record', () => {
|
|
|
50
50
|
type A = Mutable<Record<string, number>>;
|
|
51
51
|
`,
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
name: 'stays silent when another ts-type-forge export is imported as `MutableRecord`',
|
|
55
|
+
code: dedent`
|
|
56
|
+
import { type DeepReadonly as MutableRecord } from 'ts-type-forge';
|
|
57
|
+
type A = Mutable<Record<string, number>>;
|
|
58
|
+
`,
|
|
59
|
+
},
|
|
53
60
|
{
|
|
54
61
|
name: 'stays silent when `MutableRecord` is bound to another module',
|
|
55
62
|
code: dedent`
|
|
@@ -107,16 +114,124 @@ describe('prefer-canonical-mutable-record', () => {
|
|
|
107
114
|
type A = Mutable<Record<string, number>>;
|
|
108
115
|
type B = Mutable<ReadonlyRecord<string, boolean>>;
|
|
109
116
|
`,
|
|
110
|
-
output:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
output: [
|
|
118
|
+
dedent`
|
|
119
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
120
|
+
type A = MutableRecord<string, number>;
|
|
121
|
+
type B = Mutable<ReadonlyRecord<string, boolean>>;
|
|
122
|
+
`,
|
|
123
|
+
dedent`
|
|
124
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
125
|
+
type A = MutableRecord<string, number>;
|
|
126
|
+
type B = MutableRecord<string, boolean>;
|
|
127
|
+
`,
|
|
128
|
+
],
|
|
115
129
|
errors: [
|
|
116
130
|
{ messageId: 'useMutableRecord' },
|
|
117
131
|
{ messageId: 'useMutableRecord' },
|
|
118
132
|
],
|
|
119
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: 'adds the name to an existing `import type` from ts-type-forge',
|
|
136
|
+
code: dedent`
|
|
137
|
+
import type { Mutable } from 'ts-type-forge';
|
|
138
|
+
type A = Mutable<Record<string, number>>;
|
|
139
|
+
`,
|
|
140
|
+
output: dedent`
|
|
141
|
+
import type { Mutable, MutableRecord } from 'ts-type-forge';
|
|
142
|
+
type A = MutableRecord<string, number>;
|
|
143
|
+
`,
|
|
144
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'adds the name to an existing `import { … }` from ts-type-forge',
|
|
148
|
+
code: dedent`
|
|
149
|
+
import { type Mutable, type DeepReadonly } from 'ts-type-forge';
|
|
150
|
+
type A = Mutable<Record<string, number>>;
|
|
151
|
+
`,
|
|
152
|
+
output: dedent`
|
|
153
|
+
import { type Mutable, type DeepReadonly, type MutableRecord } from 'ts-type-forge';
|
|
154
|
+
type A = MutableRecord<string, number>;
|
|
155
|
+
`,
|
|
156
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'adds named braces after a lone default import',
|
|
160
|
+
code: dedent`
|
|
161
|
+
import forge from 'ts-type-forge';
|
|
162
|
+
type A = Mutable<Record<string, number>>;
|
|
163
|
+
`,
|
|
164
|
+
output: dedent`
|
|
165
|
+
import forge, { type MutableRecord } from 'ts-type-forge';
|
|
166
|
+
type A = MutableRecord<string, number>;
|
|
167
|
+
`,
|
|
168
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'fills the empty braces of `import d, {}`',
|
|
172
|
+
code: dedent`
|
|
173
|
+
import forge, {} from 'ts-type-forge';
|
|
174
|
+
type A = Mutable<Record<string, number>>;
|
|
175
|
+
`,
|
|
176
|
+
output: dedent`
|
|
177
|
+
import forge, { type MutableRecord } from 'ts-type-forge';
|
|
178
|
+
type A = MutableRecord<string, number>;
|
|
179
|
+
`,
|
|
180
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'fills the empty braces of `import type {}`',
|
|
184
|
+
code: dedent`
|
|
185
|
+
import type {} from 'ts-type-forge';
|
|
186
|
+
type A = Mutable<Record<string, number>>;
|
|
187
|
+
`,
|
|
188
|
+
output: dedent`
|
|
189
|
+
import type { MutableRecord } from 'ts-type-forge';
|
|
190
|
+
type A = MutableRecord<string, number>;
|
|
191
|
+
`,
|
|
192
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'does not take the brace of import attributes for the specifiers',
|
|
196
|
+
code: dedent`
|
|
197
|
+
import type forge from 'ts-type-forge' with { 'resolution-mode': 'import' };
|
|
198
|
+
type A = Mutable<Record<string, number>>;
|
|
199
|
+
`,
|
|
200
|
+
output: dedent`
|
|
201
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
202
|
+
import type forge from 'ts-type-forge' with { 'resolution-mode': 'import' };
|
|
203
|
+
type A = MutableRecord<string, number>;
|
|
204
|
+
`,
|
|
205
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'adds a new declaration beside a type-only default import',
|
|
209
|
+
code: dedent`
|
|
210
|
+
import type forge from 'ts-type-forge';
|
|
211
|
+
type A = Mutable<Record<string, number>>;
|
|
212
|
+
`,
|
|
213
|
+
output: dedent`
|
|
214
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
215
|
+
import type forge from 'ts-type-forge';
|
|
216
|
+
type A = MutableRecord<string, number>;
|
|
217
|
+
`,
|
|
218
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'adds a new declaration beside a namespace import, after the directives',
|
|
222
|
+
code: dedent`
|
|
223
|
+
'use strict';
|
|
224
|
+
import * as forge from 'ts-type-forge';
|
|
225
|
+
type A = Mutable<Record<string, number>>;
|
|
226
|
+
`,
|
|
227
|
+
output: dedent`
|
|
228
|
+
'use strict';
|
|
229
|
+
import { type MutableRecord } from 'ts-type-forge';
|
|
230
|
+
import * as forge from 'ts-type-forge';
|
|
231
|
+
type A = MutableRecord<string, number>;
|
|
232
|
+
`,
|
|
233
|
+
errors: [{ messageId: 'useMutableRecord' }],
|
|
234
|
+
},
|
|
120
235
|
{
|
|
121
236
|
name: 'follows ts-type-forge import aliases on both sides',
|
|
122
237
|
code: dedent`
|
|
@@ -120,8 +120,35 @@ describe('prefer-readonly-or-mutable-record', () => {
|
|
|
120
120
|
messageId: 'replaceWith',
|
|
121
121
|
data: { name: 'Record', replacement: 'MutableRecord' },
|
|
122
122
|
output: dedent`
|
|
123
|
-
import { type MutableRecord } from 'ts-type-forge';
|
|
124
|
-
|
|
123
|
+
import { type ReadonlyRecord as RRecord, type MutableRecord } from 'ts-type-forge';
|
|
124
|
+
type A = MutableRecord<string, number>;
|
|
125
|
+
`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'adds the suggested name to an existing `import type`',
|
|
133
|
+
code: dedent`
|
|
134
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
135
|
+
type A = Record<string, number>;
|
|
136
|
+
`,
|
|
137
|
+
errors: [
|
|
138
|
+
{
|
|
139
|
+
messageId: 'useReadonlyOrMutableRecord',
|
|
140
|
+
suggestions: [
|
|
141
|
+
{
|
|
142
|
+
messageId: 'replaceWith',
|
|
143
|
+
output: dedent`
|
|
144
|
+
import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
|
|
145
|
+
type A = ReadonlyRecord<string, number>;
|
|
146
|
+
`,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
messageId: 'replaceWith',
|
|
150
|
+
output: dedent`
|
|
151
|
+
import type { DeepReadonly, MutableRecord } from 'ts-type-forge';
|
|
125
152
|
type A = MutableRecord<string, number>;
|
|
126
153
|
`,
|
|
127
154
|
},
|
|
@@ -197,8 +197,7 @@ describe('prefer-strict-or-relaxed-utility-type', () => {
|
|
|
197
197
|
messageId: 'replaceWith',
|
|
198
198
|
data: { name: 'Pick', replacement: 'RelaxedPick' },
|
|
199
199
|
output: dedent`
|
|
200
|
-
import { type RelaxedPick } from 'ts-type-forge';
|
|
201
|
-
import { type StrictPick as SPick } from 'ts-type-forge';
|
|
200
|
+
import { type StrictPick as SPick, type RelaxedPick } from 'ts-type-forge';
|
|
202
201
|
type A = RelaxedPick<{ a: 1 }, 'a'>;
|
|
203
202
|
`,
|
|
204
203
|
},
|