eslint-plugin-ts-type-forge 0.4.3 → 0.5.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/dist/rule-types.d.mts +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/tuple-rule-utils.d.mts.map +1 -1
- package/dist/rules/tuple-rule-utils.mjs +9 -8
- package/dist/rules/tuple-rule-utils.mjs.map +1 -1
- package/package.json +3 -2
- package/src/rule-types.mts +1 -1
- package/src/rules/ast-utils.mts +32 -3
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +84 -15
- package/src/rules/tuple-rule-utils.mts +15 -13
package/dist/rule-types.d.mts
CHANGED
|
@@ -35,7 +35,7 @@ declare namespace PreferCanonicalLengthConstrainedTuple {
|
|
|
35
35
|
* "global",
|
|
36
36
|
* "named"
|
|
37
37
|
* ],
|
|
38
|
-
* "description": "How the ts-type-forge type is brought into scope. '
|
|
38
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
39
39
|
* },
|
|
40
40
|
* "maxLength": {
|
|
41
41
|
* "type": "integer",
|
|
@@ -27,6 +27,8 @@ export type UniformTupleShape = Readonly<{
|
|
|
27
27
|
* rewriting them would drop information.
|
|
28
28
|
* - A tuple that is only a rest element (`[...V[]]`) is not a length
|
|
29
29
|
* constraint at all.
|
|
30
|
+
* - A tuple inside the `extends` clause of a conditional type is a match
|
|
31
|
+
* pattern, not a type (see {@link isInConditionalTypeExtendsClause}).
|
|
30
32
|
*/
|
|
31
33
|
export declare const analyzeUniformTuple: (tuple: TSESTree.TSTupleType, sourceCode: TSESLint.SourceCode) => UniformTupleShape | undefined;
|
|
32
34
|
//# sourceMappingURL=ast-utils.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,+EAA+E;IAC/E,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,UACvB,QAAQ,CAAC,WAAW,cACf,QAAQ,CAAC,UAAU,KAC9B,iBAAiB,GAAG,SA4DtB,CAAC"}
|
package/dist/rules/ast-utils.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
2
3
|
|
|
3
4
|
const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
5
|
+
if (isInConditionalTypeExtendsClause(tuple)) return void 0;
|
|
4
6
|
const { parent } = tuple;
|
|
5
7
|
const isReadonly = parent.type === AST_NODE_TYPES.TSTypeOperator && parent.operator === "readonly";
|
|
6
8
|
const elements = tuple.elementTypes;
|
|
7
9
|
const last = elements.at(-1);
|
|
8
10
|
const hasRest = last?.type === AST_NODE_TYPES.TSRestType;
|
|
9
|
-
const leading = hasRest ?
|
|
10
|
-
if (leading
|
|
11
|
+
const leading = hasRest ? Arr.butLast(elements) : elements;
|
|
12
|
+
if (Arr.isEmpty(leading) || leading.some(
|
|
11
13
|
(element) => element.type === AST_NODE_TYPES.TSRestType || element.type === AST_NODE_TYPES.TSOptionalType || element.type === AST_NODE_TYPES.TSNamedTupleMember
|
|
12
14
|
)) {
|
|
13
15
|
return void 0;
|
|
@@ -19,7 +21,7 @@ const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
|
19
21
|
const elementText = sourceCode.getText(head);
|
|
20
22
|
const expected = normalizeWhitespace(elementText);
|
|
21
23
|
const others = [
|
|
22
|
-
...
|
|
24
|
+
...Arr.tail(leading),
|
|
23
25
|
...restElementType === void 0 ? [] : [restElementType]
|
|
24
26
|
];
|
|
25
27
|
return others.every(
|
|
@@ -32,6 +34,11 @@ const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
|
32
34
|
hasRest
|
|
33
35
|
} : void 0;
|
|
34
36
|
};
|
|
37
|
+
const isInConditionalTypeExtendsClause = (node) => {
|
|
38
|
+
if (node.type === AST_NODE_TYPES.Program) return false;
|
|
39
|
+
const { parent } = node;
|
|
40
|
+
return parent.type === AST_NODE_TYPES.TSConditionalType && parent.extendsType === node || isInConditionalTypeExtendsClause(parent);
|
|
41
|
+
};
|
|
35
42
|
const getArrayElementType = (node) => {
|
|
36
43
|
if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;
|
|
37
44
|
return node.type === AST_NODE_TYPES.TSTypeOperator && node.operator === "readonly" && node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType ? node.typeAnnotation.elementType : void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ?
|
|
1
|
+
{"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n * - A tuple inside the `extends` clause of a conditional type is a match\n * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n if (isInConditionalTypeExtendsClause(tuple)) return undefined;\n\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ? Arr.butLast(elements) : elements;\n\n if (\n Arr.isEmpty(leading) ||\n leading.some(\n (element) =>\n element.type === AST_NODE_TYPES.TSRestType ||\n element.type === AST_NODE_TYPES.TSOptionalType ||\n element.type === AST_NODE_TYPES.TSNamedTupleMember,\n )\n ) {\n return undefined;\n }\n\n const restElementType =\n last !== undefined && hasRest\n ? getArrayElementType(last.typeAnnotation)\n : undefined;\n\n if (hasRest && restElementType === undefined) return undefined;\n\n const [head] = leading;\n\n if (head === undefined) return undefined;\n\n const elementText = sourceCode.getText(head);\n\n const expected = normalizeWhitespace(elementText);\n\n const others = [\n ...Arr.tail(leading),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n return others.every(\n (element) => normalizeWhitespace(sourceCode.getText(element)) === expected,\n )\n ? {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n }\n : undefined;\n};\n\n/**\n * Whether `node` sits inside the `extends` clause of a conditional type, where\n * a tuple is a *match pattern* rather than a type.\n *\n * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are\n * still generic. The canonical spellings resolve through a mapped type\n * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and\n * against one of those the checker defers the whole conditional instead —\n * yielding `boolean` where the tuple pattern yielded `true` or `false`, which\n * silently widens everything downstream. So those positions are left alone.\n */\nconst isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {\n // The root has no parent, so it terminates the walk.\n if (node.type === AST_NODE_TYPES.Program) return false;\n\n const { parent } = node;\n\n return (\n (parent.type === AST_NODE_TYPES.TSConditionalType &&\n parent.extendsType === node) ||\n isInConditionalTypeExtendsClause(parent)\n );\n};\n\n/** Unwraps `V[]` and `readonly V[]` to `V`. */\nconst getArrayElementType = (\n node: TSESTree.TypeNode,\n): TSESTree.TypeNode | undefined => {\n if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;\n\n return node.type === AST_NODE_TYPES.TSTypeOperator &&\n node.operator === 'readonly' &&\n node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType\n ? node.typeAnnotation.elementType\n : undefined;\n};\n\nconst normalizeWhitespace = (text: string): string =>\n text.replaceAll(/\\s+/gu, ' ').trim();\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tuple-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/tuple-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"tuple-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/tuple-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAYxE,mEAAmE;AACnE,KAAK,UAAU,GAAG,OAAO,CACvB,QAAQ,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,EACjE,SAAS,OAAO,EAAE,CACnB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE7C,eAAO,MAAM,oBAAoB,EAAE,WAAqB,CAAC;AAEzD,eAAO,MAAM,4BAA4B,EAAE,UAW1C,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,YACjB,QAAQ,CAAC,OAAO,iBACV,MAAM,eACR,WAAW,KACvB,QAAQ,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,SAa1D,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,SAAS,MAAM,WAClD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,YACnD,SAAS,YAAY,EAAE,aACtB,UAAU,eACR,WAAW,KACvB,IAuCF,CAAC"}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
import { Arr } from 'ts-data-forge';
|
|
1
2
|
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
2
3
|
import { buildTypeImportFix, getImportedLocalName, getTsTypeForgeImports, hasConflictingDeclaration } from './import-utils.mjs';
|
|
3
4
|
|
|
4
|
-
const DEFAULT_IMPORT_STYLE = "
|
|
5
|
+
const DEFAULT_IMPORT_STYLE = "named";
|
|
5
6
|
const IMPORT_STYLE_SCHEMA_PROPERTY = {
|
|
6
7
|
type: "string",
|
|
7
8
|
enum: ["global", "named"],
|
|
8
9
|
description: [
|
|
9
10
|
"How the ts-type-forge type is brought into scope.",
|
|
10
|
-
"'
|
|
11
|
-
"`ts-type-forge/global` and never touches imports;",
|
|
12
|
-
"'named' makes the autofix add the corresponding",
|
|
11
|
+
"'named' (default) makes the autofix add the corresponding",
|
|
13
12
|
`\`import { type \u2026 } from '${TS_TYPE_FORGE_MODULE}';\``,
|
|
14
|
-
"when the name is not imported yet
|
|
13
|
+
"when the name is not imported yet; 'global' assumes the",
|
|
14
|
+
"ambient globals of `ts-type-forge/global` and never touches",
|
|
15
|
+
"imports."
|
|
15
16
|
].join(" ")
|
|
16
17
|
};
|
|
17
18
|
const resolveTypeName = (program, canonicalName, importStyle) => {
|
|
@@ -42,12 +43,12 @@ const reportTupleRewrites = (context, rewrites, messageId, importStyle) => {
|
|
|
42
43
|
node: rewrite.node,
|
|
43
44
|
messageId,
|
|
44
45
|
data: { original: sourceCode.getText(rewrite.node), replacement },
|
|
45
|
-
fix: (fixer) =>
|
|
46
|
-
|
|
46
|
+
fix: (fixer) => Arr.toPushed(
|
|
47
|
+
index === 0 ? importNames.flatMap(
|
|
47
48
|
(name) => buildTypeImportFix(fixer, program, name)
|
|
48
49
|
) : [],
|
|
49
50
|
fixer.replaceText(rewrite.node, replacement)
|
|
50
|
-
|
|
51
|
+
)
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tuple-rule-utils.mjs","sources":["../../src/rules/tuple-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\nimport {\n buildTypeImportFix,\n getImportedLocalName,\n getTsTypeForgeImports,\n hasConflictingDeclaration,\n} from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** `JSONSchema4`, reached through the public rule-module types. */\ntype JsonSchema = Exclude<\n TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],\n readonly unknown[]\n>;\n\nexport type ImportStyle = 'global' | 'named';\n\nexport const DEFAULT_IMPORT_STYLE: ImportStyle = '
|
|
1
|
+
{"version":3,"file":"tuple-rule-utils.mjs","sources":["../../src/rules/tuple-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { TS_TYPE_FORGE_MODULE } from './constants.mjs';\nimport {\n buildTypeImportFix,\n getImportedLocalName,\n getTsTypeForgeImports,\n hasConflictingDeclaration,\n} from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** `JSONSchema4`, reached through the public rule-module types. */\ntype JsonSchema = Exclude<\n TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'],\n readonly unknown[]\n>;\n\nexport type ImportStyle = 'global' | 'named';\n\nexport const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';\n\nexport const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {\n type: 'string',\n enum: ['global', 'named'],\n description: [\n 'How the ts-type-forge type is brought into scope.',\n \"'named' (default) makes the autofix add the corresponding\",\n `\\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\\``,\n \"when the name is not imported yet; 'global' assumes the\",\n 'ambient globals of `ts-type-forge/global` and never touches',\n 'imports.',\n ].join(' '),\n};\n\n/**\n * Resolves how a ts-type-forge type name should be written in this file, and\n * whether the autofix has to add an import for it.\n *\n * Returns `undefined` when the name is already bound to something else (a local\n * alias, or an import from another module) — rewriting to it would then silently\n * resolve to the wrong declaration, which is exactly what happens inside\n * ts-type-forge's own sources.\n */\nexport const resolveTypeName = (\n program: TSESTree.Program,\n canonicalName: string,\n importStyle: ImportStyle,\n): Readonly<{ localName: string; needsImport: boolean }> | undefined => {\n const importedLocalName = getImportedLocalName(\n getTsTypeForgeImports(program),\n canonicalName,\n );\n\n if (importedLocalName !== undefined) {\n return { localName: importedLocalName, needsImport: false };\n }\n\n return hasConflictingDeclaration(program, canonicalName)\n ? undefined\n : { localName: canonicalName, needsImport: importStyle === 'named' };\n};\n\nexport type TupleRewrite = Readonly<{\n node: TSESTree.Node;\n canonicalName: string;\n /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */\n typeArgs: string;\n}>;\n\n/**\n * Emits one report per rewrite, resolving each target name and attaching every\n * required import to the *first* report.\n *\n * All import fixes must live on a single report: `insertTextBefore(program, …)`\n * produces overlapping ranges, and ESLint merges each report's fixes into one\n * span — so spreading them across reports makes the fixes conflict and only one\n * survives per pass.\n */\nexport const reportTupleRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TupleRewrite[],\n messageId: MessageIds,\n importStyle: ImportStyle,\n): void => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const resolved = rewrites.flatMap((rewrite) => {\n const target = resolveTypeName(program, rewrite.canonicalName, importStyle);\n\n // The name is already bound to something else in this file, so the rewrite\n // would resolve to the wrong declaration.\n return target === undefined ? [] : [{ rewrite, target }];\n });\n\n const importNames = Array.from(\n new Set(\n resolved\n .filter(({ target }) => target.needsImport)\n .map(({ rewrite }) => rewrite.canonicalName),\n ),\n );\n\n for (const [index, { rewrite, target }] of resolved.entries()) {\n const replacement = `${target.localName}<${rewrite.typeArgs}>`;\n\n context.report({\n node: rewrite.node,\n messageId,\n data: { original: sourceCode.getText(rewrite.node), replacement },\n fix: (fixer) =>\n Arr.toPushed(\n index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : [],\n fixer.replaceText(rewrite.node, replacement),\n ),\n });\n }\n};\n"],"names":[],"mappings":";;;;AAoBO,MAAM,oBAAA,GAAoC;AAE1C,MAAM,4BAAA,GAA2C;AAAA,EACtD,IAAA,EAAM,QAAA;AAAA,EACN,IAAA,EAAM,CAAC,QAAA,EAAU,OAAO,CAAA;AAAA,EACxB,WAAA,EAAa;AAAA,IACX,mDAAA;AAAA,IACA,2DAAA;AAAA,IACA,kCAA6B,oBAAoB,CAAA,IAAA,CAAA;AAAA,IACjD,yDAAA;AAAA,IACA,6DAAA;AAAA,IACA;AAAA,GACF,CAAE,KAAK,GAAG;AACZ;AAWO,MAAM,eAAA,GAAkB,CAC7B,OAAA,EACA,aAAA,EACA,WAAA,KACsE;AACtE,EAAA,MAAM,iBAAA,GAAoB,oBAAA;AAAA,IACxB,sBAAsB,OAAO,CAAA;AAAA,IAC7B;AAAA,GACF;AAEA,EAAA,IAAI,sBAAsB,MAAA,EAAW;AACnC,IAAA,OAAO,EAAE,SAAA,EAAW,iBAAA,EAAmB,WAAA,EAAa,KAAA,EAAM;AAAA,EAC5D;AAEA,EAAA,OAAO,yBAAA,CAA0B,OAAA,EAAS,aAAa,CAAA,GACnD,MAAA,GACA,EAAE,SAAA,EAAW,aAAA,EAAe,WAAA,EAAa,WAAA,KAAgB,OAAA,EAAQ;AACvE;AAkBO,MAAM,mBAAA,GAAsB,CACjC,OAAA,EACA,QAAA,EACA,WACA,WAAA,KACS;AACT,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,EAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC7C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,eAAe,WAAW,CAAA;AAI1E,IAAA,OAAO,MAAA,KAAW,SAAY,EAAC,GAAI,CAAC,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA;AAAA,IACxB,IAAI,GAAA;AAAA,MACF,QAAA,CACG,MAAA,CAAO,CAAC,EAAE,QAAO,KAAM,MAAA,CAAO,WAAW,CAAA,CACzC,IAAI,CAAC,EAAE,OAAA,EAAQ,KAAM,QAAQ,aAAa;AAAA;AAC/C,GACF;AAEA,EAAA,KAAA,MAAW,CAAC,OAAO,EAAE,OAAA,EAAS,QAAQ,CAAA,IAAK,QAAA,CAAS,OAAA,EAAQ,EAAG;AAC7D,IAAA,MAAM,cAAc,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,CAAA,EAAI,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAE3D,IAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,MACb,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA;AAAA,MACA,IAAA,EAAM,EAAE,QAAA,EAAU,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAI,GAAG,WAAA,EAAY;AAAA,MAChE,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,QACF,KAAA,KAAU,IACN,WAAA,CAAY,OAAA;AAAA,UAAQ,CAAC,IAAA,KACnB,kBAAA,CAAmB,KAAA,EAAO,SAAS,IAAI;AAAA,YAEzC,EAAC;AAAA,QACL,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,WAAW;AAAA;AAC7C,KACH,CAAA;AAAA,EACH;AACF;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ts-type-forge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer type declarations toward ts-type-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"LICENSE"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@typescript-eslint/utils": "8.65.0",
|
|
38
|
+
"@typescript-eslint/utils": "~8.65.0",
|
|
39
|
+
"ts-data-forge": "~14.0.0",
|
|
39
40
|
"ts-type-forge": "9.1.1"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
package/src/rule-types.mts
CHANGED
|
@@ -53,7 +53,7 @@ namespace PreferCanonicalLengthConstrainedTuple {
|
|
|
53
53
|
* "global",
|
|
54
54
|
* "named"
|
|
55
55
|
* ],
|
|
56
|
-
* "description": "How the ts-type-forge type is brought into scope. '
|
|
56
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
57
57
|
* },
|
|
58
58
|
* "maxLength": {
|
|
59
59
|
* "type": "integer",
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import { Arr } from 'ts-data-forge';
|
|
6
7
|
|
|
7
8
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
8
9
|
|
|
@@ -35,11 +36,15 @@ export type UniformTupleShape = Readonly<{
|
|
|
35
36
|
* rewriting them would drop information.
|
|
36
37
|
* - A tuple that is only a rest element (`[...V[]]`) is not a length
|
|
37
38
|
* constraint at all.
|
|
39
|
+
* - A tuple inside the `extends` clause of a conditional type is a match
|
|
40
|
+
* pattern, not a type (see {@link isInConditionalTypeExtendsClause}).
|
|
38
41
|
*/
|
|
39
42
|
export const analyzeUniformTuple = (
|
|
40
43
|
tuple: TSESTree.TSTupleType,
|
|
41
44
|
sourceCode: TSESLint.SourceCode,
|
|
42
45
|
): UniformTupleShape | undefined => {
|
|
46
|
+
if (isInConditionalTypeExtendsClause(tuple)) return undefined;
|
|
47
|
+
|
|
43
48
|
const { parent } = tuple;
|
|
44
49
|
|
|
45
50
|
const isReadonly =
|
|
@@ -52,10 +57,10 @@ export const analyzeUniformTuple = (
|
|
|
52
57
|
|
|
53
58
|
const hasRest = last?.type === AST_NODE_TYPES.TSRestType;
|
|
54
59
|
|
|
55
|
-
const leading = hasRest ?
|
|
60
|
+
const leading = hasRest ? Arr.butLast(elements) : elements;
|
|
56
61
|
|
|
57
62
|
if (
|
|
58
|
-
leading
|
|
63
|
+
Arr.isEmpty(leading) ||
|
|
59
64
|
leading.some(
|
|
60
65
|
(element) =>
|
|
61
66
|
element.type === AST_NODE_TYPES.TSRestType ||
|
|
@@ -82,7 +87,7 @@ export const analyzeUniformTuple = (
|
|
|
82
87
|
const expected = normalizeWhitespace(elementText);
|
|
83
88
|
|
|
84
89
|
const others = [
|
|
85
|
-
...
|
|
90
|
+
...Arr.tail(leading),
|
|
86
91
|
...(restElementType === undefined ? [] : [restElementType]),
|
|
87
92
|
];
|
|
88
93
|
|
|
@@ -99,6 +104,30 @@ export const analyzeUniformTuple = (
|
|
|
99
104
|
: undefined;
|
|
100
105
|
};
|
|
101
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Whether `node` sits inside the `extends` clause of a conditional type, where
|
|
109
|
+
* a tuple is a *match pattern* rather than a type.
|
|
110
|
+
*
|
|
111
|
+
* `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are
|
|
112
|
+
* still generic. The canonical spellings resolve through a mapped type
|
|
113
|
+
* (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and
|
|
114
|
+
* against one of those the checker defers the whole conditional instead —
|
|
115
|
+
* yielding `boolean` where the tuple pattern yielded `true` or `false`, which
|
|
116
|
+
* silently widens everything downstream. So those positions are left alone.
|
|
117
|
+
*/
|
|
118
|
+
const isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {
|
|
119
|
+
// The root has no parent, so it terminates the walk.
|
|
120
|
+
if (node.type === AST_NODE_TYPES.Program) return false;
|
|
121
|
+
|
|
122
|
+
const { parent } = node;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
(parent.type === AST_NODE_TYPES.TSConditionalType &&
|
|
126
|
+
parent.extendsType === node) ||
|
|
127
|
+
isInConditionalTypeExtendsClause(parent)
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
102
131
|
/** Unwraps `V[]` and `readonly V[]` to `V`. */
|
|
103
132
|
const getArrayElementType = (
|
|
104
133
|
node: TSESTree.TypeNode,
|
|
@@ -45,6 +45,21 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
45
45
|
name: 'ignores plain arrays',
|
|
46
46
|
code: 'type T = readonly string[];',
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'ignores tuple patterns in a conditional type `extends` clause',
|
|
50
|
+
code: dedent`
|
|
51
|
+
type And<A extends boolean, B extends boolean> = [A, B] extends [
|
|
52
|
+
true,
|
|
53
|
+
true,
|
|
54
|
+
]
|
|
55
|
+
? true
|
|
56
|
+
: false;
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'ignores tuple patterns nested in an `extends` clause',
|
|
61
|
+
code: 'type T<A> = A extends readonly (readonly [string, string])[] ? 1 : 2;',
|
|
62
|
+
},
|
|
48
63
|
{
|
|
49
64
|
name: 'stays silent when the target name is bound locally',
|
|
50
65
|
code: dedent`
|
|
@@ -54,75 +69,116 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
54
69
|
},
|
|
55
70
|
],
|
|
56
71
|
invalid: [
|
|
72
|
+
{
|
|
73
|
+
name: 'still rewrites the branches of a conditional type',
|
|
74
|
+
code: 'type T<A> = A extends 1 ? [true, true] : 2;',
|
|
75
|
+
output: dedent`
|
|
76
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
77
|
+
type T<A> = A extends 1 ? MutableFixedLengthTuple<2, true> : 2;
|
|
78
|
+
`,
|
|
79
|
+
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
80
|
+
},
|
|
57
81
|
{
|
|
58
82
|
name: 'readonly non-empty tuple',
|
|
59
83
|
code: 'type T = readonly [string, ...string[]];',
|
|
60
|
-
output:
|
|
84
|
+
output: dedent`
|
|
85
|
+
import { type NonEmptyTuple } from 'ts-type-forge';
|
|
86
|
+
type T = NonEmptyTuple<string>;
|
|
87
|
+
`,
|
|
61
88
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
62
89
|
},
|
|
63
90
|
{
|
|
64
91
|
name: 'mutable non-empty tuple',
|
|
65
92
|
code: 'type T = [string, ...string[]];',
|
|
66
|
-
output:
|
|
93
|
+
output: dedent`
|
|
94
|
+
import { type MutableNonEmptyTuple } from 'ts-type-forge';
|
|
95
|
+
type T = MutableNonEmptyTuple<string>;
|
|
96
|
+
`,
|
|
67
97
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
68
98
|
},
|
|
69
99
|
{
|
|
70
100
|
name: 'readonly non-empty tuple with a readonly rest spelling',
|
|
71
101
|
code: 'type T = readonly [string, ...(readonly string[])];',
|
|
72
|
-
output:
|
|
102
|
+
output: dedent`
|
|
103
|
+
import { type NonEmptyTuple } from 'ts-type-forge';
|
|
104
|
+
type T = NonEmptyTuple<string>;
|
|
105
|
+
`,
|
|
73
106
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
74
107
|
},
|
|
75
108
|
{
|
|
76
109
|
name: 'readonly fixed-length tuple',
|
|
77
110
|
code: 'type T = readonly [string, string, string];',
|
|
78
|
-
output:
|
|
111
|
+
output: dedent`
|
|
112
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
113
|
+
type T = FixedLengthTuple<3, string>;
|
|
114
|
+
`,
|
|
79
115
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
80
116
|
},
|
|
81
117
|
{
|
|
82
118
|
name: 'mutable fixed-length tuple',
|
|
83
119
|
code: 'type T = [string, string, string];',
|
|
84
|
-
output:
|
|
120
|
+
output: dedent`
|
|
121
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
122
|
+
type T = MutableFixedLengthTuple<3, string>;
|
|
123
|
+
`,
|
|
85
124
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
86
125
|
},
|
|
87
126
|
{
|
|
88
127
|
name: 'readonly min-length tuple',
|
|
89
128
|
code: 'type T = readonly [number, number, ...number[]];',
|
|
90
|
-
output:
|
|
129
|
+
output: dedent`
|
|
130
|
+
import { type MinLengthTuple } from 'ts-type-forge';
|
|
131
|
+
type T = MinLengthTuple<2, number>;
|
|
132
|
+
`,
|
|
91
133
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
92
134
|
},
|
|
93
135
|
{
|
|
94
136
|
name: 'mutable min-length tuple',
|
|
95
137
|
code: 'type T = [number, number, ...number[]];',
|
|
96
|
-
output:
|
|
138
|
+
output: dedent`
|
|
139
|
+
import { type MutableMinLengthTuple } from 'ts-type-forge';
|
|
140
|
+
type T = MutableMinLengthTuple<2, number>;
|
|
141
|
+
`,
|
|
97
142
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
98
143
|
},
|
|
99
144
|
{
|
|
100
145
|
name: 'readonly rest spelling inside a readonly tuple',
|
|
101
146
|
code: 'type T = readonly [number, number, ...(readonly number[])];',
|
|
102
|
-
output:
|
|
147
|
+
output: dedent`
|
|
148
|
+
import { type MinLengthTuple } from 'ts-type-forge';
|
|
149
|
+
type T = MinLengthTuple<2, number>;
|
|
150
|
+
`,
|
|
103
151
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
104
152
|
},
|
|
105
153
|
{
|
|
106
154
|
name: 'handles the default maximum length of 10',
|
|
107
155
|
code: 'type T = readonly [1, 1, 1, 1, 1, 1, 1, 1, 1, 1];',
|
|
108
|
-
output:
|
|
156
|
+
output: dedent`
|
|
157
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
158
|
+
type T = FixedLengthTuple<10, 1>;
|
|
159
|
+
`,
|
|
109
160
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
110
161
|
},
|
|
111
162
|
{
|
|
112
163
|
name: 'keeps a composite element type intact',
|
|
113
164
|
code: 'type T = readonly [string | number, string | number];',
|
|
114
|
-
output:
|
|
165
|
+
output: dedent`
|
|
166
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
167
|
+
type T = FixedLengthTuple<2, string | number>;
|
|
168
|
+
`,
|
|
115
169
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
116
170
|
},
|
|
117
171
|
{
|
|
118
172
|
name: 'handles generic element types',
|
|
119
173
|
code: 'type T<V> = [V, V, ...V[]];',
|
|
120
|
-
output:
|
|
174
|
+
output: dedent`
|
|
175
|
+
import { type MutableMinLengthTuple } from 'ts-type-forge';
|
|
176
|
+
type T<V> = MutableMinLengthTuple<2, V>;
|
|
177
|
+
`,
|
|
121
178
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
122
179
|
},
|
|
123
180
|
{
|
|
124
181
|
name: 'adds named imports once per target type',
|
|
125
|
-
options: [{ importStyle: 'named' }],
|
|
126
182
|
code: dedent`
|
|
127
183
|
type A = readonly [string, string];
|
|
128
184
|
type B = readonly [number, number, number];
|
|
@@ -141,6 +197,13 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
141
197
|
{ messageId: 'useCanonicalTuple' },
|
|
142
198
|
],
|
|
143
199
|
},
|
|
200
|
+
{
|
|
201
|
+
name: 'adds no import under importStyle: global',
|
|
202
|
+
options: [{ importStyle: 'global' }],
|
|
203
|
+
code: 'type T = readonly [string, string];',
|
|
204
|
+
output: 'type T = FixedLengthTuple<2, string>;',
|
|
205
|
+
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
206
|
+
},
|
|
144
207
|
{
|
|
145
208
|
name: 'reuses an aliased import',
|
|
146
209
|
code: dedent`
|
|
@@ -158,8 +221,11 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
158
221
|
{
|
|
159
222
|
name: 'rewrites parameter and return type positions',
|
|
160
223
|
code: 'declare const f: (xs: readonly [number, number]) => [string, string];',
|
|
161
|
-
output:
|
|
162
|
-
|
|
224
|
+
output: dedent`
|
|
225
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
226
|
+
import { type MutableFixedLengthTuple } from 'ts-type-forge';
|
|
227
|
+
declare const f: (xs: FixedLengthTuple<2, number>) => MutableFixedLengthTuple<2, string>;
|
|
228
|
+
`,
|
|
163
229
|
errors: [
|
|
164
230
|
{ messageId: 'useCanonicalTuple' },
|
|
165
231
|
{ messageId: 'useCanonicalTuple' },
|
|
@@ -169,7 +235,10 @@ describe('prefer-canonical-length-constrained-tuple', () => {
|
|
|
169
235
|
name: 'respects a raised maxLength',
|
|
170
236
|
options: [{ maxLength: 12 }],
|
|
171
237
|
code: 'type T = readonly [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];',
|
|
172
|
-
output:
|
|
238
|
+
output: dedent`
|
|
239
|
+
import { type FixedLengthTuple } from 'ts-type-forge';
|
|
240
|
+
type T = FixedLengthTuple<12, 0>;
|
|
241
|
+
`,
|
|
173
242
|
errors: [{ messageId: 'useCanonicalTuple' }],
|
|
174
243
|
},
|
|
175
244
|
],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
2
3
|
import { TS_TYPE_FORGE_MODULE } from './constants.mjs';
|
|
3
4
|
import {
|
|
4
5
|
buildTypeImportFix,
|
|
@@ -17,18 +18,18 @@ type JsonSchema = Exclude<
|
|
|
17
18
|
|
|
18
19
|
export type ImportStyle = 'global' | 'named';
|
|
19
20
|
|
|
20
|
-
export const DEFAULT_IMPORT_STYLE: ImportStyle = '
|
|
21
|
+
export const DEFAULT_IMPORT_STYLE: ImportStyle = 'named';
|
|
21
22
|
|
|
22
23
|
export const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema = {
|
|
23
24
|
type: 'string',
|
|
24
25
|
enum: ['global', 'named'],
|
|
25
26
|
description: [
|
|
26
27
|
'How the ts-type-forge type is brought into scope.',
|
|
27
|
-
"'
|
|
28
|
-
'`ts-type-forge/global` and never touches imports;',
|
|
29
|
-
"'named' makes the autofix add the corresponding",
|
|
28
|
+
"'named' (default) makes the autofix add the corresponding",
|
|
30
29
|
`\`import { type … } from '${TS_TYPE_FORGE_MODULE}';\``,
|
|
31
|
-
|
|
30
|
+
"when the name is not imported yet; 'global' assumes the",
|
|
31
|
+
'ambient globals of `ts-type-forge/global` and never touches',
|
|
32
|
+
'imports.',
|
|
32
33
|
].join(' '),
|
|
33
34
|
};
|
|
34
35
|
|
|
@@ -109,14 +110,15 @@ export const reportTupleRewrites = <MessageIds extends string>(
|
|
|
109
110
|
node: rewrite.node,
|
|
110
111
|
messageId,
|
|
111
112
|
data: { original: sourceCode.getText(rewrite.node), replacement },
|
|
112
|
-
fix: (fixer) =>
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
fix: (fixer) =>
|
|
114
|
+
Arr.toPushed(
|
|
115
|
+
index === 0
|
|
116
|
+
? importNames.flatMap((name) =>
|
|
117
|
+
buildTypeImportFix(fixer, program, name),
|
|
118
|
+
)
|
|
119
|
+
: [],
|
|
120
|
+
fixer.replaceText(rewrite.node, replacement),
|
|
121
|
+
),
|
|
120
122
|
});
|
|
121
123
|
}
|
|
122
124
|
};
|