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.
Files changed (37) hide show
  1. package/README.md +53 -31
  2. package/dist/rule-types.d.mts +11 -6
  3. package/dist/rule-types.d.mts.map +1 -1
  4. package/dist/rules/ast-utils.d.mts +26 -11
  5. package/dist/rules/ast-utils.d.mts.map +1 -1
  6. package/dist/rules/ast-utils.mjs +30 -14
  7. package/dist/rules/ast-utils.mjs.map +1 -1
  8. package/dist/rules/constants.d.mts +12 -1
  9. package/dist/rules/constants.d.mts.map +1 -1
  10. package/dist/rules/constants.mjs +8 -2
  11. package/dist/rules/constants.mjs.map +1 -1
  12. package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts +11 -0
  13. package/dist/rules/prefer-canonical-length-constrained-tuple.d.mts.map +1 -0
  14. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +81 -0
  15. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -0
  16. package/dist/rules/rules.d.mts +3 -2
  17. package/dist/rules/rules.d.mts.map +1 -1
  18. package/dist/rules/rules.mjs +2 -2
  19. package/dist/rules/rules.mjs.map +1 -1
  20. package/dist/rules/tuple-rule-utils.d.mts +37 -0
  21. package/dist/rules/tuple-rule-utils.d.mts.map +1 -0
  22. package/dist/rules/tuple-rule-utils.mjs +56 -0
  23. package/dist/rules/tuple-rule-utils.mjs.map +1 -0
  24. package/package.json +22 -22
  25. package/src/rule-types.mts +13 -6
  26. package/src/rules/ast-utils.mts +72 -30
  27. package/src/rules/constants.mts +18 -1
  28. package/src/rules/prefer-canonical-length-constrained-tuple.mts +149 -0
  29. package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +178 -0
  30. package/src/rules/rules.mts +3 -2
  31. package/src/rules/tuple-rule-utils.mts +124 -0
  32. package/dist/rules/prefer-non-empty-array.d.mts +0 -11
  33. package/dist/rules/prefer-non-empty-array.d.mts.map +0 -1
  34. package/dist/rules/prefer-non-empty-array.mjs +0 -81
  35. package/dist/rules/prefer-non-empty-array.mjs.map +0 -1
  36. package/src/rules/prefer-non-empty-array.mts +0 -129
  37. 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 — e.g.
5
- preferring `NonEmptyArray<V>` over the hand-rolled `readonly [V, ...V[]]`
6
- tuple spelling. Every rule is auto-fixable.
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-non-empty-array': 'error',
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 | Description |
39
- | ------------------------ | ------------------------------------------------------- |
40
- | `prefer-non-empty-array` | Replace `readonly [V, ...V[]]` with `NonEmptyArray<V>`. |
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
- ### `prefer-non-empty-array`
42
+ It covers the whole uniform-tuple family:
43
43
 
44
- `readonly [V, ...V[]]` is the hand-rolled spelling of "an array with at least
45
- one element". `ts-type-forge` already exposes it as `NonEmptyArray<V>` (an
46
- alias of `MinLengthArray<1, V>`), which reads better and stays consistent with
47
- the rest of the length-constrained array family.
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
- declare const head: (xs: readonly [number, ...(readonly number[])]) => number;
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 = NonEmptyArray<string>;
56
- declare const head: (xs: NonEmptyArray<number>) => number;
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
- The rule deliberately leaves alone:
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
- - mutable tuples (`[V, ...V[]]`), which correspond to
62
- `MutableNonEmptyArray<V>` rather than `NonEmptyArray<V>`;
63
- - longer minimum lengths (`readonly [V, V, ...V[]]` — that is
64
- `MinLengthArray<2, V>`);
65
- - labelled members (`readonly [head: V, ...tail: V[]]`), because rewriting
66
- them would silently drop the labels;
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
- #### Options
89
+ ## Options
71
90
 
72
91
  | Option | Type | Default | Description |
73
- | ------------- | --------------------- | ---------- | --------------------------------------------------------- |
74
- | `importStyle` | `'global' \| 'named'` | `'global'` | How `NonEmptyArray` is expected to be brought into scope. |
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 NonEmptyArray } from 'ts-type-forge';` when the name is not
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 `NonEmptyArray` from `ts-type-forge`
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-non-empty-array': ['error', { importStyle: 'named' }],
108
+ 'ts-type-forge/prefer-canonical-length-constrained-tuple': [
109
+ 'error',
110
+ { importStyle: 'named', maxLength: 6 },
111
+ ],
90
112
  },
91
113
  }
92
114
  ```
