eslint-plugin-ts-fortress 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +68 -24
  2. package/dist/rule-types.d.mts +3 -3
  3. package/dist/rule-types.d.mts.map +1 -1
  4. package/dist/rules/constants.d.mts +13 -0
  5. package/dist/rules/constants.d.mts.map +1 -1
  6. package/dist/rules/constants.mjs +7 -1
  7. package/dist/rules/constants.mjs.map +1 -1
  8. package/dist/rules/import-utils.d.mts +15 -8
  9. package/dist/rules/import-utils.d.mts.map +1 -1
  10. package/dist/rules/import-utils.mjs +13 -24
  11. package/dist/rules/import-utils.mjs.map +1 -1
  12. package/dist/rules/prefer-canonical-length-constrained-type.d.mts +6 -0
  13. package/dist/rules/prefer-canonical-length-constrained-type.d.mts.map +1 -0
  14. package/dist/rules/prefer-canonical-length-constrained-type.mjs +169 -0
  15. package/dist/rules/prefer-canonical-length-constrained-type.mjs.map +1 -0
  16. package/dist/rules/rules.d.mts +1 -1
  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/package.json +1 -1
  21. package/src/rule-types.mts +3 -3
  22. package/src/rules/constants.mts +19 -0
  23. package/src/rules/import-utils.mts +50 -62
  24. package/src/rules/prefer-canonical-length-constrained-type.mts +360 -0
  25. package/src/rules/prefer-canonical-length-constrained-type.test.mts +435 -0
  26. package/src/rules/rules.mts +3 -2
  27. package/dist/rules/prefer-non-empty-array.d.mts +0 -6
  28. package/dist/rules/prefer-non-empty-array.d.mts.map +0 -1
  29. package/dist/rules/prefer-non-empty-array.mjs +0 -100
  30. package/dist/rules/prefer-non-empty-array.mjs.map +0 -1
  31. package/src/rules/prefer-non-empty-array.mts +0 -190
  32. package/src/rules/prefer-non-empty-array.test.mts +0 -222
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ESLint rules that steer schema definitions toward
4
4
  [`ts-fortress`](https://www.npmjs.com/package/ts-fortress) idioms. Every rule is
5
- auto-fixable.
5
+ auto-fixable, and every rewrite is **type-preserving**.
6
6
 
7
7
  ## Installation
8
8
 
@@ -26,7 +26,7 @@ export default [
26
26
  {
27
27
  plugins: { 'ts-fortress': eslintPluginTsFortress },
28
28
  rules: {
29
- 'ts-fortress/prefer-non-empty-array': 'error',
29
+ 'ts-fortress/prefer-canonical-length-constrained-type': 'error',
30
30
  } satisfies Partial<EslintTsFortressRules>,
31
31
  },
32
32
  ];
@@ -34,43 +34,87 @@ export default [
34
34
 
35
35
  ## Rules
36
36
 
37
- | Rule | Description |
38
- | ------------------------ | ------------------------------------------------------- |
39
- | `prefer-non-empty-array` | Replace `minLengthArray(1, …)` with `nonEmptyArray(…)`. |
37
+ | Rule | Description |
38
+ | :----------------------------------------- | :-------------------------------------------------------------------------------------------- |
39
+ | `prefer-canonical-length-constrained-type` | Normalize a length-constrained array combinator with degenerate bounds to its canonical form. |
40
40
 
41
- ### `prefer-non-empty-array`
41
+ ### `prefer-canonical-length-constrained-type`
42
42
 
43
- `t.minLengthArray(1, x)` and `t.nonEmptyArray(x)` build the **same type** —
44
- `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` — so the rewrite is
45
- type-preserving. `nonEmptyArray` states the intent directly and, when no
46
- explicit `typeName` is given, reports the clearer `NonEmptyArray<…>` instead of
47
- `MinLengthArray<1, …>` in validation errors.
43
+ Several of ts-fortress's array combinators build the very same type once their
44
+ length arguments hit a degenerate value. The rule rewrites each of those to the
45
+ combinator that names the constraint directly:
46
+
47
+ | ❌ written as | ✅ canonical form | why they are the same type |
48
+ | :---------------------------- | :----------------------- | :----------------------------------------------------------------- |
49
+ | `minLengthArray(1, x)` | `nonEmptyArray(x)` | `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` |
50
+ | `minLengthTuple(0, x)` | `array(x)` | `MinLengthTuple<0, A>` is `readonly A[]` — no constraint at all |
51
+ | `maxLengthTuple(0, x)` | `fixedLengthTuple(0, x)` | both are `readonly []` |
52
+ | `boundedLengthTuple(n, n, x)` | `fixedLengthTuple(n, x)` | the length union collapses to its single member |
53
+ | `boundedLengthTuple(0, n, x)` | `maxLengthTuple(n, x)` | `MaxLengthTuple<N, A>` is defined as `BoundedLengthTuple<0, N, A>` |
48
54
 
49
55
  ```ts
50
56
  import * as t from 'ts-fortress';
51
57
 
52
58
  // ❌
53
59
  const Tags = t.minLengthArray(1, t.string());
60
+ const Rgb = t.boundedLengthTuple(3, 3, t.number());
61
+ const Page = t.boundedLengthTuple(0, 20, t.string());
54
62
 
55
63
  // ✅
56
64
  const Tags = t.nonEmptyArray(t.string());
65
+ const Rgb = t.fixedLengthTuple(3, t.number());
66
+ const Page = t.maxLengthTuple(20, t.string());
57
67
  ```
58
68
 
69
+ Each rewrite keeps the accepted values, the `defaultValue`, and the options
70
+ object exactly as they were. The only observable change is the default
71
+ `typeName` — and the `details.kind` of the length error derived from it — which
72
+ becomes the one that names the constraint actually being checked.
73
+
74
+ #### Why the branded `*Array` family is left alone
75
+
76
+ `boundedLengthArray(0, n, x)` and `boundedLengthArray(n, n, x)` look like the
77
+ same degenerate cases, but their types are branded rather than structural, and
78
+ the analogous rewrites would silently change them:
79
+
80
+ - `BoundedLengthArray<Min, Max, A>` is
81
+ `MaxLengthArray<Max, A> & MinLengthArray<Min, A>`, so rewriting
82
+ `boundedLengthArray(0, n, x)` to `maxLengthArray(n, x)` **drops** the
83
+ `MinLengthArray<0, A>` brand — a widening.
84
+ - `FixedLengthArray<N, A>` additionally intersects the exact tuple
85
+ `FixedLengthTuple<N, A>` for `N <= 10`, so rewriting
86
+ `boundedLengthArray(n, n, x)` to `fixedLengthArray(n, x)` **adds** a
87
+ constraint — a narrowing.
88
+
89
+ Neither is a pure rename, so the rule does not report them.
90
+
91
+ #### Bounds above the structural cap
92
+
93
+ The `*Tuple` combinators only encode lengths up to `10` (ts-type-forge's
94
+ `StructuralPrefixLength`); past that they fall back to an overload that drops
95
+ the constraint from the result type. The rule therefore only fires when the
96
+ bound it carries over to the other combinator is a literal within `0..10`.
97
+
98
+ #### Imports
99
+
59
100
  Both the namespace style (`import * as t from 'ts-fortress'`) and named imports
60
101
  — including aliases — are recognized, and the autofix reuses whatever binding
61
- the file already has, adding `import { nonEmptyArray } from 'ts-fortress';`
62
- only when a named call needs it.
63
-
64
- The rule deliberately leaves alone:
65
-
66
- - other minimum lengths (`minLengthArray(2, …)` is `MinLengthArray<2, …>`);
67
- - `minLengthTuple(1, …)`, which produces the structural
68
- `readonly [A, ...A[]]` tuple rather than the branded `NonEmptyArray`;
69
- - calls passing a `defaultValue` option, because `nonEmptyArray` types it as
70
- `NonEmptyTuple<A>` while `minLengthArray` types it as the branded
71
- `MinLengthArray<1, A>` — a blind rewrite could stop type-checking. A
72
- `typeName`-only options object is safe and is rewritten;
73
- - files that already bind the name `nonEmptyArray` to something else.
102
+ the file already has, adding `import { … } from 'ts-fortress';` only when a
103
+ named call needs one.
104
+
105
+ #### The rule deliberately leaves alone
106
+
107
+ - non-degenerate bounds (`minLengthArray(2, …)`, `boundedLengthTuple(1, 3, …)`);
108
+ - computed bounds and calls with explicit type arguments, since the literal is
109
+ what makes the two forms equivalent;
110
+ - `minLengthArray(1, …)` calls passing a `defaultValue`, because
111
+ `nonEmptyArray` types that option as `NonEmptyTuple<A>` while
112
+ `minLengthArray` types it as the branded `MinLengthArray<1, A>` — a blind
113
+ rewrite could stop type-checking. A `typeName`-only options object is safe
114
+ and is rewritten. (The other rewrites type `defaultValue` identically, so
115
+ they keep it.)
116
+ - calls where the target name is already bound to something else at the call
117
+ site.
74
118
 
75
119
  ## License
76
120
 
@@ -2,7 +2,7 @@ import { type Linter } from 'eslint';
2
2
  import { type TypeEq } from 'ts-type-forge';
3
3
  import { type tsFortressRules } from './rules/index.mjs';
4
4
  /**
5
- * @description Replace `minLengthArray(1, ...)` with `nonEmptyArray(...)` from ts-fortress.
5
+ * @description Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.
6
6
  *
7
7
  * ```md
8
8
  * | key | value |
@@ -12,11 +12,11 @@ import { type tsFortressRules } from './rules/index.mjs';
12
12
  * | fixable | code |
13
13
  * ```
14
14
  */
15
- declare namespace PreferNonEmptyArray {
15
+ declare namespace PreferCanonicalLengthConstrainedType {
16
16
  type RuleEntry = Linter.StringSeverity;
17
17
  }
18
18
  export type EslintTsFortressRules = Readonly<{
19
- 'ts-fortress/prefer-non-empty-array': PreferNonEmptyArray.RuleEntry;
19
+ 'ts-fortress/prefer-canonical-length-constrained-type': PreferCanonicalLengthConstrainedType.RuleEntry;
20
20
  }>;
21
21
  export type EslintTsFortressRulesOption = Readonly<Record<never, never>>;
22
22
  type AssertTrue<B extends true> = B;
@@ -1 +1 @@
1
- {"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,kBAAU,mBAAmB,CAAC;IAC5B,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,oCAAoC,EAAE,mBAAmB,CAAC,SAAS,CAAC;CACrE,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAIzE,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,qBAAqB,EAC3B,eAAe,MAAM,OAAO,eAAe,EAAE,CAC9C,CACF,CAAC"}
1
+ {"version":3,"file":"rule-types.d.mts","sourceRoot":"","sources":["../src/rule-types.mts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AACrC,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,kBAAU,oCAAoC,CAAC;IAC7C,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,sDAAsD,EAAE,oCAAoC,CAAC,SAAS,CAAC;CACxG,CAAC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAIzE,KAAK,UAAU,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAEpC,MAAM,MAAM,kBAAkB,GAAG,UAAU,CACzC,MAAM,CACJ,MAAM,qBAAqB,EAC3B,eAAe,MAAM,OAAO,eAAe,EAAE,CAC9C,CACF,CAAC"}
@@ -1,4 +1,17 @@
1
1
  export declare const TS_FORTRESS_MODULE = "ts-fortress";
2
+ export declare const ARRAY_FN = "array";
3
+ export declare const BOUNDED_LENGTH_TUPLE_FN = "boundedLengthTuple";
4
+ export declare const FIXED_LENGTH_TUPLE_FN = "fixedLengthTuple";
5
+ export declare const MAX_LENGTH_TUPLE_FN = "maxLengthTuple";
2
6
  export declare const MIN_LENGTH_ARRAY_FN = "minLengthArray";
7
+ export declare const MIN_LENGTH_TUPLE_FN = "minLengthTuple";
3
8
  export declare const NON_EMPTY_ARRAY_FN = "nonEmptyArray";
9
+ /**
10
+ * Inclusive upper bound of ts-type-forge's `StructuralPrefixLength` (`0..10`) —
11
+ * the lengths the structural `*Tuple` combinators encode in their result type.
12
+ * A bound outside this range selects a different overload (one that drops the
13
+ * constraint), so a rewrite that carries a bound over to another combinator
14
+ * only stays type-identical while the bound is within it.
15
+ */
16
+ export declare const STRUCTURAL_PREFIX_CAP = 10;
4
17
  //# 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,kBAAkB,gBAAgB,CAAC;AAEhD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD,eAAO,MAAM,kBAAkB,kBAAkB,CAAC"}
1
+ {"version":3,"file":"constants.d.mts","sourceRoot":"","sources":["../../src/rules/constants.mts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,gBAAgB,CAAC;AAEhD,eAAO,MAAM,QAAQ,UAAU,CAAC;AAEhC,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC;AAEpD,eAAO,MAAM,kBAAkB,kBAAkB,CAAC;AAElD;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,KAAK,CAAC"}
@@ -1,6 +1,12 @@
1
1
  const TS_FORTRESS_MODULE = "ts-fortress";
2
+ const ARRAY_FN = "array";
3
+ const BOUNDED_LENGTH_TUPLE_FN = "boundedLengthTuple";
4
+ const FIXED_LENGTH_TUPLE_FN = "fixedLengthTuple";
5
+ const MAX_LENGTH_TUPLE_FN = "maxLengthTuple";
2
6
  const MIN_LENGTH_ARRAY_FN = "minLengthArray";
7
+ const MIN_LENGTH_TUPLE_FN = "minLengthTuple";
3
8
  const NON_EMPTY_ARRAY_FN = "nonEmptyArray";
9
+ const STRUCTURAL_PREFIX_CAP = 10;
4
10
 
5
- export { MIN_LENGTH_ARRAY_FN, NON_EMPTY_ARRAY_FN, TS_FORTRESS_MODULE };
11
+ export { ARRAY_FN, BOUNDED_LENGTH_TUPLE_FN, FIXED_LENGTH_TUPLE_FN, MAX_LENGTH_TUPLE_FN, MIN_LENGTH_ARRAY_FN, MIN_LENGTH_TUPLE_FN, NON_EMPTY_ARRAY_FN, STRUCTURAL_PREFIX_CAP, TS_FORTRESS_MODULE };
6
12
  //# sourceMappingURL=constants.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_FORTRESS_MODULE = 'ts-fortress';\n\nexport const MIN_LENGTH_ARRAY_FN = 'minLengthArray';\n\nexport const NON_EMPTY_ARRAY_FN = 'nonEmptyArray';\n"],"names":[],"mappings":"AAAO,MAAM,kBAAA,GAAqB;AAE3B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,kBAAA,GAAqB;;;;"}
1
+ {"version":3,"file":"constants.mjs","sources":["../../src/rules/constants.mts"],"sourcesContent":["export const TS_FORTRESS_MODULE = 'ts-fortress';\n\nexport const ARRAY_FN = 'array';\n\nexport const BOUNDED_LENGTH_TUPLE_FN = 'boundedLengthTuple';\n\nexport const FIXED_LENGTH_TUPLE_FN = 'fixedLengthTuple';\n\nexport const MAX_LENGTH_TUPLE_FN = 'maxLengthTuple';\n\nexport const MIN_LENGTH_ARRAY_FN = 'minLengthArray';\n\nexport const MIN_LENGTH_TUPLE_FN = 'minLengthTuple';\n\nexport const NON_EMPTY_ARRAY_FN = 'nonEmptyArray';\n\n/**\n * Inclusive upper bound of ts-type-forge's `StructuralPrefixLength` (`0..10`) —\n * the lengths the structural `*Tuple` combinators encode in their result type.\n * A bound outside this range selects a different overload (one that drops the\n * constraint), so a rewrite that carries a bound over to another combinator\n * only stays type-identical while the bound is within it.\n */\nexport const STRUCTURAL_PREFIX_CAP = 10;\n"],"names":[],"mappings":"AAAO,MAAM,kBAAA,GAAqB;AAE3B,MAAM,QAAA,GAAW;AAEjB,MAAM,uBAAA,GAA0B;AAEhC,MAAM,qBAAA,GAAwB;AAE9B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,mBAAA,GAAsB;AAE5B,MAAM,kBAAA,GAAqB;AAS3B,MAAM,qBAAA,GAAwB;;;;"}
@@ -25,16 +25,23 @@ export declare const getImportedLocalName: (importDeclarations: readonly TSESTre
25
25
  */
26
26
  export declare const buildCalleeResolver: (importDeclarations: readonly TSESTree.ImportDeclaration[]) => ((callee: TSESTree.Expression) => ResolvedCallee | undefined);
27
27
  /**
28
- * Whether the file already binds `name` to something that is not the
29
- * ts-fortress export of the same name — a local declaration or an import from
30
- * another module. Autofixes that introduce `name` must bail out in that case,
28
+ * Whether `name` resolves, at `node`, to a binding that is not a ts-fortress
29
+ * import — a local declaration, a parameter, or an import from another module.
30
+ * An autofix that makes a call reference `name` must bail out in that case,
31
31
  * because the rewritten call would resolve to the wrong binding.
32
+ *
33
+ * Scope-based rather than a scan of the top-level statements, so a binding that
34
+ * merely shadows the ts-fortress one around the call site is caught too.
32
35
  */
33
- export declare const hasConflictingDeclaration: (program: TSESTree.Program, name: string) => boolean;
36
+ export declare const hasForeignBinding: (sourceCode: TSESLint.SourceCode, node: TSESTree.Node, name: string) => boolean;
34
37
  /**
35
- * Inserts `import { <name> } from 'ts-fortress';` at the top of the file.
36
- * Placement is deliberately naive: prettier-plugin-organize-imports (or any
37
- * equivalent) merges and sorts it afterwards.
38
+ * Inserts `import { <name> } from 'ts-fortress';` for each name at the top of
39
+ * the file. Placement is deliberately naive: prettier-plugin-organize-imports
40
+ * (or any equivalent) merges and sorts them afterwards.
41
+ *
42
+ * All names go into a single fix on purpose: ESLint merges the fixes of one
43
+ * report into one edit span, so two reports each inserting at the start of the
44
+ * file would conflict and one of them would be dropped.
38
45
  */
39
- export declare const buildImportFix: (fixer: TSESLint.RuleFixer, program: TSESTree.Program, name: string) => readonly TSESLint.RuleFix[];
46
+ export declare const buildImportsFix: (fixer: TSESLint.RuleFixer, program: TSESTree.Program, names: readonly string[]) => readonly TSESLint.RuleFix[];
40
47
  //# sourceMappingURL=import-utils.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,YACtB,QAAQ,CAAC,OAAO,KACxB,SAAS,QAAQ,CAAC,iBAAiB,EAKnC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uBACX,SAAS,QAAQ,CAAC,iBAAiB,EAAE,gBAC3C,MAAM,KACnB,MAAM,GAAG,SASK,CAAC;AAElB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,uBACV,SAAS,QAAQ,CAAC,iBAAiB,EAAE,KACxD,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,KAAK,cAAc,GAAG,SAAS,CAkD9D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,YAC3B,QAAQ,CAAC,OAAO,QACnB,MAAM,KACX,OAgBC,CAAC;AAEL;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAClB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,QACnB,MAAM,KACX,SAAS,QAAQ,CAAC,OAAO,EAMhB,CAAC"}
1
+ {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC,MAAM,MAAM,cAAc,GAAG,QAAQ,CAAC;IACpC,4EAA4E;IAC5E,aAAa,EAAE,MAAM,CAAC;IACtB,qEAAqE;IACrE,YAAY,EAAE,QAAQ,CAAC,IAAI,CAAC;IAC5B,2EAA2E;IAC3E,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,qEAAqE;AACrE,eAAO,MAAM,oBAAoB,YACtB,QAAQ,CAAC,OAAO,KACxB,SAAS,QAAQ,CAAC,iBAAiB,EAKnC,CAAC;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,uBACX,SAAS,QAAQ,CAAC,iBAAiB,EAAE,gBAC3C,MAAM,KACnB,MAAM,GAAG,SASK,CAAC;AAElB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,uBACV,SAAS,QAAQ,CAAC,iBAAiB,EAAE,KACxD,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,UAAU,KAAK,cAAc,GAAG,SAAS,CAkD9D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,eAChB,QAAQ,CAAC,UAAU,QACzB,QAAQ,CAAC,IAAI,QACb,MAAM,KACX,OAgBF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,eAAe,UACnB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,SAClB,SAAS,MAAM,EAAE,KACvB,SAAS,QAAQ,CAAC,OAAO,EAUX,CAAC"}
@@ -35,34 +35,23 @@ const buildCalleeResolver = (importDeclarations) => {
35
35
  } : void 0;
36
36
  };
37
37
  };
38
- const hasConflictingDeclaration = (program, name) => program.body.some((node) => {
39
- if (node.type === AST_NODE_TYPES.ImportDeclaration) {
40
- return node.source.value !== TS_FORTRESS_MODULE && node.specifiers.some((specifier) => specifier.local.name === name);
38
+ const hasForeignBinding = (sourceCode, node, name) => {
39
+ for (let mut_scope = sourceCode.getScope(node); mut_scope !== null; mut_scope = mut_scope.upper) {
40
+ const variable = mut_scope.set.get(name);
41
+ if (variable !== void 0) {
42
+ return variable.defs.some((def) => !isTsFortressImportDef(def.node));
43
+ }
41
44
  }
42
- return declaresName(
43
- node.type === AST_NODE_TYPES.ExportNamedDeclaration || node.type === AST_NODE_TYPES.ExportDefaultDeclaration ? node.declaration : node,
44
- name
45
- );
46
- });
47
- const buildImportFix = (fixer, program, name) => [
45
+ return false;
46
+ };
47
+ const buildImportsFix = (fixer, program, names) => names.length === 0 ? [] : [
48
48
  fixer.insertTextBefore(
49
49
  program,
50
- `import { ${name} } from '${TS_FORTRESS_MODULE}';
51
- `
50
+ names.map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';
51
+ `).join("")
52
52
  )
53
53
  ];
54
- const declaresName = (node, name) => {
55
- if (node === null || node === void 0) return false;
56
- if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration || node.type === AST_NODE_TYPES.TSInterfaceDeclaration || node.type === AST_NODE_TYPES.TSEnumDeclaration) {
57
- return node.id.name === name;
58
- }
59
- if (node.type === AST_NODE_TYPES.ClassDeclaration || node.type === AST_NODE_TYPES.FunctionDeclaration) {
60
- return node.id?.name === name;
61
- }
62
- return node.type === AST_NODE_TYPES.VariableDeclaration && node.declarations.some(
63
- (declarator) => declarator.id.type === AST_NODE_TYPES.Identifier && declarator.id.name === name
64
- );
65
- };
54
+ const isTsFortressImportDef = (node) => (node.type === AST_NODE_TYPES.ImportSpecifier || node.type === AST_NODE_TYPES.ImportDefaultSpecifier || node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) && node.parent.type === AST_NODE_TYPES.ImportDeclaration && node.parent.source.value === TS_FORTRESS_MODULE;
66
55
 
67
- export { buildCalleeResolver, buildImportFix, getImportedLocalName, getTsFortressImports, hasConflictingDeclaration };
56
+ export { buildCalleeResolver, buildImportsFix, getImportedLocalName, getTsFortressImports, hasForeignBinding };
68
57
  //# sourceMappingURL=import-utils.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { TS_FORTRESS_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type ResolvedCallee = Readonly<{\n /** The canonical (imported) ts-fortress name, regardless of local alias. */\n canonicalName: string;\n /** The identifier node to rewrite when replacing the callee name. */\n propertyNode: TSESTree.Node;\n /** Whether the call was made through a namespace import (`t.foo(...)`). */\n isNamespace: boolean;\n}>;\n\n/** Every `import ... from 'ts-fortress'` declaration in the file. */\nexport const getTsFortressImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_FORTRESS_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-fortress export is imported as\n * (handling aliases such as `import { nonEmptyArray as nea }` → `'nea'`), or\n * `undefined` when it is not imported. Used so autofixes reference the binding\n * that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Builds a resolver that maps a call expression's callee to the canonical\n * ts-fortress function it refers to. Handles named imports (including aliases)\n * and namespace imports (`import * as t` → `t.foo(...)`) — the latter being the\n * idiomatic way to consume ts-fortress. Returns `undefined` for any callee that\n * does not reference the given ts-fortress imports.\n */\nexport const buildCalleeResolver = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n): ((callee: TSESTree.Expression) => ResolvedCallee | undefined) => {\n const specifiers = importDeclarations.flatMap(\n (declaration) => declaration.specifiers,\n );\n\n const namespaceNames = new Set(\n specifiers\n .filter(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,\n )\n .map((specifier) => specifier.local.name),\n );\n\n const localToCanonical = new Map<string, string>(\n specifiers.flatMap((specifier) => {\n if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) return [];\n\n const importedName =\n specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value;\n\n return typeof importedName === 'string'\n ? [[specifier.local.name, importedName] as const]\n : [];\n }),\n );\n\n return (callee) => {\n if (callee.type === AST_NODE_TYPES.Identifier) {\n const canonicalName = localToCanonical.get(callee.name);\n\n return canonicalName === undefined\n ? undefined\n : { canonicalName, propertyNode: callee, isNamespace: false };\n }\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n namespaceNames.has(callee.object.name) &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? {\n canonicalName: callee.property.name,\n propertyNode: callee.property,\n isNamespace: true,\n }\n : undefined;\n };\n};\n\n/**\n * Whether the file already binds `name` to something that is not the\n * ts-fortress export of the same name — a local declaration or an import from\n * another module. Autofixes that introduce `name` must bail out in that case,\n * because the rewritten call would resolve to the wrong binding.\n */\nexport const hasConflictingDeclaration = (\n program: TSESTree.Program,\n name: string,\n): boolean =>\n program.body.some((node) => {\n if (node.type === AST_NODE_TYPES.ImportDeclaration) {\n return (\n node.source.value !== TS_FORTRESS_MODULE &&\n node.specifiers.some((specifier) => specifier.local.name === name)\n );\n }\n\n return declaresName(\n node.type === AST_NODE_TYPES.ExportNamedDeclaration ||\n node.type === AST_NODE_TYPES.ExportDefaultDeclaration\n ? node.declaration\n : node,\n name,\n );\n });\n\n/**\n * Inserts `import { <name> } from 'ts-fortress';` at the top of the file.\n * Placement is deliberately naive: prettier-plugin-organize-imports (or any\n * equivalent) merges and sorts it afterwards.\n */\nexport const buildImportFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n name: string,\n): readonly TSESLint.RuleFix[] =>\n [\n fixer.insertTextBefore(\n program,\n `import { ${name} } from '${TS_FORTRESS_MODULE}';\\n`,\n ),\n ] as const;\n\nconst declaresName = (\n node: TSESTree.Node | null | undefined,\n name: string,\n): boolean => {\n if (node === null || node === undefined) return false;\n\n if (\n node.type === AST_NODE_TYPES.TSTypeAliasDeclaration ||\n node.type === AST_NODE_TYPES.TSInterfaceDeclaration ||\n node.type === AST_NODE_TYPES.TSEnumDeclaration\n ) {\n return node.id.name === name;\n }\n\n if (\n node.type === AST_NODE_TYPES.ClassDeclaration ||\n node.type === AST_NODE_TYPES.FunctionDeclaration\n ) {\n return node.id?.name === name;\n }\n\n return (\n node.type === AST_NODE_TYPES.VariableDeclaration &&\n node.declarations.some(\n (declarator) =>\n declarator.id.type === AST_NODE_TYPES.Identifier &&\n declarator.id.name === name,\n )\n );\n};\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"import-utils.mjs","sources":["../../src/rules/import-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { TS_FORTRESS_MODULE } from './constants.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type ResolvedCallee = Readonly<{\n /** The canonical (imported) ts-fortress name, regardless of local alias. */\n canonicalName: string;\n /** The identifier node to rewrite when replacing the callee name. */\n propertyNode: TSESTree.Node;\n /** Whether the call was made through a namespace import (`t.foo(...)`). */\n isNamespace: boolean;\n}>;\n\n/** Every `import ... from 'ts-fortress'` declaration in the file. */\nexport const getTsFortressImports = (\n program: TSESTree.Program,\n): readonly TSESTree.ImportDeclaration[] =>\n program.body.filter(\n (node): node is TSESTree.ImportDeclaration =>\n node.type === AST_NODE_TYPES.ImportDeclaration &&\n node.source.value === TS_FORTRESS_MODULE,\n );\n\n/**\n * Returns the in-scope local name a given ts-fortress export is imported as\n * (handling aliases such as `import { nonEmptyArray as nea }` → `'nea'`), or\n * `undefined` when it is not imported. Used so autofixes reference the binding\n * that actually exists rather than the canonical name.\n */\nexport const getImportedLocalName = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n importedName: string,\n): string | undefined =>\n importDeclarations\n .flatMap((declaration) => declaration.specifiers)\n .find(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportSpecifier &&\n (specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value) === importedName,\n )?.local.name;\n\n/**\n * Builds a resolver that maps a call expression's callee to the canonical\n * ts-fortress function it refers to. Handles named imports (including aliases)\n * and namespace imports (`import * as t` → `t.foo(...)`) — the latter being the\n * idiomatic way to consume ts-fortress. Returns `undefined` for any callee that\n * does not reference the given ts-fortress imports.\n */\nexport const buildCalleeResolver = (\n importDeclarations: readonly TSESTree.ImportDeclaration[],\n): ((callee: TSESTree.Expression) => ResolvedCallee | undefined) => {\n const specifiers = importDeclarations.flatMap(\n (declaration) => declaration.specifiers,\n );\n\n const namespaceNames = new Set(\n specifiers\n .filter(\n (specifier) =>\n specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier,\n )\n .map((specifier) => specifier.local.name),\n );\n\n const localToCanonical = new Map<string, string>(\n specifiers.flatMap((specifier) => {\n if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) return [];\n\n const importedName =\n specifier.imported.type === AST_NODE_TYPES.Identifier\n ? specifier.imported.name\n : specifier.imported.value;\n\n return typeof importedName === 'string'\n ? [[specifier.local.name, importedName] as const]\n : [];\n }),\n );\n\n return (callee) => {\n if (callee.type === AST_NODE_TYPES.Identifier) {\n const canonicalName = localToCanonical.get(callee.name);\n\n return canonicalName === undefined\n ? undefined\n : { canonicalName, propertyNode: callee, isNamespace: false };\n }\n\n return callee.type === AST_NODE_TYPES.MemberExpression &&\n !callee.computed &&\n callee.object.type === AST_NODE_TYPES.Identifier &&\n namespaceNames.has(callee.object.name) &&\n callee.property.type === AST_NODE_TYPES.Identifier\n ? {\n canonicalName: callee.property.name,\n propertyNode: callee.property,\n isNamespace: true,\n }\n : undefined;\n };\n};\n\n/**\n * Whether `name` resolves, at `node`, to a binding that is not a ts-fortress\n * import — a local declaration, a parameter, or an import from another module.\n * An autofix that makes a call reference `name` must bail out in that case,\n * because the rewritten call would resolve to the wrong binding.\n *\n * Scope-based rather than a scan of the top-level statements, so a binding that\n * merely shadows the ts-fortress one around the call site is caught too.\n */\nexport const hasForeignBinding = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n for (\n let mut_scope: TSESLint.Scope.Scope | null = sourceCode.getScope(node);\n mut_scope !== null;\n mut_scope = mut_scope.upper\n ) {\n const variable = mut_scope.set.get(name);\n\n // The innermost binding wins, so the first scope that declares `name`\n // decides — an outer ts-fortress import does not rescue a shadowed name.\n if (variable !== undefined) {\n return variable.defs.some((def) => !isTsFortressImportDef(def.node));\n }\n }\n\n return false;\n};\n\n/**\n * Inserts `import { <name> } from 'ts-fortress';` for each name at the top of\n * the file. Placement is deliberately naive: prettier-plugin-organize-imports\n * (or any equivalent) merges and sorts them afterwards.\n *\n * All names go into a single fix on purpose: ESLint merges the fixes of one\n * report into one edit span, so two reports each inserting at the start of the\n * file would conflict and one of them would be dropped.\n */\nexport const buildImportsFix = (\n fixer: TSESLint.RuleFixer,\n program: TSESTree.Program,\n names: readonly string[],\n): readonly TSESLint.RuleFix[] =>\n names.length === 0\n ? ([] as const)\n : ([\n fixer.insertTextBefore(\n program,\n names\n .map((name) => `import { ${name} } from '${TS_FORTRESS_MODULE}';\\n`)\n .join(''),\n ),\n ] as const);\n\nconst isTsFortressImportDef = (node: TSESTree.Node): boolean =>\n (node.type === AST_NODE_TYPES.ImportSpecifier ||\n node.type === AST_NODE_TYPES.ImportDefaultSpecifier ||\n node.type === AST_NODE_TYPES.ImportNamespaceSpecifier) &&\n node.parent.type === AST_NODE_TYPES.ImportDeclaration &&\n node.parent.source.value === TS_FORTRESS_MODULE;\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type TSESLint } from '@typescript-eslint/utils';
2
+ type Options = readonly [];
3
+ type MessageIds = 'useCanonicalType';
4
+ export declare const preferCanonicalLengthConstrainedType: TSESLint.RuleModule<MessageIds, Options>;
5
+ export {};
6
+ //# sourceMappingURL=prefer-canonical-length-constrained-type.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-canonical-length-constrained-type.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-canonical-length-constrained-type.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAmBlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,kBAAkB,CAAC;AAyFrC,eAAO,MAAM,oCAAoC,EAAE,QAAQ,CAAC,UAAU,CACpE,UAAU,EACV,OAAO,CA+HC,CAAC"}
@@ -0,0 +1,169 @@
1
+ import { AST_NODE_TYPES } from '@typescript-eslint/utils';
2
+ import { NON_EMPTY_ARRAY_FN, MIN_LENGTH_ARRAY_FN, ARRAY_FN, MIN_LENGTH_TUPLE_FN, FIXED_LENGTH_TUPLE_FN, MAX_LENGTH_TUPLE_FN, BOUNDED_LENGTH_TUPLE_FN, STRUCTURAL_PREFIX_CAP } from './constants.mjs';
3
+ import { getTsFortressImports, buildCalleeResolver, buildImportsFix, getImportedLocalName, hasForeignBinding } from './import-utils.mjs';
4
+
5
+ const REWRITES = [
6
+ {
7
+ from: MIN_LENGTH_ARRAY_FN,
8
+ bounds: [1],
9
+ to: NON_EMPTY_ARRAY_FN,
10
+ keep: [],
11
+ blockedByDefaultValue: true
12
+ },
13
+ {
14
+ from: MIN_LENGTH_TUPLE_FN,
15
+ bounds: [0],
16
+ to: ARRAY_FN,
17
+ keep: [],
18
+ blockedByDefaultValue: false
19
+ },
20
+ {
21
+ from: MAX_LENGTH_TUPLE_FN,
22
+ bounds: [0],
23
+ to: FIXED_LENGTH_TUPLE_FN,
24
+ keep: [0],
25
+ blockedByDefaultValue: false
26
+ },
27
+ // Listed before the `0, n` entry so that `boundedLengthTuple(0, 0, x)`
28
+ // normalizes to `fixedLengthTuple(0, x)`, the more specific of the two.
29
+ {
30
+ from: BOUNDED_LENGTH_TUPLE_FN,
31
+ bounds: ["anyLength", "sameAsFirst"],
32
+ to: FIXED_LENGTH_TUPLE_FN,
33
+ keep: [0],
34
+ blockedByDefaultValue: false
35
+ },
36
+ {
37
+ from: BOUNDED_LENGTH_TUPLE_FN,
38
+ bounds: [0, "anyLength"],
39
+ to: MAX_LENGTH_TUPLE_FN,
40
+ keep: [1],
41
+ blockedByDefaultValue: false
42
+ }
43
+ ];
44
+ const preferCanonicalLengthConstrainedType = {
45
+ meta: {
46
+ type: "suggestion",
47
+ docs: {
48
+ description: "Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly."
49
+ },
50
+ fixable: "code",
51
+ schema: [],
52
+ messages: {
53
+ useCanonicalType: "Use `{{replacement}}(...)` instead of `{{original}}({{boundsText}}, ...)`: with these bounds both build exactly the same type, and `{{replacement}}` names the constraint directly (and reports it on validation failure)."
54
+ }
55
+ },
56
+ create: (context) => {
57
+ const sourceCode = context.sourceCode;
58
+ const program = sourceCode.ast;
59
+ const tsFortressImports = getTsFortressImports(program);
60
+ const resolveCallee = buildCalleeResolver(tsFortressImports);
61
+ const mut_matches = [];
62
+ return {
63
+ CallExpression: (node) => {
64
+ const resolved = resolveCallee(node.callee);
65
+ if (resolved === void 0) return;
66
+ const rewrite = REWRITES.find(
67
+ (candidate) => candidate.from === resolved.canonicalName && matchesRewrite(node, candidate)
68
+ );
69
+ if (rewrite === void 0) return;
70
+ if (resolved.isNamespace) {
71
+ mut_matches.push({
72
+ node,
73
+ propertyNode: resolved.propertyNode,
74
+ isNamespace: true,
75
+ rewrite,
76
+ localName: rewrite.to,
77
+ needsImport: false
78
+ });
79
+ return;
80
+ }
81
+ const importedLocalName = getImportedLocalName(
82
+ tsFortressImports,
83
+ rewrite.to
84
+ );
85
+ const localName = importedLocalName ?? rewrite.to;
86
+ mut_matches.push({
87
+ node,
88
+ propertyNode: resolved.propertyNode,
89
+ isNamespace: false,
90
+ rewrite,
91
+ localName: hasForeignBinding(sourceCode, node, localName) ? void 0 : localName,
92
+ needsImport: importedLocalName === void 0
93
+ });
94
+ },
95
+ "Program:exit": () => {
96
+ const reportable = mut_matches.filter(
97
+ (match) => match.localName !== void 0
98
+ );
99
+ const namesNeedingImport = Array.from(
100
+ new Set(
101
+ reportable.filter((match) => match.needsImport).map((match) => match.rewrite.to)
102
+ )
103
+ );
104
+ const importFixIndex = reportable.findIndex(
105
+ (match) => match.needsImport
106
+ );
107
+ for (const [
108
+ index,
109
+ { node, propertyNode, rewrite, localName }
110
+ ] of reportable.entries()) {
111
+ if (localName === void 0) continue;
112
+ context.report({
113
+ node,
114
+ messageId: "useCanonicalType",
115
+ data: {
116
+ original: rewrite.from,
117
+ replacement: localName,
118
+ boundsText: boundsText(node, rewrite, sourceCode)
119
+ },
120
+ fix: (fixer) => [
121
+ ...index === importFixIndex ? buildImportsFix(fixer, program, namesNeedingImport) : [],
122
+ fixer.replaceText(propertyNode, localName),
123
+ ...dropBoundsFix(fixer, node, rewrite)
124
+ ]
125
+ });
126
+ }
127
+ }
128
+ };
129
+ },
130
+ defaultOptions: []
131
+ };
132
+ const matchesRewrite = (node, rewrite) => {
133
+ const args = node.arguments;
134
+ const boundCount = rewrite.bounds.length;
135
+ if (args.length < boundCount + 1 || args.length > boundCount + 2 || // Explicit type arguments pin the bound independently of the literal, so
136
+ // dropping or moving the literal could change the resulting type.
137
+ node.typeArguments !== void 0 || args.some((arg) => arg.type === AST_NODE_TYPES.SpreadElement)) {
138
+ return false;
139
+ }
140
+ const bounds = args.slice(0, boundCount).map(getLengthLiteral);
141
+ const [first] = bounds;
142
+ const boundsMatch = rewrite.bounds.every((matcher, index) => {
143
+ const value = bounds[index];
144
+ if (value === void 0) return false;
145
+ switch (matcher) {
146
+ case "anyLength":
147
+ return value <= STRUCTURAL_PREFIX_CAP;
148
+ case "sameAsFirst":
149
+ return value === first && value <= STRUCTURAL_PREFIX_CAP;
150
+ default:
151
+ return value === matcher;
152
+ }
153
+ });
154
+ return boundsMatch && (!rewrite.blockedByDefaultValue || !passesDefaultValue(args[boundCount + 1]));
155
+ };
156
+ const boundsText = (node, rewrite, sourceCode) => node.arguments.slice(0, rewrite.bounds.length).map((arg) => sourceCode.getText(arg)).join(", ");
157
+ const dropBoundsFix = (fixer, node, rewrite) => rewrite.bounds.flatMap((_, index) => {
158
+ if (rewrite.keep.includes(index)) return [];
159
+ const dropped = node.arguments[index];
160
+ const next = node.arguments[index + 1];
161
+ return dropped === void 0 || next === void 0 ? [] : [fixer.removeRange([dropped.range[0], next.range[0]])];
162
+ });
163
+ const getLengthLiteral = (node) => node.type === AST_NODE_TYPES.Literal && typeof node.value === "number" && Number.isSafeInteger(node.value) && node.value >= 0 ? node.value : void 0;
164
+ const passesDefaultValue = (node) => node !== void 0 && (node.type !== AST_NODE_TYPES.ObjectExpression || node.properties.some(
165
+ (property) => property.type !== AST_NODE_TYPES.Property || property.computed || property.key.type !== AST_NODE_TYPES.Identifier || property.key.name === "defaultValue"
166
+ ));
167
+
168
+ export { preferCanonicalLengthConstrainedType };
169
+ //# sourceMappingURL=prefer-canonical-length-constrained-type.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-canonical-length-constrained-type.mjs","sources":["../../src/rules/prefer-canonical-length-constrained-type.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport {\n ARRAY_FN,\n BOUNDED_LENGTH_TUPLE_FN,\n FIXED_LENGTH_TUPLE_FN,\n MAX_LENGTH_TUPLE_FN,\n MIN_LENGTH_ARRAY_FN,\n MIN_LENGTH_TUPLE_FN,\n NON_EMPTY_ARRAY_FN,\n STRUCTURAL_PREFIX_CAP,\n} from './constants.mjs';\nimport {\n buildCalleeResolver,\n buildImportsFix,\n getImportedLocalName,\n getTsFortressImports,\n hasForeignBinding,\n} from './import-utils.mjs';\n\ntype Options = readonly [];\n\ntype MessageIds = 'useCanonicalType';\n\n/** How one leading length argument of a combinator call must look. */\ntype BoundMatcher = number | 'anyLength' | 'sameAsFirst';\n\ntype Rewrite = Readonly<{\n /** The canonical ts-fortress combinator the call resolves to. */\n from: string;\n /** One matcher per leading length argument the call must have. */\n bounds: readonly BoundMatcher[];\n /** The combinator the call is rewritten to. */\n to: string;\n /** Indices into `bounds` of the length arguments the rewrite keeps, in order. */\n keep: readonly number[];\n /**\n * Whether an options object carrying `defaultValue` blocks the rewrite,\n * because the two combinators type that option differently.\n */\n blockedByDefaultValue: boolean;\n}>;\n\n/**\n * Combinator calls whose length arguments make them a longhand spelling of\n * another combinator.\n *\n * Every entry is **type-identical**, not merely assignable — the two calls\n * produce the very same `Type<T>`, verified against the ts-type-forge\n * definitions:\n *\n * | call | canonical form | why |\n * | :---------------------------- | :------------------------ | :-- |\n * | `minLengthArray(1, x)` | `nonEmptyArray(x)` | `NonEmptyArray<A>` is defined as `MinLengthArray<1, A>` |\n * | `minLengthTuple(0, x)` | `array(x)` | `MinLengthTuple<0, A>` is `readonly A[]` — no constraint at all |\n * | `maxLengthTuple(0, x)` | `fixedLengthTuple(0, x)` | both are `readonly []` |\n * | `boundedLengthTuple(n, n, x)` | `fixedLengthTuple(n, x)` | the union collapses to its single longest member |\n * | `boundedLengthTuple(0, n, x)` | `maxLengthTuple(n, x)` | `MaxLengthTuple<N, A>` is defined as `BoundedLengthTuple<0, N, A>` |\n *\n * The rewrite also preserves `defaultValue` and every accepted/rejected value;\n * only the default `typeName` (and the `details.kind` of the length error it\n * reports) changes, to the one that names the constraint being checked.\n *\n * The `*Array` family has no analogous entries on purpose — see the README:\n * its length constraint lives in a brand, and `BoundedLengthArray<Min, Max, A>`\n * is an intersection of both bounds' brands, so neither\n * `boundedLengthArray(0, n, x)` → `maxLengthArray(n, x)` (which drops the\n * `MinLengthArray<0>` brand) nor `boundedLengthArray(n, n, x)` →\n * `fixedLengthArray(n, x)` (which adds an exact tuple for `n <= 10`) keeps the\n * type unchanged.\n */\nconst REWRITES = [\n {\n from: MIN_LENGTH_ARRAY_FN,\n bounds: [1],\n to: NON_EMPTY_ARRAY_FN,\n keep: [],\n blockedByDefaultValue: true,\n },\n {\n from: MIN_LENGTH_TUPLE_FN,\n bounds: [0],\n to: ARRAY_FN,\n keep: [],\n blockedByDefaultValue: false,\n },\n {\n from: MAX_LENGTH_TUPLE_FN,\n bounds: [0],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n // Listed before the `0, n` entry so that `boundedLengthTuple(0, 0, x)`\n // normalizes to `fixedLengthTuple(0, x)`, the more specific of the two.\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: ['anyLength', 'sameAsFirst'],\n to: FIXED_LENGTH_TUPLE_FN,\n keep: [0],\n blockedByDefaultValue: false,\n },\n {\n from: BOUNDED_LENGTH_TUPLE_FN,\n bounds: [0, 'anyLength'],\n to: MAX_LENGTH_TUPLE_FN,\n keep: [1],\n blockedByDefaultValue: false,\n },\n] as const satisfies readonly Rewrite[];\n\nexport const preferCanonicalLengthConstrainedType: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Normalize a length-constrained ts-fortress array combinator whose bounds are degenerate (e.g. `minLengthArray(1, x)`, `boundedLengthTuple(n, n, x)`) to the combinator that names that constraint directly.',\n },\n fixable: 'code',\n schema: [],\n messages: {\n useCanonicalType:\n 'Use `{{replacement}}(...)` instead of `{{original}}({{boundsText}}, ...)`: with these bounds both build exactly the same type, and `{{replacement}}` names the constraint directly (and reports it on validation failure).',\n },\n },\n\n create: (context) => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const tsFortressImports = getTsFortressImports(program);\n\n const resolveCallee = buildCalleeResolver(tsFortressImports);\n\n const mut_matches: {\n node: TSESTree.CallExpression;\n propertyNode: TSESTree.Node;\n isNamespace: boolean;\n rewrite: Rewrite;\n /** The name the rewritten call will use; `undefined` when unavailable. */\n localName: string | undefined;\n needsImport: boolean;\n }[] = [];\n\n return {\n CallExpression: (node) => {\n const resolved = resolveCallee(node.callee);\n\n if (resolved === undefined) return;\n\n const rewrite = REWRITES.find(\n (candidate) =>\n candidate.from === resolved.canonicalName &&\n matchesRewrite(node, candidate),\n );\n\n if (rewrite === undefined) return;\n\n // A namespace call (`t.maxLengthTuple(...)`) only swaps the property\n // name, so it never needs a binding of its own.\n if (resolved.isNamespace) {\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: true,\n rewrite,\n localName: rewrite.to,\n needsImport: false,\n });\n\n return;\n }\n\n const importedLocalName = getImportedLocalName(\n tsFortressImports,\n rewrite.to,\n );\n\n const localName = importedLocalName ?? rewrite.to;\n\n mut_matches.push({\n node,\n propertyNode: resolved.propertyNode,\n isNamespace: false,\n rewrite,\n localName: hasForeignBinding(sourceCode, node, localName)\n ? undefined\n : localName,\n needsImport: importedLocalName === undefined,\n });\n },\n 'Program:exit': () => {\n const reportable = mut_matches.filter(\n (match) => match.localName !== undefined,\n );\n\n const namesNeedingImport = Array.from(\n new Set(\n reportable\n .filter((match) => match.needsImport)\n .map((match) => match.rewrite.to),\n ),\n );\n\n // Every missing import rides on a single report; see `buildImportsFix`.\n const importFixIndex = reportable.findIndex(\n (match) => match.needsImport,\n );\n\n for (const [\n index,\n { node, propertyNode, rewrite, localName },\n ] of reportable.entries()) {\n if (localName === undefined) continue;\n\n context.report({\n node,\n messageId: 'useCanonicalType',\n data: {\n original: rewrite.from,\n replacement: localName,\n boundsText: boundsText(node, rewrite, sourceCode),\n },\n fix: (fixer) => [\n ...(index === importFixIndex\n ? buildImportsFix(fixer, program, namesNeedingImport)\n : []),\n fixer.replaceText(propertyNode, localName),\n ...dropBoundsFix(fixer, node, rewrite),\n ],\n });\n }\n },\n };\n },\n defaultOptions: [],\n} as const;\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `node` is a `<combinator>(...bounds, elementType, options?)` call\n * whose bounds match `rewrite` exactly as written.\n */\nconst matchesRewrite = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): boolean => {\n const args = node.arguments;\n\n const boundCount = rewrite.bounds.length;\n\n if (\n args.length < boundCount + 1 ||\n args.length > boundCount + 2 ||\n // Explicit type arguments pin the bound independently of the literal, so\n // dropping or moving the literal could change the resulting type.\n node.typeArguments !== undefined ||\n args.some((arg) => arg.type === AST_NODE_TYPES.SpreadElement)\n ) {\n return false;\n }\n\n const bounds = args.slice(0, boundCount).map(getLengthLiteral);\n\n const [first] = bounds;\n\n const boundsMatch = rewrite.bounds.every((matcher, index) => {\n const value = bounds[index];\n\n if (value === undefined) return false;\n\n switch (matcher) {\n case 'anyLength':\n return value <= STRUCTURAL_PREFIX_CAP;\n\n case 'sameAsFirst':\n return value === first && value <= STRUCTURAL_PREFIX_CAP;\n\n default:\n return value === matcher;\n }\n });\n\n return (\n boundsMatch &&\n (!rewrite.blockedByDefaultValue ||\n !passesDefaultValue(args[boundCount + 1]))\n );\n};\n\n/** The bounds of the matched call, as written, for the report message. */\nconst boundsText = (\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n sourceCode: TSESLint.SourceCode,\n): string =>\n node.arguments\n .slice(0, rewrite.bounds.length)\n .map((arg) => sourceCode.getText(arg))\n .join(', ');\n\n/**\n * Removes the length arguments the rewrite drops. Each removal runs from the\n * dropped argument up to the start of the next one, so the comma, whitespace\n * and any comments in between go with it; the ranges never overlap.\n */\nconst dropBoundsFix = (\n fixer: TSESLint.RuleFixer,\n node: TSESTree.CallExpression,\n rewrite: Rewrite,\n): readonly TSESLint.RuleFix[] =>\n rewrite.bounds.flatMap((_, index) => {\n if (rewrite.keep.includes(index)) return [];\n\n const dropped = node.arguments[index];\n\n const next = node.arguments[index + 1];\n\n return dropped === undefined || next === undefined\n ? []\n : [fixer.removeRange([dropped.range[0], next.range[0]])];\n });\n\n/**\n * A non-negative integer literal's value, or `undefined` for anything else —\n * a computed bound cannot be matched against the rewrite table.\n */\nconst getLengthLiteral = (node: TSESTree.Node): number | undefined =>\n node.type === AST_NODE_TYPES.Literal &&\n typeof node.value === 'number' &&\n Number.isSafeInteger(node.value) &&\n node.value >= 0\n ? node.value\n : undefined;\n\n/**\n * Whether the options argument may carry a `defaultValue`. Anything that is not\n * a plain object literal of statically known keys counts, since its contents\n * cannot be ruled out.\n */\nconst passesDefaultValue = (node: TSESTree.Node | undefined): boolean =>\n node !== undefined &&\n (node.type !== AST_NODE_TYPES.ObjectExpression ||\n node.properties.some(\n (property) =>\n property.type !== AST_NODE_TYPES.Property ||\n property.computed ||\n property.key.type !== AST_NODE_TYPES.Identifier ||\n property.key.name === 'defaultValue',\n ));\n\n/* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  export declare const tsFortressRules: {
2
- readonly 'prefer-non-empty-array': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useNonEmptyArray", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
2
+ readonly 'prefer-canonical-length-constrained-type': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useCanonicalType", readonly [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
3
3
  };
4
4
  //# 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,eAAe;;CAEc,CAAC"}
1
+ {"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe;;CAGc,CAAC"}
@@ -1,7 +1,7 @@
1
- import { preferNonEmptyArray } from './prefer-non-empty-array.mjs';
1
+ import { preferCanonicalLengthConstrainedType } from './prefer-canonical-length-constrained-type.mjs';
2
2
 
3
3
  const tsFortressRules = {
4
- "prefer-non-empty-array": preferNonEmptyArray
4
+ "prefer-canonical-length-constrained-type": preferCanonicalLengthConstrainedType
5
5
  };
6
6
 
7
7
  export { tsFortressRules };
@@ -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 tsFortressRules = {\n 'prefer-non-empty-array': preferNonEmptyArray,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;AAGO,MAAM,eAAA,GAAkB;AAAA,EAC7B,wBAAA,EAA0B;AAC5B;;;;"}
1
+ {"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferCanonicalLengthConstrainedType } from './prefer-canonical-length-constrained-type.mjs';\n\nexport const tsFortressRules = {\n 'prefer-canonical-length-constrained-type':\n preferCanonicalLengthConstrainedType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;AAGO,MAAM,eAAA,GAAkB;AAAA,EAC7B,0CAAA,EACE;AACJ;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-fortress",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer schema definitions toward ts-fortress idioms.",
6
6
  "keywords": [