eslint-plugin-ts-type-forge 0.2.0 → 0.3.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 +53 -31
- package/dist/rule-types.d.mts +11 -6
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +26 -11
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +30 -14
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +12 -1
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +8 -2
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts +11 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +81 -0
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -0
- package/dist/rules/rules.d.mts +3 -2
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -2
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/rules/tuple-rule-utils.d.mts +37 -0
- package/dist/rules/tuple-rule-utils.d.mts.map +1 -0
- package/dist/rules/tuple-rule-utils.mjs +56 -0
- package/dist/rules/tuple-rule-utils.mjs.map +1 -0
- package/package.json +22 -22
- package/src/rule-types.mts +13 -6
- package/src/rules/ast-utils.mts +72 -30
- package/src/rules/constants.mts +18 -1
- package/src/rules/prefer-canonical-length-constrained-tuple.mts +149 -0
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +178 -0
- package/src/rules/rules.mts +3 -2
- package/src/rules/tuple-rule-utils.mts +124 -0
- package/dist/rules/prefer-non-empty-array.d.mts +0 -11
- package/dist/rules/prefer-non-empty-array.d.mts.map +0 -1
- package/dist/rules/prefer-non-empty-array.mjs +0 -81
- package/dist/rules/prefer-non-empty-array.mjs.map +0 -1
- package/src/rules/prefer-non-empty-array.mts +0 -129
- package/src/rules/prefer-non-empty-array.test.mts +0 -192
package/src/rules/rules.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ESLintPlugin } from '../types.mjs';
|
|
2
|
-
import {
|
|
2
|
+
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
3
3
|
|
|
4
4
|
export const tsTypeForgeRules = {
|
|
5
|
-
'prefer-
|
|
5
|
+
'prefer-canonical-length-constrained-tuple':
|
|
6
|
+
preferCanonicalLengthConstrainedTuple,
|
|
6
7
|
} as const satisfies ESLintPlugin['rules'];
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
3
|
+
import {
|
|
4
|
+
buildTypeImportFix,
|
|
5
|
+
getImportedLocalName,
|
|
6
|
+
getTsTypeForgeImports,
|
|
7
|
+
hasConflictingDeclaration,
|
|
8
|
+
} from './import-utils.mjs';
|
|
9
|
+
|
|
10
|
+
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
11
|
+
|
|
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
|
+
export type TupleRewrite = Readonly<{
|
|
64
|
+
node: TSESTree.Node;
|
|
65
|
+
canonicalName: string;
|
|
66
|
+
/** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */
|
|
67
|
+
typeArgs: string;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Emits one report per rewrite, resolving each target name and attaching every
|
|
72
|
+
* required import to the *first* report.
|
|
73
|
+
*
|
|
74
|
+
* All import fixes must live on a single report: `insertTextBefore(program, …)`
|
|
75
|
+
* produces overlapping ranges, and ESLint merges each report's fixes into one
|
|
76
|
+
* span — so spreading them across reports makes the fixes conflict and only one
|
|
77
|
+
* survives per pass.
|
|
78
|
+
*/
|
|
79
|
+
export const reportTupleRewrites = <MessageIds extends string>(
|
|
80
|
+
context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,
|
|
81
|
+
rewrites: readonly TupleRewrite[],
|
|
82
|
+
messageId: MessageIds,
|
|
83
|
+
importStyle: ImportStyle,
|
|
84
|
+
): void => {
|
|
85
|
+
const sourceCode = context.sourceCode;
|
|
86
|
+
|
|
87
|
+
const program = sourceCode.ast;
|
|
88
|
+
|
|
89
|
+
const resolved = rewrites.flatMap((rewrite) => {
|
|
90
|
+
const target = resolveTypeName(program, rewrite.canonicalName, importStyle);
|
|
91
|
+
|
|
92
|
+
// The name is already bound to something else in this file, so the rewrite
|
|
93
|
+
// would resolve to the wrong declaration.
|
|
94
|
+
return target === undefined ? [] : [{ rewrite, target }];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const importNames = Array.from(
|
|
98
|
+
new Set(
|
|
99
|
+
resolved
|
|
100
|
+
.filter(({ target }) => target.needsImport)
|
|
101
|
+
.map(({ rewrite }) => rewrite.canonicalName),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
for (const [index, { rewrite, target }] of resolved.entries()) {
|
|
106
|
+
const replacement = `${target.localName}<${rewrite.typeArgs}>`;
|
|
107
|
+
|
|
108
|
+
context.report({
|
|
109
|
+
node: rewrite.node,
|
|
110
|
+
messageId,
|
|
111
|
+
data: { original: sourceCode.getText(rewrite.node), replacement },
|
|
112
|
+
fix: (fixer) => [
|
|
113
|
+
...(index === 0
|
|
114
|
+
? importNames.flatMap((name) =>
|
|
115
|
+
buildTypeImportFix(fixer, program, name),
|
|
116
|
+
)
|
|
117
|
+
: []),
|
|
118
|
+
fixer.replaceText(rewrite.node, replacement),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
type ImportStyle = 'global' | 'named';
|
|
3
|
-
type Options = readonly [
|
|
4
|
-
Readonly<{
|
|
5
|
-
importStyle?: ImportStyle;
|
|
6
|
-
}>?
|
|
7
|
-
];
|
|
8
|
-
type MessageIds = 'useNonEmptyArray';
|
|
9
|
-
export declare const preferNonEmptyArray: TSESLint.RuleModule<MessageIds, Options>;
|
|
10
|
-
export {};
|
|
11
|
-
//# sourceMappingURL=prefer-non-empty-array.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-non-empty-array.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-non-empty-array.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAiB,MAAM,0BAA0B,CAAC;AAaxE,KAAK,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEtC,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,WAAW,CAAC,EAAE,WAAW,CAAC;KAC3B,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,kBAAkB,CAAC;AAIrC,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CAuG/D,CAAC"}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { getNonEmptyArrayElementText } from './ast-utils.mjs';
|
|
2
|
-
import { NON_EMPTY_ARRAY_TYPE_NAME, TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
3
|
-
import { getImportedLocalName, getTsTypeForgeImports, hasConflictingDeclaration, buildTypeImportFix } from './import-utils.mjs';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_IMPORT_STYLE = "global";
|
|
6
|
-
const preferNonEmptyArray = {
|
|
7
|
-
meta: {
|
|
8
|
-
type: "suggestion",
|
|
9
|
-
docs: {
|
|
10
|
-
description: "Replace the `readonly [V, ...V[]]` tuple spelling with `NonEmptyArray<V>` from ts-type-forge."
|
|
11
|
-
},
|
|
12
|
-
fixable: "code",
|
|
13
|
-
schema: [
|
|
14
|
-
{
|
|
15
|
-
type: "object",
|
|
16
|
-
properties: {
|
|
17
|
-
importStyle: {
|
|
18
|
-
type: "string",
|
|
19
|
-
enum: ["global", "named"],
|
|
20
|
-
description: [
|
|
21
|
-
"How `NonEmptyArray` is brought into scope.",
|
|
22
|
-
"'global' (default) assumes the ambient globals of",
|
|
23
|
-
"`ts-type-forge/global` and never touches imports;",
|
|
24
|
-
"'named' makes the autofix add",
|
|
25
|
-
`\`import { type ${NON_EMPTY_ARRAY_TYPE_NAME} } from '${TS_TYPE_FORGE_MODULE}';\``,
|
|
26
|
-
"when the name is not imported yet."
|
|
27
|
-
].join(" ")
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
additionalProperties: false
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
messages: {
|
|
34
|
-
useNonEmptyArray: "Replace `{{original}}` with `{{replacement}}` from ts-type-forge."
|
|
35
|
-
}
|
|
36
|
-
},
|
|
37
|
-
create: (context) => {
|
|
38
|
-
const sourceCode = context.sourceCode;
|
|
39
|
-
const program = sourceCode.ast;
|
|
40
|
-
const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
41
|
-
const importedLocalName = getImportedLocalName(
|
|
42
|
-
getTsTypeForgeImports(program),
|
|
43
|
-
NON_EMPTY_ARRAY_TYPE_NAME
|
|
44
|
-
);
|
|
45
|
-
const mut_nodesToFix = [];
|
|
46
|
-
return {
|
|
47
|
-
TSTypeOperator: (node) => {
|
|
48
|
-
const elementText = getNonEmptyArrayElementText(node, sourceCode);
|
|
49
|
-
if (elementText === void 0) return;
|
|
50
|
-
mut_nodesToFix.push({ node, elementText });
|
|
51
|
-
},
|
|
52
|
-
"Program:exit": () => {
|
|
53
|
-
if (mut_nodesToFix.length === 0) return;
|
|
54
|
-
if (importedLocalName === void 0 && hasConflictingDeclaration(program, NON_EMPTY_ARRAY_TYPE_NAME)) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
const localName = importedLocalName ?? NON_EMPTY_ARRAY_TYPE_NAME;
|
|
58
|
-
const needsImport = importStyle === "named" && importedLocalName === void 0;
|
|
59
|
-
for (const [index, { node, elementText }] of mut_nodesToFix.entries()) {
|
|
60
|
-
const replacement = `${localName}<${elementText}>`;
|
|
61
|
-
context.report({
|
|
62
|
-
node,
|
|
63
|
-
messageId: "useNonEmptyArray",
|
|
64
|
-
data: {
|
|
65
|
-
original: sourceCode.getText(node),
|
|
66
|
-
replacement
|
|
67
|
-
},
|
|
68
|
-
fix: (fixer) => [
|
|
69
|
-
...index === 0 && needsImport ? buildTypeImportFix(fixer, program, NON_EMPTY_ARRAY_TYPE_NAME) : [],
|
|
70
|
-
fixer.replaceText(node, replacement)
|
|
71
|
-
]
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }]
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export { preferNonEmptyArray };
|
|
81
|
-
//# sourceMappingURL=prefer-non-empty-array.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-non-empty-array.mjs","sources":["../../src/rules/prefer-non-empty-array.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { getNonEmptyArrayElementText } from './ast-utils.mjs';\nimport {\n NON_EMPTY_ARRAY_TYPE_NAME,\n TS_TYPE_FORGE_MODULE,\n} from './constants.mjs';\nimport {\n buildTypeImportFix,\n getImportedLocalName,\n getTsTypeForgeImports,\n hasConflictingDeclaration,\n} from './import-utils.mjs';\n\ntype ImportStyle = 'global' | 'named';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: ImportStyle;\n }>?,\n];\n\ntype MessageIds = 'useNonEmptyArray';\n\nconst DEFAULT_IMPORT_STYLE: ImportStyle = 'global';\n\nexport const preferNonEmptyArray: TSESLint.RuleModule<MessageIds, Options> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace the `readonly [V, ...V[]]` tuple spelling with `NonEmptyArray<V>` from ts-type-forge.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: {\n type: 'string',\n enum: ['global', 'named'],\n description: [\n 'How `NonEmptyArray` is brought into scope.',\n \"'global' (default) assumes the ambient globals of\",\n '`ts-type-forge/global` and never touches imports;',\n \"'named' makes the autofix add\",\n `\\`import { type ${NON_EMPTY_ARRAY_TYPE_NAME} } from '${TS_TYPE_FORGE_MODULE}';\\``,\n 'when the name is not imported yet.',\n ].join(' '),\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useNonEmptyArray:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n // `context.options` is what the user actually configured; `defaultOptions`\n // is only merged in by `RuleCreator`, which plain rule objects do not use.\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const importedLocalName = getImportedLocalName(\n getTsTypeForgeImports(program),\n NON_EMPTY_ARRAY_TYPE_NAME,\n );\n\n const mut_nodesToFix: {\n node: TSESTree.TSTypeOperator;\n elementText: string;\n }[] = [];\n\n return {\n TSTypeOperator: (node) => {\n const elementText = getNonEmptyArrayElementText(node, sourceCode);\n\n if (elementText === undefined) return;\n\n mut_nodesToFix.push({ node, elementText });\n },\n 'Program:exit': () => {\n if (mut_nodesToFix.length === 0) return;\n\n // Rewriting to `NonEmptyArray<...>` is only sound when that name is\n // free (or already bound to the ts-type-forge export). Otherwise the\n // rewritten type would resolve to the wrong declaration — most\n // notably inside ts-type-forge's own sources, which define\n // `NonEmptyArray`.\n if (\n importedLocalName === undefined &&\n hasConflictingDeclaration(program, NON_EMPTY_ARRAY_TYPE_NAME)\n ) {\n return;\n }\n\n const localName = importedLocalName ?? NON_EMPTY_ARRAY_TYPE_NAME;\n\n const needsImport =\n importStyle === 'named' && importedLocalName === undefined;\n\n for (const [index, { node, elementText }] of mut_nodesToFix.entries()) {\n const replacement = `${localName}<${elementText}>`;\n\n context.report({\n node,\n messageId: 'useNonEmptyArray',\n data: {\n original: sourceCode.getText(node),\n replacement,\n },\n fix: (fixer) => [\n ...(index === 0 && needsImport\n ? buildTypeImportFix(fixer, program, NON_EMPTY_ARRAY_TYPE_NAME)\n : []),\n fixer.replaceText(node, replacement),\n ],\n });\n }\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import { getNonEmptyArrayElementText } from './ast-utils.mjs';
|
|
3
|
-
import {
|
|
4
|
-
NON_EMPTY_ARRAY_TYPE_NAME,
|
|
5
|
-
TS_TYPE_FORGE_MODULE,
|
|
6
|
-
} from './constants.mjs';
|
|
7
|
-
import {
|
|
8
|
-
buildTypeImportFix,
|
|
9
|
-
getImportedLocalName,
|
|
10
|
-
getTsTypeForgeImports,
|
|
11
|
-
hasConflictingDeclaration,
|
|
12
|
-
} from './import-utils.mjs';
|
|
13
|
-
|
|
14
|
-
type ImportStyle = 'global' | 'named';
|
|
15
|
-
|
|
16
|
-
type Options = readonly [
|
|
17
|
-
Readonly<{
|
|
18
|
-
importStyle?: ImportStyle;
|
|
19
|
-
}>?,
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
type MessageIds = 'useNonEmptyArray';
|
|
23
|
-
|
|
24
|
-
const DEFAULT_IMPORT_STYLE: ImportStyle = 'global';
|
|
25
|
-
|
|
26
|
-
export const preferNonEmptyArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
27
|
-
meta: {
|
|
28
|
-
type: 'suggestion',
|
|
29
|
-
docs: {
|
|
30
|
-
description:
|
|
31
|
-
'Replace the `readonly [V, ...V[]]` tuple spelling with `NonEmptyArray<V>` from ts-type-forge.',
|
|
32
|
-
},
|
|
33
|
-
fixable: 'code',
|
|
34
|
-
schema: [
|
|
35
|
-
{
|
|
36
|
-
type: 'object',
|
|
37
|
-
properties: {
|
|
38
|
-
importStyle: {
|
|
39
|
-
type: 'string',
|
|
40
|
-
enum: ['global', 'named'],
|
|
41
|
-
description: [
|
|
42
|
-
'How `NonEmptyArray` is brought into scope.',
|
|
43
|
-
"'global' (default) assumes the ambient globals of",
|
|
44
|
-
'`ts-type-forge/global` and never touches imports;',
|
|
45
|
-
"'named' makes the autofix add",
|
|
46
|
-
`\`import { type ${NON_EMPTY_ARRAY_TYPE_NAME} } from '${TS_TYPE_FORGE_MODULE}';\``,
|
|
47
|
-
'when the name is not imported yet.',
|
|
48
|
-
].join(' '),
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
additionalProperties: false,
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
messages: {
|
|
55
|
-
useNonEmptyArray:
|
|
56
|
-
'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
|
|
60
|
-
create: (context) => {
|
|
61
|
-
const sourceCode = context.sourceCode;
|
|
62
|
-
|
|
63
|
-
const program = sourceCode.ast;
|
|
64
|
-
|
|
65
|
-
// `context.options` is what the user actually configured; `defaultOptions`
|
|
66
|
-
// is only merged in by `RuleCreator`, which plain rule objects do not use.
|
|
67
|
-
const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
68
|
-
|
|
69
|
-
const importedLocalName = getImportedLocalName(
|
|
70
|
-
getTsTypeForgeImports(program),
|
|
71
|
-
NON_EMPTY_ARRAY_TYPE_NAME,
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const mut_nodesToFix: {
|
|
75
|
-
node: TSESTree.TSTypeOperator;
|
|
76
|
-
elementText: string;
|
|
77
|
-
}[] = [];
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
TSTypeOperator: (node) => {
|
|
81
|
-
const elementText = getNonEmptyArrayElementText(node, sourceCode);
|
|
82
|
-
|
|
83
|
-
if (elementText === undefined) return;
|
|
84
|
-
|
|
85
|
-
mut_nodesToFix.push({ node, elementText });
|
|
86
|
-
},
|
|
87
|
-
'Program:exit': () => {
|
|
88
|
-
if (mut_nodesToFix.length === 0) return;
|
|
89
|
-
|
|
90
|
-
// Rewriting to `NonEmptyArray<...>` is only sound when that name is
|
|
91
|
-
// free (or already bound to the ts-type-forge export). Otherwise the
|
|
92
|
-
// rewritten type would resolve to the wrong declaration — most
|
|
93
|
-
// notably inside ts-type-forge's own sources, which define
|
|
94
|
-
// `NonEmptyArray`.
|
|
95
|
-
if (
|
|
96
|
-
importedLocalName === undefined &&
|
|
97
|
-
hasConflictingDeclaration(program, NON_EMPTY_ARRAY_TYPE_NAME)
|
|
98
|
-
) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const localName = importedLocalName ?? NON_EMPTY_ARRAY_TYPE_NAME;
|
|
103
|
-
|
|
104
|
-
const needsImport =
|
|
105
|
-
importStyle === 'named' && importedLocalName === undefined;
|
|
106
|
-
|
|
107
|
-
for (const [index, { node, elementText }] of mut_nodesToFix.entries()) {
|
|
108
|
-
const replacement = `${localName}<${elementText}>`;
|
|
109
|
-
|
|
110
|
-
context.report({
|
|
111
|
-
node,
|
|
112
|
-
messageId: 'useNonEmptyArray',
|
|
113
|
-
data: {
|
|
114
|
-
original: sourceCode.getText(node),
|
|
115
|
-
replacement,
|
|
116
|
-
},
|
|
117
|
-
fix: (fixer) => [
|
|
118
|
-
...(index === 0 && needsImport
|
|
119
|
-
? buildTypeImportFix(fixer, program, NON_EMPTY_ARRAY_TYPE_NAME)
|
|
120
|
-
: []),
|
|
121
|
-
fixer.replaceText(node, replacement),
|
|
122
|
-
],
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
},
|
|
128
|
-
defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],
|
|
129
|
-
} as const;
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import parser from '@typescript-eslint/parser';
|
|
2
|
-
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
-
import dedent from 'dedent';
|
|
4
|
-
import { preferNonEmptyArray } from './prefer-non-empty-array.mjs';
|
|
5
|
-
|
|
6
|
-
const tester = new RuleTester({
|
|
7
|
-
languageOptions: {
|
|
8
|
-
parser,
|
|
9
|
-
parserOptions: {
|
|
10
|
-
ecmaVersion: 2020,
|
|
11
|
-
sourceType: 'module',
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
describe('prefer-non-empty-array', () => {
|
|
17
|
-
tester.run('prefer-non-empty-array', preferNonEmptyArray, {
|
|
18
|
-
valid: [
|
|
19
|
-
{
|
|
20
|
-
name: 'ignores mutable tuples (that spelling is MutableNonEmptyArray)',
|
|
21
|
-
code: 'type T = [number, ...number[]];',
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'ignores plain readonly arrays',
|
|
25
|
-
code: 'type T = readonly number[];',
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
name: 'ignores tuples whose rest element type differs from the head',
|
|
29
|
-
code: 'type T = readonly [string, ...number[]];',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'ignores min-length-2 tuples',
|
|
33
|
-
code: 'type T = readonly [number, number, ...number[]];',
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: 'ignores fixed-length tuples',
|
|
37
|
-
code: 'type T = readonly [number];',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: 'ignores leading-rest tuples',
|
|
41
|
-
code: 'type T = readonly [...number[], number];',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: 'ignores optional heads',
|
|
45
|
-
code: 'type T = readonly [number?, ...number[]];',
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'ignores labelled tuple members (the labels would be lost)',
|
|
49
|
-
code: 'type T = readonly [head: number, ...tail: number[]];',
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: 'ignores readonly operators applied to non-tuples',
|
|
53
|
-
code: 'type T = readonly string[];',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
name: 'stays silent when NonEmptyArray is declared locally',
|
|
57
|
-
code: dedent`
|
|
58
|
-
type NonEmptyArray<A> = readonly [A, ...A[]];
|
|
59
|
-
type T = readonly [number, ...number[]];
|
|
60
|
-
`,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'stays silent when NonEmptyArray is exported locally',
|
|
64
|
-
code: 'export type NonEmptyArray<A> = readonly [A, ...A[]];',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'stays silent when NonEmptyArray is imported from elsewhere',
|
|
68
|
-
code: dedent`
|
|
69
|
-
import { type NonEmptyArray } from './my-types.mjs';
|
|
70
|
-
|
|
71
|
-
type T = readonly [number, ...number[]];
|
|
72
|
-
type U = NonEmptyArray<string>;
|
|
73
|
-
`,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
invalid: [
|
|
77
|
-
{
|
|
78
|
-
name: 'replaces readonly [V, ...V[]] with NonEmptyArray<V>',
|
|
79
|
-
code: 'type T = readonly [number, ...number[]];',
|
|
80
|
-
output: 'type T = NonEmptyArray<number>;',
|
|
81
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'replaces readonly [V, ...(readonly V[])] with NonEmptyArray<V>',
|
|
85
|
-
code: 'type T = readonly [number, ...(readonly number[])];',
|
|
86
|
-
output: 'type T = NonEmptyArray<number>;',
|
|
87
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
name: 'keeps a composite element type intact',
|
|
91
|
-
code: 'type T = readonly [string | number, ...(string | number)[]];',
|
|
92
|
-
output: 'type T = NonEmptyArray<string | number>;',
|
|
93
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'handles generic element types',
|
|
97
|
-
code: 'type T<A> = readonly [A, ...A[]];',
|
|
98
|
-
output: 'type T<A> = NonEmptyArray<A>;',
|
|
99
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'rewrites occurrences nested inside another tuple',
|
|
103
|
-
code: 'type T = readonly [readonly [number, ...number[]], ...string[]];',
|
|
104
|
-
output: 'type T = readonly [NonEmptyArray<number>, ...string[]];',
|
|
105
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
name: 'rewrites parameter and return type positions',
|
|
109
|
-
code: dedent`
|
|
110
|
-
declare const f: (xs: readonly [number, ...number[]]) => readonly [string, ...string[]];
|
|
111
|
-
`,
|
|
112
|
-
output: dedent`
|
|
113
|
-
declare const f: (xs: NonEmptyArray<number>) => NonEmptyArray<string>;
|
|
114
|
-
`,
|
|
115
|
-
errors: [
|
|
116
|
-
{ messageId: 'useNonEmptyArray' },
|
|
117
|
-
{ messageId: 'useNonEmptyArray' },
|
|
118
|
-
],
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'reuses the local alias when NonEmptyArray is imported aliased',
|
|
122
|
-
code: dedent`
|
|
123
|
-
import { type NonEmptyArray as NEA } from 'ts-type-forge';
|
|
124
|
-
|
|
125
|
-
type T = readonly [number, ...number[]];
|
|
126
|
-
`,
|
|
127
|
-
output: dedent`
|
|
128
|
-
import { type NonEmptyArray as NEA } from 'ts-type-forge';
|
|
129
|
-
|
|
130
|
-
type T = NEA<number>;
|
|
131
|
-
`,
|
|
132
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: 'does not add an import with the default (global) import style',
|
|
136
|
-
code: dedent`
|
|
137
|
-
import { type StrictOmit } from 'ts-type-forge';
|
|
138
|
-
|
|
139
|
-
type T = readonly [number, ...number[]];
|
|
140
|
-
`,
|
|
141
|
-
output: dedent`
|
|
142
|
-
import { type StrictOmit } from 'ts-type-forge';
|
|
143
|
-
|
|
144
|
-
type T = NonEmptyArray<number>;
|
|
145
|
-
`,
|
|
146
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
147
|
-
},
|
|
148
|
-
{
|
|
149
|
-
name: 'adds the named import when importStyle is "named"',
|
|
150
|
-
options: [{ importStyle: 'named' }],
|
|
151
|
-
code: 'type T = readonly [number, ...number[]];',
|
|
152
|
-
output: dedent`
|
|
153
|
-
import { type NonEmptyArray } from 'ts-type-forge';
|
|
154
|
-
type T = NonEmptyArray<number>;
|
|
155
|
-
`,
|
|
156
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
157
|
-
},
|
|
158
|
-
{
|
|
159
|
-
name: 'adds the named import only once per file',
|
|
160
|
-
options: [{ importStyle: 'named' }],
|
|
161
|
-
code: dedent`
|
|
162
|
-
type T = readonly [number, ...number[]];
|
|
163
|
-
type U = readonly [string, ...string[]];
|
|
164
|
-
`,
|
|
165
|
-
output: dedent`
|
|
166
|
-
import { type NonEmptyArray } from 'ts-type-forge';
|
|
167
|
-
type T = NonEmptyArray<number>;
|
|
168
|
-
type U = NonEmptyArray<string>;
|
|
169
|
-
`,
|
|
170
|
-
errors: [
|
|
171
|
-
{ messageId: 'useNonEmptyArray' },
|
|
172
|
-
{ messageId: 'useNonEmptyArray' },
|
|
173
|
-
],
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: 'keeps an existing NonEmptyArray import untouched',
|
|
177
|
-
options: [{ importStyle: 'named' }],
|
|
178
|
-
code: dedent`
|
|
179
|
-
import { type NonEmptyArray } from 'ts-type-forge';
|
|
180
|
-
|
|
181
|
-
type T = readonly [number, ...number[]];
|
|
182
|
-
`,
|
|
183
|
-
output: dedent`
|
|
184
|
-
import { type NonEmptyArray } from 'ts-type-forge';
|
|
185
|
-
|
|
186
|
-
type T = NonEmptyArray<number>;
|
|
187
|
-
`,
|
|
188
|
-
errors: [{ messageId: 'useNonEmptyArray' }],
|
|
189
|
-
},
|
|
190
|
-
],
|
|
191
|
-
});
|
|
192
|
-
});
|