eslint-plugin-ts-type-forge 0.2.0 → 0.3.1
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/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# eslint-plugin-ts-type-forge
|
|
2
2
|
|
|
3
3
|
ESLint rules that steer TypeScript **type declarations** toward
|
|
4
|
-
[`ts-type-forge`](https://www.npmjs.com/package/ts-type-forge) idioms —
|
|
5
|
-
|
|
6
|
-
tuple
|
|
4
|
+
[`ts-type-forge`](https://www.npmjs.com/package/ts-type-forge) idioms — replacing
|
|
5
|
+
hand-rolled uniform tuple spellings with the library's named length-constrained
|
|
6
|
+
tuple types. Every rule is auto-fixable, and every rewrite is **type-preserving**.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -27,7 +27,7 @@ export default [
|
|
|
27
27
|
{
|
|
28
28
|
plugins: { 'ts-type-forge': eslintPluginTsTypeForge },
|
|
29
29
|
rules: {
|
|
30
|
-
'ts-type-forge/prefer-
|
|
30
|
+
'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',
|
|
31
31
|
} satisfies Partial<EslintTsTypeForgeRules>,
|
|
32
32
|
},
|
|
33
33
|
];
|
|
@@ -35,58 +35,80 @@ export default [
|
|
|
35
35
|
|
|
36
36
|
## Rules
|
|
37
37
|
|
|
38
|
-
| Rule
|
|
39
|
-
|
|
|
40
|
-
| `prefer-
|
|
38
|
+
| Rule | Description |
|
|
39
|
+
| :------------------------------------------ | :--------------------------------------------------------------------------------------- |
|
|
40
|
+
| `prefer-canonical-length-constrained-tuple` | Replace hand-rolled uniform tuple spellings with the canonical ts-type-forge tuple type. |
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
It covers the whole uniform-tuple family:
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
| spelling | readonly target | mutable target |
|
|
45
|
+
| :------------------------- | :----------------------- | :------------------------------ |
|
|
46
|
+
| `[V, ...V[]]` | `NonEmptyTuple<V>` | `MutableNonEmptyTuple<V>` |
|
|
47
|
+
| `[V, …×N, ...V[]]` (N ≥ 2) | `MinLengthTuple<N, V>` | `MutableMinLengthTuple<N, V>` |
|
|
48
|
+
| `[V, …×N]` (N ≥ 2) | `FixedLengthTuple<N, V>` | `MutableFixedLengthTuple<N, V>` |
|
|
49
|
+
|
|
50
|
+
### Why the `*Tuple` types and not `NonEmptyArray` / `MinLengthArray`
|
|
51
|
+
|
|
52
|
+
The `*Array` family (`NonEmptyArray`, `MinLengthArray`, `FixedLengthArray`, …)
|
|
53
|
+
encodes its length constraint in a **brand**: `NonEmptyArray<V>` is
|
|
54
|
+
`MinLengthTuple<1, V> & Brand<…>`, a strict _subtype_ of `readonly [V, ...V[]]`.
|
|
55
|
+
Rewriting a declaration to it would narrow the type — an array literal is not
|
|
56
|
+
assignable to a branded type — so the autofix could break call sites.
|
|
57
|
+
|
|
58
|
+
The `*Tuple` family is structural and therefore exactly equal to the spelled-out
|
|
59
|
+
tuple, which makes every fix in this plugin a pure rename.
|
|
60
|
+
|
|
61
|
+
### Examples
|
|
48
62
|
|
|
49
63
|
```ts
|
|
50
64
|
// ❌
|
|
51
65
|
type Names = readonly [string, ...string[]];
|
|
52
|
-
|
|
66
|
+
type Queue = [number, ...number[]];
|
|
67
|
+
type Rgb = readonly [number, number, number];
|
|
68
|
+
type AtLeastTwo = [string, string, ...string[]];
|
|
53
69
|
|
|
54
70
|
// ✅
|
|
55
|
-
type Names =
|
|
56
|
-
|
|
71
|
+
type Names = NonEmptyTuple<string>;
|
|
72
|
+
type Queue = MutableNonEmptyTuple<number>;
|
|
73
|
+
type Rgb = FixedLengthTuple<3, number>;
|
|
74
|
+
type AtLeastTwo = MutableMinLengthTuple<2, string>;
|
|
57
75
|
```
|
|
58
76
|
|
|
59
|
-
|
|
77
|
+
Single-element tuples without a rest (`readonly [V]`) are left alone — they read
|
|
78
|
+
better as-is than `FixedLengthTuple<1, V>`.
|
|
79
|
+
|
|
80
|
+
### The rule deliberately leaves alone
|
|
60
81
|
|
|
61
|
-
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
- files that already bind the name `NonEmptyArray` to something else (a local
|
|
68
|
-
alias, or an import from another module).
|
|
82
|
+
- heterogeneous tuples (`readonly [string, number]`);
|
|
83
|
+
- labelled members (`readonly [head: V, ...tail: V[]]`), because rewriting them
|
|
84
|
+
would silently drop the labels;
|
|
85
|
+
- optional members (`readonly [V, V?]`) and rest-only tuples (`[...V[]]`);
|
|
86
|
+
- files that already bind the target name to something else (a local alias, or
|
|
87
|
+
an import from another module).
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
## Options
|
|
71
90
|
|
|
72
91
|
| Option | Type | Default | Description |
|
|
73
|
-
|
|
|
74
|
-
| `importStyle` | `'global' \| 'named'` | `'global'` | How
|
|
92
|
+
| :------------ | :-------------------- | :--------- | :-------------------------------------------------------- |
|
|
93
|
+
| `importStyle` | `'global' \| 'named'` | `'global'` | How the ts-type-forge type is brought into scope. |
|
|
94
|
+
| `maxLength` | `integer` | `10` | Longest tuple `prefer-length-constrained-tuple` rewrites. |
|
|
75
95
|
|
|
76
96
|
- `'global'` — the ambient globals of `ts-type-forge/global` are in use
|
|
77
97
|
(`/// <reference types="ts-type-forge/global" />`), so the autofix only
|
|
78
98
|
rewrites the type and never touches imports.
|
|
79
99
|
- `'named'` — the autofix additionally inserts
|
|
80
|
-
`import { type
|
|
81
|
-
imported yet.
|
|
100
|
+
`import { type … } from 'ts-type-forge';` when the name is not imported yet.
|
|
82
101
|
|
|
83
|
-
Either way, if the file already imports
|
|
102
|
+
Either way, if the file already imports the target type from `ts-type-forge`
|
|
84
103
|
(possibly under an alias), the autofix reuses that binding.
|
|
85
104
|
|
|
86
105
|
```ts
|
|
87
106
|
{
|
|
88
107
|
rules: {
|
|
89
|
-
'ts-type-forge/prefer-
|
|
108
|
+
'ts-type-forge/prefer-canonical-length-constrained-tuple': [
|
|
109
|
+
'error',
|
|
110
|
+
{ importStyle: 'named', maxLength: 6 },
|
|
111
|
+
],
|
|
90
112
|
},
|
|
91
113
|
}
|
|
92
114
|
```
|
package/dist/rule-types.d.mts
CHANGED
|
@@ -10,7 +10,7 @@ type SpreadOptionsIfIsArray<T extends readonly [Linter.StringSeverity, unknown]>
|
|
|
10
10
|
*/
|
|
11
11
|
type RuleOptionsOf<Name extends keyof typeof tsTypeForgeRules> = (typeof tsTypeForgeRules)[Name] extends TSESLint.RuleModule<infer _MessageIds extends string, infer Options extends readonly unknown[]> ? Options : never;
|
|
12
12
|
/**
|
|
13
|
-
* @description Replace
|
|
13
|
+
* @description Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.
|
|
14
14
|
*
|
|
15
15
|
* ```md
|
|
16
16
|
* | key | value |
|
|
@@ -20,7 +20,7 @@ type RuleOptionsOf<Name extends keyof typeof tsTypeForgeRules> = (typeof tsTypeF
|
|
|
20
20
|
* | fixable | code |
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
declare namespace
|
|
23
|
+
declare namespace PreferCanonicalLengthConstrainedTuple {
|
|
24
24
|
/**
|
|
25
25
|
* ### schema
|
|
26
26
|
*
|
|
@@ -35,7 +35,12 @@ declare namespace PreferNonEmptyArray {
|
|
|
35
35
|
* "global",
|
|
36
36
|
* "named"
|
|
37
37
|
* ],
|
|
38
|
-
* "description": "How
|
|
38
|
+
* "description": "How the ts-type-forge type is brought into scope. 'global' (default) assumes the ambient globals of `ts-type-forge/global` and never touches imports; 'named' makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet."
|
|
39
|
+
* },
|
|
40
|
+
* "maxLength": {
|
|
41
|
+
* "type": "integer",
|
|
42
|
+
* "minimum": 2,
|
|
43
|
+
* "description": "Longest tuple the rule rewrites (default 10). Longer tuples are usually spelled out on purpose, and the named form stops being more readable."
|
|
39
44
|
* }
|
|
40
45
|
* },
|
|
41
46
|
* "additionalProperties": false
|
|
@@ -43,14 +48,14 @@ declare namespace PreferNonEmptyArray {
|
|
|
43
48
|
* ]
|
|
44
49
|
* ```
|
|
45
50
|
*/
|
|
46
|
-
type Options = NonNullable<RuleOptionsOf<'prefer-
|
|
51
|
+
type Options = NonNullable<RuleOptionsOf<'prefer-canonical-length-constrained-tuple'>[0]>;
|
|
47
52
|
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
48
53
|
}
|
|
49
54
|
export type EslintTsTypeForgeRules = Readonly<{
|
|
50
|
-
'ts-type-forge/prefer-
|
|
55
|
+
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
|
|
51
56
|
}>;
|
|
52
57
|
export type EslintTsTypeForgeRulesOption = Readonly<{
|
|
53
|
-
'ts-type-forge/prefer-
|
|
58
|
+
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.Options;
|
|
54
59
|
}>;
|
|
55
60
|
type AssertTrue<B extends true> = B;
|
|
56
61
|
export type RuleNamesAreInSync = AssertTrue<TypeEq<keyof EslintTsTypeForgeRules, `ts-type-forge/${keyof typeof tsTypeForgeRules}`>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;GAIG;AACH,KAAK,aAAa,CAAC,IAAI,SAAS,MAAM,OAAO,gBAAgB,IAC3D,CAAC,OAAO,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,UAAU,CACzD,MAAM,WAAW,SAAS,MAAM,EAChC,MAAM,OAAO,SAAS,SAAS,OAAO,EAAE,CACzC,GACG,OAAO,GACP,KAAK,CAAC;AAEZ;;;;;;;;;;GAUG;AACH,kBAAU,
|
|
1
|
+
{"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;GAIG;AACH,KAAK,aAAa,CAAC,IAAI,SAAS,MAAM,OAAO,gBAAgB,IAC3D,CAAC,OAAO,gBAAgB,CAAC,CAAC,IAAI,CAAC,SAAS,QAAQ,CAAC,UAAU,CACzD,MAAM,WAAW,SAAS,MAAM,EAChC,MAAM,OAAO,SAAS,SAAS,OAAO,EAAE,CACzC,GACG,OAAO,GACP,KAAK,CAAC;AAEZ;;;;;;;;;;GAUG;AACH,kBAAU,qCAAqC,CAAC;IAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,KAAY,OAAO,GAAG,WAAW,CAC/B,aAAa,CAAC,2CAA2C,CAAC,CAAC,CAAC,CAAC,CAC9D,CAAC;IAEF,KAAY,SAAS,GACjB,KAAK,GACL,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,CAAC;CACvE;AAED,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAC5C,yDAAyD,EAAE,qCAAqC,CAAC,SAAS,CAAC;CAC5G,CAAC,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,yDAAyD,EAAE,qCAAqC,CAAC,OAAO,CAAC;CAC1G,CAAC,CAAC;AAIH,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,sBAAsB,EAC5B,iBAAiB,MAAM,OAAO,gBAAgB,EAAE,CACjD,CACF,CAAC"}
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
export type UniformTupleShape = Readonly<{
|
|
3
|
+
/** The node an autofix must replace: the tuple, or the `readonly` operator. */
|
|
4
|
+
node: TSESTree.Node;
|
|
5
|
+
isReadonly: boolean;
|
|
6
|
+
/** Source text of the (single, shared) element type. */
|
|
7
|
+
elementText: string;
|
|
8
|
+
/** Number of leading fixed-position elements. */
|
|
9
|
+
fixedCount: number;
|
|
10
|
+
/** Whether a trailing `...V[]` rest element is present. */
|
|
11
|
+
hasRest: boolean;
|
|
12
|
+
}>;
|
|
2
13
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* text of `V`, or `undefined` when `node` is not that shape.
|
|
14
|
+
* Recognizes a tuple whose every position has the *same* element type — the
|
|
15
|
+
* hand-rolled spelling of ts-type-forge's length-constrained tuple family:
|
|
6
16
|
*
|
|
7
|
-
*
|
|
17
|
+
* | shape | meaning |
|
|
18
|
+
* | :------------------- | :------------------------- |
|
|
19
|
+
* | `[V, ..., V]` | `FixedLengthTuple<N, V>` |
|
|
20
|
+
* | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |
|
|
8
21
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* rewriting them would
|
|
22
|
+
* Returns `undefined` for anything else. Deliberately narrow:
|
|
23
|
+
*
|
|
24
|
+
* - Element types are compared by normalized source text, so heterogeneous
|
|
25
|
+
* tuples are never matched.
|
|
26
|
+
* - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since
|
|
27
|
+
* rewriting them would drop information.
|
|
28
|
+
* - A tuple that is only a rest element (`[...V[]]`) is not a length
|
|
29
|
+
* constraint at all.
|
|
15
30
|
*/
|
|
16
|
-
export declare const
|
|
31
|
+
export declare const analyzeUniformTuple: (tuple: TSESTree.TSTupleType, sourceCode: TSESLint.SourceCode) => UniformTupleShape | undefined;
|
|
17
32
|
//# 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;AAIlC
|
|
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;AAIlC,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;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,mBAAmB,UACvB,QAAQ,CAAC,WAAW,cACf,QAAQ,CAAC,UAAU,KAC9B,iBAAiB,GAAG,SA0DtB,CAAC"}
|
package/dist/rules/ast-utils.mjs
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
3
|
+
const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
4
|
+
const { parent } = tuple;
|
|
5
|
+
const isReadonly = parent.type === AST_NODE_TYPES.TSTypeOperator && parent.operator === "readonly";
|
|
6
|
+
const elements = tuple.elementTypes;
|
|
7
|
+
const last = elements.at(-1);
|
|
8
|
+
const hasRest = last?.type === AST_NODE_TYPES.TSRestType;
|
|
9
|
+
const leading = hasRest ? elements.slice(0, -1) : elements;
|
|
10
|
+
if (leading.length === 0 || leading.some(
|
|
11
|
+
(element) => element.type === AST_NODE_TYPES.TSRestType || element.type === AST_NODE_TYPES.TSOptionalType || element.type === AST_NODE_TYPES.TSNamedTupleMember
|
|
12
|
+
)) {
|
|
9
13
|
return void 0;
|
|
10
14
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
15
|
+
const restElementType = last !== void 0 && hasRest ? getArrayElementType(last.typeAnnotation) : void 0;
|
|
16
|
+
if (hasRest && restElementType === void 0) return void 0;
|
|
17
|
+
const [head] = leading;
|
|
18
|
+
if (head === void 0) return void 0;
|
|
19
|
+
const elementText = sourceCode.getText(head);
|
|
20
|
+
const expected = normalizeWhitespace(elementText);
|
|
21
|
+
const others = [
|
|
22
|
+
...leading.slice(1),
|
|
23
|
+
...restElementType === void 0 ? [] : [restElementType]
|
|
24
|
+
];
|
|
25
|
+
return others.every(
|
|
26
|
+
(element) => normalizeWhitespace(sourceCode.getText(element)) === expected
|
|
27
|
+
) ? {
|
|
28
|
+
node: isReadonly ? parent : tuple,
|
|
29
|
+
isReadonly,
|
|
30
|
+
elementText,
|
|
31
|
+
fixedCount: leading.length,
|
|
32
|
+
hasRest
|
|
33
|
+
} : void 0;
|
|
18
34
|
};
|
|
19
35
|
const getArrayElementType = (node) => {
|
|
20
36
|
if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;
|
|
@@ -22,5 +38,5 @@ const getArrayElementType = (node) => {
|
|
|
22
38
|
};
|
|
23
39
|
const normalizeWhitespace = (text) => text.replaceAll(/\s+/gu, " ").trim();
|
|
24
40
|
|
|
25
|
-
export {
|
|
41
|
+
export { analyzeUniformTuple };
|
|
26
42
|
//# sourceMappingURL=ast-utils.mjs.map
|
|
@@ -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\
|
|
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 ? elements.slice(0, -1) : elements;\n\n if (\n leading.length === 0 ||\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 ...leading.slice(1),\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/** 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\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":";;AAsCO,MAAM,mBAAA,GAAsB,CACjC,KAAA,EACA,UAAA,KACkC;AAClC,EAAA,MAAM,EAAE,QAAO,GAAI,KAAA;AAEnB,EAAA,MAAM,aACJ,MAAA,CAAO,IAAA,KAAS,cAAA,CAAe,cAAA,IAC/B,OAAO,QAAA,KAAa,UAAA;AAEtB,EAAA,MAAM,WAAW,KAAA,CAAM,YAAA;AAEvB,EAAA,MAAM,IAAA,GAAO,QAAA,CAAS,EAAA,CAAG,EAAE,CAAA;AAE3B,EAAA,MAAM,OAAA,GAAU,IAAA,EAAM,IAAA,KAAS,cAAA,CAAe,UAAA;AAE9C,EAAA,MAAM,UAAU,OAAA,GAAU,QAAA,CAAS,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,GAAI,QAAA;AAElD,EAAA,IACE,OAAA,CAAQ,MAAA,KAAW,CAAA,IACnB,OAAA,CAAQ,IAAA;AAAA,IACN,CAAC,OAAA,KACC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,UAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,cAAA,IAChC,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe;AAAA,GACpC,EACA;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,kBACJ,IAAA,KAAS,MAAA,IAAa,UAClB,mBAAA,CAAoB,IAAA,CAAK,cAAc,CAAA,GACvC,MAAA;AAEN,EAAA,IAAI,OAAA,IAAW,eAAA,KAAoB,MAAA,EAAW,OAAO,MAAA;AAErD,EAAA,MAAM,CAAC,IAAI,CAAA,GAAI,OAAA;AAEf,EAAA,IAAI,IAAA,KAAS,QAAW,OAAO,MAAA;AAE/B,EAAA,MAAM,WAAA,GAAc,UAAA,CAAW,OAAA,CAAQ,IAAI,CAAA;AAE3C,EAAA,MAAM,QAAA,GAAW,oBAAoB,WAAW,CAAA;AAEhD,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,GAAG,OAAA,CAAQ,KAAA,CAAM,CAAC,CAAA;AAAA,IAClB,GAAI,eAAA,KAAoB,MAAA,GAAY,EAAC,GAAI,CAAC,eAAe;AAAA,GAC3D;AAEA,EAAA,OAAO,MAAA,CAAO,KAAA;AAAA,IACZ,CAAC,OAAA,KAAY,mBAAA,CAAoB,WAAW,OAAA,CAAQ,OAAO,CAAC,CAAA,KAAM;AAAA,GACpE,GACI;AAAA,IACE,IAAA,EAAM,aAAa,MAAA,GAAS,KAAA;AAAA,IAC5B,UAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAY,OAAA,CAAQ,MAAA;AAAA,IACpB;AAAA,GACF,GACA,MAAA;AACN;AAGA,MAAM,mBAAA,GAAsB,CAC1B,IAAA,KACkC;AAClC,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,WAAA,SAAoB,IAAA,CAAK,WAAA;AAE1D,EAAA,OAAO,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,cAAA,IAClC,KAAK,QAAA,KAAa,UAAA,IAClB,IAAA,CAAK,cAAA,EAAgB,IAAA,KAAS,cAAA,CAAe,WAAA,GAC3C,IAAA,CAAK,eAAe,WAAA,GACpB,MAAA;AACN,CAAA;AAEA,MAAM,mBAAA,GAAsB,CAAC,IAAA,KAC3B,IAAA,CAAK,WAAW,OAAA,EAAS,GAAG,EAAE,IAAA,EAAK;;;;"}
|
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
export declare const TS_TYPE_FORGE_MODULE = "ts-type-forge";
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const NON_EMPTY_TUPLE_TYPE_NAME = "NonEmptyTuple";
|
|
3
|
+
export declare const MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME = "MutableNonEmptyTuple";
|
|
4
|
+
export declare const MIN_LENGTH_TUPLE_TYPE_NAME = "MinLengthTuple";
|
|
5
|
+
export declare const MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME = "MutableMinLengthTuple";
|
|
6
|
+
export declare const FIXED_LENGTH_TUPLE_TYPE_NAME = "FixedLengthTuple";
|
|
7
|
+
export declare const MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME = "MutableFixedLengthTuple";
|
|
8
|
+
/**
|
|
9
|
+
* Upper bound for the `maxLength` option of `prefer-length-constrained-tuple`.
|
|
10
|
+
* Beyond this, spelling the tuple out is usually intentional (e.g. a matrix
|
|
11
|
+
* row), and the named form stops being more readable.
|
|
12
|
+
*/
|
|
13
|
+
export declare const DEFAULT_MAX_TUPLE_LENGTH = 10;
|
|
3
14
|
//# sourceMappingURL=constants.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.mts","sourceRoot":"","sources":["../../src/rules/constants.mts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,eAAO,MAAM,yBAAyB,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.mts","sourceRoot":"","sources":["../../src/rules/constants.mts"],"names":[],"mappings":"AAAA,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD,eAAO,MAAM,yBAAyB,kBAAkB,CAAC;AAEzD,eAAO,MAAM,iCAAiC,yBAAyB,CAAC;AAExE,eAAO,MAAM,0BAA0B,mBAAmB,CAAC;AAE3D,eAAO,MAAM,kCAAkC,0BAA0B,CAAC;AAE1E,eAAO,MAAM,4BAA4B,qBAAqB,CAAC;AAE/D,eAAO,MAAM,oCAAoC,4BAA4B,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC"}
|
package/dist/rules/constants.mjs
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const TS_TYPE_FORGE_MODULE = "ts-type-forge";
|
|
2
|
-
const
|
|
2
|
+
const NON_EMPTY_TUPLE_TYPE_NAME = "NonEmptyTuple";
|
|
3
|
+
const MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME = "MutableNonEmptyTuple";
|
|
4
|
+
const MIN_LENGTH_TUPLE_TYPE_NAME = "MinLengthTuple";
|
|
5
|
+
const MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME = "MutableMinLengthTuple";
|
|
6
|
+
const FIXED_LENGTH_TUPLE_TYPE_NAME = "FixedLengthTuple";
|
|
7
|
+
const MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME = "MutableFixedLengthTuple";
|
|
8
|
+
const DEFAULT_MAX_TUPLE_LENGTH = 10;
|
|
3
9
|
|
|
4
|
-
export {
|
|
10
|
+
export { DEFAULT_MAX_TUPLE_LENGTH, FIXED_LENGTH_TUPLE_TYPE_NAME, MIN_LENGTH_TUPLE_TYPE_NAME, MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME, MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME, MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME, NON_EMPTY_TUPLE_TYPE_NAME, TS_TYPE_FORGE_MODULE };
|
|
5
11
|
//# sourceMappingURL=constants.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_TYPE_FORGE_MODULE = 'ts-type-forge';\n\nexport const
|
|
1
|
+
{"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_TYPE_FORGE_MODULE = 'ts-type-forge';\n\nexport const NON_EMPTY_TUPLE_TYPE_NAME = 'NonEmptyTuple';\n\nexport const MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME = 'MutableNonEmptyTuple';\n\nexport const MIN_LENGTH_TUPLE_TYPE_NAME = 'MinLengthTuple';\n\nexport const MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME = 'MutableMinLengthTuple';\n\nexport const FIXED_LENGTH_TUPLE_TYPE_NAME = 'FixedLengthTuple';\n\nexport const MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME = 'MutableFixedLengthTuple';\n\n/**\n * Upper bound for the `maxLength` option of `prefer-length-constrained-tuple`.\n * Beyond this, spelling the tuple out is usually intentional (e.g. a matrix\n * row), and the named form stops being more readable.\n */\nexport const DEFAULT_MAX_TUPLE_LENGTH = 10;\n"],"names":[],"mappings":"AAAO,MAAM,oBAAA,GAAuB;AAE7B,MAAM,yBAAA,GAA4B;AAElC,MAAM,iCAAA,GAAoC;AAE1C,MAAM,0BAAA,GAA6B;AAEnC,MAAM,kCAAA,GAAqC;AAE3C,MAAM,4BAAA,GAA+B;AAErC,MAAM,oCAAA,GAAuC;AAO7C,MAAM,wBAAA,GAA2B;;;;"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = readonly [
|
|
3
|
+
Readonly<{
|
|
4
|
+
importStyle?: 'global' | 'named';
|
|
5
|
+
maxLength?: number;
|
|
6
|
+
}>?
|
|
7
|
+
];
|
|
8
|
+
type MessageIds = 'useCanonicalTuple';
|
|
9
|
+
export declare const preferCanonicalLengthConstrainedTuple: TSESLint.RuleModule<MessageIds, Options>;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=prefer-canonical-length-constrained-tuple.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-canonical-length-constrained-tuple.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-canonical-length-constrained-tuple.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAkBzD,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,WAAW,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;QACjC,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,mBAAmB,CAAC;AAQtC,eAAO,MAAM,qCAAqC,EAAE,QAAQ,CAAC,UAAU,CACrE,UAAU,EACV,OAAO,CA6EC,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { analyzeUniformTuple } from './ast-utils.mjs';
|
|
2
|
+
import { DEFAULT_MAX_TUPLE_LENGTH, NON_EMPTY_TUPLE_TYPE_NAME, MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME, MIN_LENGTH_TUPLE_TYPE_NAME, MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME, FIXED_LENGTH_TUPLE_TYPE_NAME, MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME } from './constants.mjs';
|
|
3
|
+
import { DEFAULT_IMPORT_STYLE, IMPORT_STYLE_SCHEMA_PROPERTY, reportTupleRewrites } from './tuple-rule-utils.mjs';
|
|
4
|
+
|
|
5
|
+
const MIN_FIXED_LENGTH = 2;
|
|
6
|
+
const preferCanonicalLengthConstrainedTuple = {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "suggestion",
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type."
|
|
11
|
+
},
|
|
12
|
+
fixable: "code",
|
|
13
|
+
schema: [
|
|
14
|
+
{
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,
|
|
18
|
+
maxLength: {
|
|
19
|
+
type: "integer",
|
|
20
|
+
minimum: MIN_FIXED_LENGTH,
|
|
21
|
+
description: [
|
|
22
|
+
"Longest tuple the rule rewrites (default",
|
|
23
|
+
`${DEFAULT_MAX_TUPLE_LENGTH}).`,
|
|
24
|
+
"Longer tuples are usually spelled out on purpose, and the named",
|
|
25
|
+
"form stops being more readable."
|
|
26
|
+
].join(" ")
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
messages: {
|
|
33
|
+
useCanonicalTuple: "Replace `{{original}}` with `{{replacement}}` from ts-type-forge."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
create: (context) => {
|
|
37
|
+
const sourceCode = context.sourceCode;
|
|
38
|
+
const options = context.options[0];
|
|
39
|
+
const importStyle = options?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
40
|
+
const maxLength = options?.maxLength ?? DEFAULT_MAX_TUPLE_LENGTH;
|
|
41
|
+
const mut_rewrites = [];
|
|
42
|
+
return {
|
|
43
|
+
TSTupleType: (node) => {
|
|
44
|
+
const shape = analyzeUniformTuple(node, sourceCode);
|
|
45
|
+
if (shape === void 0 || shape.fixedCount > maxLength) return;
|
|
46
|
+
const canonicalName = canonicalNameFor(shape);
|
|
47
|
+
if (canonicalName === void 0) return;
|
|
48
|
+
mut_rewrites.push({
|
|
49
|
+
node: shape.node,
|
|
50
|
+
canonicalName,
|
|
51
|
+
typeArgs: shape.fixedCount === 1 ? shape.elementText : `${shape.fixedCount}, ${shape.elementText}`
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
"Program:exit": () => {
|
|
55
|
+
reportTupleRewrites(
|
|
56
|
+
context,
|
|
57
|
+
mut_rewrites,
|
|
58
|
+
"useCanonicalTuple",
|
|
59
|
+
importStyle
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
defaultOptions: [
|
|
65
|
+
{ importStyle: DEFAULT_IMPORT_STYLE, maxLength: DEFAULT_MAX_TUPLE_LENGTH }
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
const canonicalNameFor = (shape) => {
|
|
69
|
+
const { isReadonly, hasRest, fixedCount } = shape;
|
|
70
|
+
if (hasRest) {
|
|
71
|
+
if (fixedCount === 1) {
|
|
72
|
+
return isReadonly ? NON_EMPTY_TUPLE_TYPE_NAME : MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME;
|
|
73
|
+
}
|
|
74
|
+
return isReadonly ? MIN_LENGTH_TUPLE_TYPE_NAME : MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME;
|
|
75
|
+
}
|
|
76
|
+
if (fixedCount < MIN_FIXED_LENGTH) return void 0;
|
|
77
|
+
return isReadonly ? FIXED_LENGTH_TUPLE_TYPE_NAME : MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { preferCanonicalLengthConstrainedTuple };
|
|
81
|
+
//# sourceMappingURL=prefer-canonical-length-constrained-tuple.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-canonical-length-constrained-tuple.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-tuple.mts"],"sourcesContent":["import { type TSESLint } from '@typescript-eslint/utils';\nimport { analyzeUniformTuple, type UniformTupleShape } from './ast-utils.mjs';\nimport {\n DEFAULT_MAX_TUPLE_LENGTH,\n FIXED_LENGTH_TUPLE_TYPE_NAME,\n MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME,\n MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME,\n NON_EMPTY_TUPLE_TYPE_NAME,\n} from './constants.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n reportTupleRewrites,\n type TupleRewrite,\n} from './tuple-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n maxLength?: number;\n }>?,\n];\n\ntype MessageIds = 'useCanonicalTuple';\n\n/**\n * A single fixed element without a rest (`[V]`) is left alone: it reads better\n * than `FixedLengthTuple<1, V>`.\n */\nconst MIN_FIXED_LENGTH = 2;\n\nexport const preferCanonicalLengthConstrainedTuple: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace uniform tuple spellings such as `[V, ...V[]]` / `[V, V, ...V[]]` / `[V, V, V]` with the canonical ts-type-forge tuple type.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n maxLength: {\n type: 'integer',\n minimum: MIN_FIXED_LENGTH,\n description: [\n 'Longest tuple the rule rewrites (default',\n `${DEFAULT_MAX_TUPLE_LENGTH}).`,\n 'Longer tuples are usually spelled out on purpose, and the named',\n 'form stops being more readable.',\n ].join(' '),\n },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useCanonicalTuple:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const options = context.options[0];\n\n const importStyle = options?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const maxLength = options?.maxLength ?? DEFAULT_MAX_TUPLE_LENGTH;\n\n const mut_rewrites: TupleRewrite[] = [];\n\n return {\n TSTupleType: (node) => {\n const shape = analyzeUniformTuple(node, sourceCode);\n\n if (shape === undefined || shape.fixedCount > maxLength) return;\n\n const canonicalName = canonicalNameFor(shape);\n\n if (canonicalName === undefined) return;\n\n mut_rewrites.push({\n node: shape.node,\n canonicalName,\n typeArgs:\n shape.fixedCount === 1\n ? shape.elementText\n : `${shape.fixedCount}, ${shape.elementText}`,\n });\n },\n 'Program:exit': () => {\n reportTupleRewrites(\n context,\n mut_rewrites,\n 'useCanonicalTuple',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [\n { importStyle: DEFAULT_IMPORT_STYLE, maxLength: DEFAULT_MAX_TUPLE_LENGTH },\n ],\n} as const;\n\n/**\n * The ts-type-forge type a given tuple shape spells out, or `undefined` when the\n * shape is deliberately left alone.\n *\n * | shape | readonly | mutable |\n * | :-------------------- | :------------------ | :------------------------- |\n * | `[V, ...V[]]` | `NonEmptyTuple` | `MutableNonEmptyTuple` |\n * | `[V, …×N, ...V[]]` | `MinLengthTuple` | `MutableMinLengthTuple` |\n * | `[V, …×N]` (N >= 2) | `FixedLengthTuple` | `MutableFixedLengthTuple` |\n */\nconst canonicalNameFor = (\n // `UniformTupleShape` carries a `TSESTree.Node`, which is not deeply readonly.\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n shape: UniformTupleShape,\n): string | undefined => {\n const { isReadonly, hasRest, fixedCount } = shape;\n\n if (hasRest) {\n if (fixedCount === 1) {\n return isReadonly\n ? NON_EMPTY_TUPLE_TYPE_NAME\n : MUTABLE_NON_EMPTY_TUPLE_TYPE_NAME;\n }\n\n return isReadonly\n ? MIN_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_MIN_LENGTH_TUPLE_TYPE_NAME;\n }\n\n if (fixedCount < MIN_FIXED_LENGTH) return undefined;\n\n return isReadonly\n ? FIXED_LENGTH_TUPLE_TYPE_NAME\n : MUTABLE_FIXED_LENGTH_TUPLE_TYPE_NAME;\n};\n"],"names":[],"mappings":";;;;AA+BA,MAAM,gBAAA,GAAmB,CAAA;AAElB,MAAM,qCAAA,GAGT;AAAA,EACF,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,YAAA;AAAA,IACN,IAAA,EAAM;AAAA,MACJ,WAAA,EACE;AAAA,KACJ;AAAA,IACA,OAAA,EAAS,MAAA;AAAA,IACT,MAAA,EAAQ;AAAA,MACN;AAAA,QACE,IAAA,EAAM,QAAA;AAAA,QACN,UAAA,EAAY;AAAA,UACV,WAAA,EAAa,4BAAA;AAAA,UACb,SAAA,EAAW;AAAA,YACT,IAAA,EAAM,SAAA;AAAA,YACN,OAAA,EAAS,gBAAA;AAAA,YACT,WAAA,EAAa;AAAA,cACX,0CAAA;AAAA,cACA,GAAG,wBAAwB,CAAA,EAAA,CAAA;AAAA,cAC3B,iEAAA;AAAA,cACA;AAAA,aACF,CAAE,KAAK,GAAG;AAAA;AACZ,SACF;AAAA,QACA,oBAAA,EAAsB;AAAA;AACxB,KACF;AAAA,IACA,QAAA,EAAU;AAAA,MACR,iBAAA,EACE;AAAA;AACJ,GACF;AAAA,EAEA,MAAA,EAAQ,CAAC,OAAA,KAAY;AACnB,IAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,IAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,CAAQ,CAAC,CAAA;AAEjC,IAAA,MAAM,WAAA,GAAc,SAAS,WAAA,IAAe,oBAAA;AAE5C,IAAA,MAAM,SAAA,GAAY,SAAS,SAAA,IAAa,wBAAA;AAExC,IAAA,MAAM,eAA+B,EAAC;AAEtC,IAAA,OAAO;AAAA,MACL,WAAA,EAAa,CAAC,IAAA,KAAS;AACrB,QAAA,MAAM,KAAA,GAAQ,mBAAA,CAAoB,IAAA,EAAM,UAAU,CAAA;AAElD,QAAA,IAAI,KAAA,KAAU,MAAA,IAAa,KAAA,CAAM,UAAA,GAAa,SAAA,EAAW;AAEzD,QAAA,MAAM,aAAA,GAAgB,iBAAiB,KAAK,CAAA;AAE5C,QAAA,IAAI,kBAAkB,MAAA,EAAW;AAEjC,QAAA,YAAA,CAAa,IAAA,CAAK;AAAA,UAChB,MAAM,KAAA,CAAM,IAAA;AAAA,UACZ,aAAA;AAAA,UACA,QAAA,EACE,KAAA,CAAM,UAAA,KAAe,CAAA,GACjB,KAAA,CAAM,WAAA,GACN,CAAA,EAAG,KAAA,CAAM,UAAU,CAAA,EAAA,EAAK,KAAA,CAAM,WAAW,CAAA;AAAA,SAChD,CAAA;AAAA,MACH,CAAA;AAAA,MACA,gBAAgB,MAAM;AACpB,QAAA,mBAAA;AAAA,UACE,OAAA;AAAA,UACA,YAAA;AAAA,UACA,mBAAA;AAAA,UACA;AAAA,SACF;AAAA,MACF;AAAA,KACF;AAAA,EACF,CAAA;AAAA,EACA,cAAA,EAAgB;AAAA,IACd,EAAE,WAAA,EAAa,oBAAA,EAAsB,SAAA,EAAW,wBAAA;AAAyB;AAE7E;AAYA,MAAM,gBAAA,GAAmB,CAGvB,KAAA,KACuB;AACvB,EAAA,MAAM,EAAE,UAAA,EAAY,OAAA,EAAS,UAAA,EAAW,GAAI,KAAA;AAE5C,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,IAAI,eAAe,CAAA,EAAG;AACpB,MAAA,OAAO,aACH,yBAAA,GACA,iCAAA;AAAA,IACN;AAEA,IAAA,OAAO,aACH,0BAAA,GACA,kCAAA;AAAA,EACN;AAEA,EAAA,IAAI,UAAA,GAAa,kBAAkB,OAAO,MAAA;AAE1C,EAAA,OAAO,aACH,4BAAA,GACA,oCAAA;AACN,CAAA;;;;"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare const tsTypeForgeRules: {
|
|
2
|
-
readonly 'prefer-
|
|
3
|
-
importStyle?:
|
|
2
|
+
readonly 'prefer-canonical-length-constrained-tuple': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalTuple", readonly [(Readonly<{
|
|
3
|
+
importStyle?: 'global' | 'named';
|
|
4
|
+
maxLength?: number;
|
|
4
5
|
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
5
6
|
};
|
|
6
7
|
//# sourceMappingURL=rules.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB;;;;;CAGa,CAAC"}
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
2
2
|
|
|
3
3
|
const tsTypeForgeRules = {
|
|
4
|
-
"prefer-
|
|
4
|
+
"prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
export { tsTypeForgeRules };
|
package/dist/rules/rules.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport {
|
|
1
|
+
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;AAGO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,2CAAA,EACE;AACJ;;;;"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/** `JSONSchema4`, reached through the public rule-module types. */
|
|
3
|
+
type JsonSchema = Exclude<TSESLint.RuleModule<string, readonly unknown[]>['meta']['schema'], readonly unknown[]>;
|
|
4
|
+
export type ImportStyle = 'global' | 'named';
|
|
5
|
+
export declare const DEFAULT_IMPORT_STYLE: ImportStyle;
|
|
6
|
+
export declare const IMPORT_STYLE_SCHEMA_PROPERTY: JsonSchema;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves how a ts-type-forge type name should be written in this file, and
|
|
9
|
+
* whether the autofix has to add an import for it.
|
|
10
|
+
*
|
|
11
|
+
* Returns `undefined` when the name is already bound to something else (a local
|
|
12
|
+
* alias, or an import from another module) — rewriting to it would then silently
|
|
13
|
+
* resolve to the wrong declaration, which is exactly what happens inside
|
|
14
|
+
* ts-type-forge's own sources.
|
|
15
|
+
*/
|
|
16
|
+
export declare const resolveTypeName: (program: TSESTree.Program, canonicalName: string, importStyle: ImportStyle) => Readonly<{
|
|
17
|
+
localName: string;
|
|
18
|
+
needsImport: boolean;
|
|
19
|
+
}> | undefined;
|
|
20
|
+
export type TupleRewrite = Readonly<{
|
|
21
|
+
node: TSESTree.Node;
|
|
22
|
+
canonicalName: string;
|
|
23
|
+
/** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */
|
|
24
|
+
typeArgs: string;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Emits one report per rewrite, resolving each target name and attaching every
|
|
28
|
+
* required import to the *first* report.
|
|
29
|
+
*
|
|
30
|
+
* All import fixes must live on a single report: `insertTextBefore(program, …)`
|
|
31
|
+
* produces overlapping ranges, and ESLint merges each report's fixes into one
|
|
32
|
+
* span — so spreading them across reports makes the fixes conflict and only one
|
|
33
|
+
* survives per pass.
|
|
34
|
+
*/
|
|
35
|
+
export declare const reportTupleRewrites: <MessageIds extends string>(context: TSESLint.RuleContext<MessageIds, readonly unknown[]>, rewrites: readonly TupleRewrite[], messageId: MessageIds, importStyle: ImportStyle) => void;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=tuple-rule-utils.d.mts.map
|
|
@@ -0,0 +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;AAWxE,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,WAAsB,CAAC;AAE1D,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,IAsCF,CAAC"}
|