@@ -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 the `readonly [V, ...V[]]` tuple spelling with `NonEmptyArray<V>` from ts-type-forge.
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 PreferNonEmptyArray {
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 `NonEmptyArray` is brought into scope. 'global' (default) assumes the ambient globals of `ts-type-forge/global` and never touches imports; 'named' makes the autofix add `import { type NonEmptyArray } from 'ts-type-forge';` when the name is not imported yet."
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-non-empty-array'>[0]>;
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-non-empty-array': PreferNonEmptyArray.RuleEntry;
55
+ 'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
51
56
  }>;
52
57
  export type EslintTsTypeForgeRulesOption = Readonly<{
53
- 'ts-type-forge/prefer-non-empty-array': PreferNonEmptyArray.Options;
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,mBAAmB,CAAC;IAC5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAY,OAAO,GAAG,WAAW,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAE9E,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,sCAAsC,EAAE,mBAAmB,CAAC,SAAS,CAAC;CACvE,CAAC,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,sCAAsC,EAAE,mBAAmB,CAAC,OAAO,CAAC;CACrE,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
+ {"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
- * Matches the "non-empty array" tuple spelling — `readonly [V, ...V[]]` (and
4
- * its equivalent `readonly [V, ...(readonly V[])]`) — and returns the source
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
- * Deliberately narrow:
17
+ * | shape | meaning |
18
+ * | :------------------- | :------------------------- |
19
+ * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |
20
+ * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |
8
21
  *
9
- * - Only the `readonly` operator is matched. A mutable `[V, ...V[]]` is a
10
- * different type (`MutableNonEmptyArray<V>`) and is left alone.
11
- * - Exactly two elements, the second being a rest element whose element type is
12
- * textually identical to the first.
13
- * - Labelled members (`readonly [head: V, ...tail: V[]]`) are skipped, since
14
- * rewriting them would silently drop the labels.
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 getNonEmptyArrayElementText: (node: TSESTree.TSTypeOperator, sourceCode: TSESLint.SourceCode) => string | undefined;
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;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,2BAA2B,SAChC,QAAQ,CAAC,cAAc,cACjB,QAAQ,CAAC,UAAU,KAC9B,MAAM,GAAG,SAgCX,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;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"}
@@ -1,20 +1,36 @@
1
1
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
2
 
3
- const getNonEmptyArrayElementText = (node, sourceCode) => {
4
- if (node.operator !== "readonly") return void 0;
5
- const tuple = node.typeAnnotation;
6
- if (tuple?.type !== AST_NODE_TYPES.TSTupleType) return void 0;
7
- const [head, tail, ...rest] = tuple.elementTypes;
8
- if (head === void 0 || tail === void 0 || rest.length > 0) {
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
- if (head.type === AST_NODE_TYPES.TSRestType || head.type === AST_NODE_TYPES.TSOptionalType || head.type === AST_NODE_TYPES.TSNamedTupleMember || tail.type !== AST_NODE_TYPES.TSRestType) {
12
- return void 0;
13
- }
14
- const restElementType = getArrayElementType(tail.typeAnnotation);
15
- if (restElementType === void 0) return void 0;
16
- const headText = sourceCode.getText(head);
17
- return normalizeWhitespace(headText) === normalizeWhitespace(sourceCode.getText(restElementType)) ? headText : void 0;
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 { getNonEmptyArrayElementText };
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\n/**\n * Matches the \"non-empty array\" tuple spelling — `readonly [V, ...V[]]` (and\n * its equivalent `readonly [V, ...(readonly V[])]`) — and returns the source\n * text of `V`, or `undefined` when `node` is not that shape.\n *\n * Deliberately narrow:\n *\n * - Only the `readonly` operator is matched. A mutable `[V, ...V[]]` is a\n * different type (`MutableNonEmptyArray<V>`) and is left alone.\n * - Exactly two elements, the second being a rest element whose element type is\n * textually identical to the first.\n * - Labelled members (`readonly [head: V, ...tail: V[]]`) are skipped, since\n * rewriting them would silently drop the labels.\n */\nexport const getNonEmptyArrayElementText = (\n node: TSESTree.TSTypeOperator,\n sourceCode: TSESLint.SourceCode,\n): string | undefined => {\n if (node.operator !== 'readonly') return undefined;\n\n const tuple = node.typeAnnotation;\n\n if (tuple?.type !== AST_NODE_TYPES.TSTupleType) return undefined;\n\n const [head, tail, ...rest] = tuple.elementTypes;\n\n if (head === undefined || tail === undefined || rest.length > 0) {\n return undefined;\n }\n\n if (\n head.type === AST_NODE_TYPES.TSRestType ||\n head.type === AST_NODE_TYPES.TSOptionalType ||\n head.type === AST_NODE_TYPES.TSNamedTupleMember ||\n tail.type !== AST_NODE_TYPES.TSRestType\n ) {\n return undefined;\n }\n\n const restElementType = getArrayElementType(tail.typeAnnotation);\n\n if (restElementType === undefined) return undefined;\n\n const headText = sourceCode.getText(head);\n\n return normalizeWhitespace(headText) ===\n normalizeWhitespace(sourceCode.getText(restElementType))\n ? headText\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":";;AAsBO,MAAM,2BAAA,GAA8B,CACzC,IAAA,EACA,UAAA,KACuB;AACvB,EAAA,IAAI,IAAA,CAAK,QAAA,KAAa,UAAA,EAAY,OAAO,MAAA;AAEzC,EAAA,MAAM,QAAQ,IAAA,CAAK,cAAA;AAEnB,EAAA,IAAI,KAAA,EAAO,IAAA,KAAS,cAAA,CAAe,WAAA,EAAa,OAAO,MAAA;AAEvD,EAAA,MAAM,CAAC,IAAA,EAAM,IAAA,EAAM,GAAG,IAAI,IAAI,KAAA,CAAM,YAAA;AAEpC,EAAA,IAAI,SAAS,MAAA,IAAa,IAAA,KAAS,MAAA,IAAa,IAAA,CAAK,SAAS,CAAA,EAAG;AAC/D,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,IACE,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,UAAA,IAC7B,KAAK,IAAA,KAAS,cAAA,CAAe,cAAA,IAC7B,IAAA,CAAK,SAAS,cAAA,CAAe,kBAAA,IAC7B,IAAA,CAAK,IAAA,KAAS,eAAe,UAAA,EAC7B;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,MAAM,eAAA,GAAkB,mBAAA,CAAoB,IAAA,CAAK,cAAc,CAAA;AAE/D,EAAA,IAAI,eAAA,KAAoB,QAAW,OAAO,MAAA;AAE1C,EAAA,MAAM,QAAA,GAAW,UAAA,CAAW,OAAA,CAAQ,IAAI,CAAA;AAExC,EAAA,OAAO,mBAAA,CAAoB,QAAQ,CAAA,KACjC,mBAAA,CAAoB,WAAW,OAAA,CAAQ,eAAe,CAAC,CAAA,GACrD,QAAA,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
+ {"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 NON_EMPTY_ARRAY_TYPE_NAME = "NonEmptyArray";
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"}
@@ -1,5 +1,11 @@
1
1
  const TS_TYPE_FORGE_MODULE = "ts-type-forge";
2
- const NON_EMPTY_ARRAY_TYPE_NAME = "NonEmptyArray";
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 { NON_EMPTY_ARRAY_TYPE_NAME, TS_TYPE_FORGE_MODULE };
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 NON_EMPTY_ARRAY_TYPE_NAME = 'NonEmptyArray';\n"],"names":[],"mappings":"AAAO,MAAM,oBAAA,GAAuB;AAE7B,MAAM,yBAAA,GAA4B;;;;"}
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;;;;"}
@@ -1,6 +1,7 @@
1
1
  export declare const tsTypeForgeRules: {
2
- readonly 'prefer-non-empty-array': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useNonEmptyArray", readonly [(Readonly<{
3
- importStyle?: "global" | "named";
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;;;;CAEa,CAAC"}
1
+ {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAGA,eAAO,MAAM,gBAAgB;;;;;CAGa,CAAC"}
@@ -1,7 +1,7 @@
1
- import { preferNonEmptyArray } from './prefer-non-empty-array.mjs';
1
+ import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
2
2
 
3
3
  const tsTypeForgeRules = {
4
- "prefer-non-empty-array": preferNonEmptyArray
4
+ "prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple
5
5
  };
6
6
 
7
7
  export { tsTypeForgeRules };
@@ -1 +1 @@
1
- {"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferNonEmptyArray } from './prefer-non-empty-array.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-non-empty-array': preferNonEmptyArray,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;AAGO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,wBAAA,EAA0B;AAC5B;;;;"}
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"}