eslint-plugin-ts-type-forge 0.6.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/plugin.d.mts +4 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +39 -0
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +11 -0
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +81 -7
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -2
- package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
- package/dist/rules/prefer-canonical-mutable-record.d.mts +10 -0
- package/dist/rules/prefer-canonical-mutable-record.d.mts.map +1 -0
- package/dist/rules/prefer-canonical-mutable-record.mjs +83 -0
- package/dist/rules/prefer-canonical-mutable-record.mjs.map +1 -0
- package/dist/rules/{tuple-rule-utils.d.mts → rewrite-rule-utils.d.mts} +4 -3
- package/dist/rules/rewrite-rule-utils.d.mts.map +1 -0
- package/dist/rules/{tuple-rule-utils.mjs → rewrite-rule-utils.mjs} +3 -3
- package/dist/rules/rewrite-rule-utils.mjs.map +1 -0
- package/dist/rules/rules.d.mts +3 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/dist/rules/std-type-rule-utils.d.mts.map +1 -1
- package/dist/rules/std-type-rule-utils.mjs +2 -5
- package/dist/rules/std-type-rule-utils.mjs.map +1 -1
- package/package.json +6 -6
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +46 -0
- package/src/rules/ast-utils.mts +217 -7
- package/src/rules/prefer-canonical-length-constrained-tuple.mts +3 -3
- package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +71 -0
- package/src/rules/prefer-canonical-mutable-record.mts +151 -0
- package/src/rules/prefer-canonical-mutable-record.test.mts +164 -0
- package/src/rules/{tuple-rule-utils.mts → rewrite-rule-utils.mts} +4 -3
- package/src/rules/rules.mts +2 -0
- package/src/rules/std-type-rule-utils.mts +1 -21
- package/dist/rules/tuple-rule-utils.d.mts.map +0 -1
- package/dist/rules/tuple-rule-utils.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -75,6 +75,7 @@ export default [
|
|
|
75
75
|
| Rule | Fix | Description |
|
|
76
76
|
| :------------------------------------------ | :--------- | :---------------------------------------------------------------------------------------- |
|
|
77
77
|
| `prefer-canonical-length-constrained-tuple` | autofix | Replace hand-rolled uniform tuple spellings with the canonical ts-type-forge tuple type. |
|
|
78
|
+
| `prefer-canonical-mutable-record` | autofix | Replace `Mutable<Record<K, V>>` with the canonical `MutableRecord<K, V>`. |
|
|
78
79
|
| `prefer-strict-or-relaxed-utility-type` | suggestion | Replace `Exclude` / `Extract` / `Omit` / `Pick` with the `Strict*` or `Relaxed*` variant. |
|
|
79
80
|
| `prefer-readonly-or-mutable-record` | suggestion | Replace `Record` with `ReadonlyRecord` or `MutableRecord`. |
|
|
80
81
|
|
|
@@ -127,6 +128,28 @@ better as-is than `FixedLengthTuple<1, V>`.
|
|
|
127
128
|
- files that already bind the target name to something else (a local alias, or
|
|
128
129
|
an import from another module).
|
|
129
130
|
|
|
131
|
+
### `prefer-canonical-mutable-record`
|
|
132
|
+
|
|
133
|
+
`Mutable` strips the `readonly` modifier from every property, so applied to a
|
|
134
|
+
record utility it spells in two steps what `MutableRecord` says in one. The
|
|
135
|
+
rewrite is exactly type-preserving, so this rule is an autofix. `Mutable`
|
|
136
|
+
applied to `ReadonlyRecord` or (redundantly) to `MutableRecord` collapses to
|
|
137
|
+
the same type and is normalized the same way.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
// ❌
|
|
141
|
+
type Counters = Mutable<Record<string, number>>;
|
|
142
|
+
type Draft = Mutable<ReadonlyRecord<string, Item>>;
|
|
143
|
+
|
|
144
|
+
// ✅
|
|
145
|
+
type Counters = MutableRecord<string, number>;
|
|
146
|
+
type Draft = MutableRecord<string, Item>;
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Qualified names (`Utils.Mutable<…>`) and files that declare or import their own
|
|
150
|
+
`Mutable` / `Record` / … are left alone — those spellings do not denote the
|
|
151
|
+
ts-type-forge (or standard-library) types this rule reasons about.
|
|
152
|
+
|
|
130
153
|
### `prefer-strict-or-relaxed-utility-type`
|
|
131
154
|
|
|
132
155
|
`Exclude<T, U>`, `Extract<T, U>`, `Omit<T, K>` and `Pick<T, K>` do not constrain
|
package/dist/plugin.d.mts
CHANGED
|
@@ -8,6 +8,9 @@ export declare const eslintPluginTsTypeForge: {
|
|
|
8
8
|
importStyle?: 'global' | 'named';
|
|
9
9
|
maxLength?: number;
|
|
10
10
|
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
11
|
+
readonly 'prefer-canonical-mutable-record': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useMutableRecord", readonly [(Readonly<{
|
|
12
|
+
importStyle?: 'global' | 'named';
|
|
13
|
+
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
11
14
|
readonly 'prefer-readonly-or-mutable-record': import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWith" | "useReadonlyOrMutableRecord", readonly [(Readonly<{
|
|
12
15
|
importStyle?: 'global' | 'named';
|
|
13
16
|
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
@@ -24,6 +27,7 @@ export declare const eslintPluginTsTypeForge: {
|
|
|
24
27
|
};
|
|
25
28
|
readonly rules: {
|
|
26
29
|
readonly 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error';
|
|
30
|
+
readonly 'ts-type-forge/prefer-canonical-mutable-record': 'error';
|
|
27
31
|
readonly 'ts-type-forge/prefer-readonly-or-mutable-record': 'error';
|
|
28
32
|
readonly 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error';
|
|
29
33
|
};
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","sourceRoot":"","sources":["../src/plugin.mts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAiCvE,eAAO,MAAM,uBAAuB;;iBAEhC,IAAI,EAAE,6BAA6B;;;;;;;;;;;;;;;;;;QAInC,oDAAoD;iBACpD,WAAW;2BAtBP,2BAA2B;;yBAQ3B,eAAe,EAAI,YAAY;;;oFAjBsB,OAAO;0EACjB,OAAO;4EACL,OAAO;gFACH,OAAO;;;;CA8B/B,CAAC"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { tsTypeForgeRules } from './rules/rules.mjs';
|
|
|
2
2
|
|
|
3
3
|
const recommendedRules = {
|
|
4
4
|
"ts-type-forge/prefer-canonical-length-constrained-tuple": "error",
|
|
5
|
+
"ts-type-forge/prefer-canonical-mutable-record": "error",
|
|
5
6
|
"ts-type-forge/prefer-readonly-or-mutable-record": "error",
|
|
6
7
|
"ts-type-forge/prefer-strict-or-relaxed-utility-type": "error"
|
|
7
8
|
};
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { tsTypeForgeRules } from './rules/index.mjs';\nimport { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';\n\n/**\n * Every rule this plugin ships, at `error`.\n *\n * The `satisfies` clause below is keyed off {@link tsTypeForgeRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',\n 'ts-type-forge/prefer-readonly-or-mutable-record': 'error',\n 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error',\n} as const satisfies Readonly<\n Record<`ts-type-forge/${keyof typeof tsTypeForgeRules}`, 'error'>\n>;\n\nconst recommendedConfig = {\n name: 'ts-type-forge/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsTypeForge` refers to. ESLint rejects a plugin name that\n // maps to two different objects with `Cannot redefine plugin`, which is\n // what a user combining this preset with their own\n // `plugins: { 'ts-type-forge': eslintPluginTsTypeForge }` entry would hit\n // if the preset embedded a separate copy of the plugin.\n get 'ts-type-forge'(): ESLintPlugin {\n return eslintPluginTsTypeForge;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsTypeForge = {\n meta: {\n name: 'eslint-plugin-ts-type-forge',\n },\n rules: tsTypeForgeRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AASA,MAAM,gBAAA,GAAmB;AAAA,EACvB,yDAAA,EAA2D,OAAA;AAAA,EAC3D,iDAAA,EAAmD,OAAA;AAAA,EACnD,qDAAA,EAAuD;AACzD,CAAA;AAIA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,2BAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,eAAA,GAAgC;AAClC,MAAA,OAAO,uBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,uBAAA,GAA0B;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,gBAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB;;;;"}
|
|
1
|
+
{"version":3,"file":"plugin.mjs","sources":["../src/plugin.mts"],"sourcesContent":["import { tsTypeForgeRules } from './rules/index.mjs';\nimport { type ESLintFlatConfig, type ESLintPlugin } from './types.mjs';\n\n/**\n * Every rule this plugin ships, at `error`.\n *\n * The `satisfies` clause below is keyed off {@link tsTypeForgeRules}, so adding\n * a rule without listing it here fails to type-check.\n */\nconst recommendedRules = {\n 'ts-type-forge/prefer-canonical-length-constrained-tuple': 'error',\n 'ts-type-forge/prefer-canonical-mutable-record': 'error',\n 'ts-type-forge/prefer-readonly-or-mutable-record': 'error',\n 'ts-type-forge/prefer-strict-or-relaxed-utility-type': 'error',\n} as const satisfies Readonly<\n Record<`ts-type-forge/${keyof typeof tsTypeForgeRules}`, 'error'>\n>;\n\nconst recommendedConfig = {\n name: 'ts-type-forge/recommended',\n plugins: {\n // Resolved lazily so that this config registers the *same* object that\n // `eslintPluginTsTypeForge` refers to. ESLint rejects a plugin name that\n // maps to two different objects with `Cannot redefine plugin`, which is\n // what a user combining this preset with their own\n // `plugins: { 'ts-type-forge': eslintPluginTsTypeForge }` entry would hit\n // if the preset embedded a separate copy of the plugin.\n get 'ts-type-forge'(): ESLintPlugin {\n return eslintPluginTsTypeForge;\n },\n },\n rules: recommendedRules,\n} as const satisfies ESLintFlatConfig;\n\nexport const eslintPluginTsTypeForge = {\n meta: {\n name: 'eslint-plugin-ts-type-forge',\n },\n rules: tsTypeForgeRules,\n configs: {\n /** Enables every rule of this plugin at `error`. */\n recommended: recommendedConfig,\n },\n} as const satisfies ESLintPlugin;\n"],"names":[],"mappings":";;AASA,MAAM,gBAAA,GAAmB;AAAA,EACvB,yDAAA,EAA2D,OAAA;AAAA,EAC3D,+CAAA,EAAiD,OAAA;AAAA,EACjD,iDAAA,EAAmD,OAAA;AAAA,EACnD,qDAAA,EAAuD;AACzD,CAAA;AAIA,MAAM,iBAAA,GAAoB;AAAA,EACxB,IAAA,EAAM,2BAAA;AAAA,EACN,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOP,IAAI,eAAA,GAAgC;AAClC,MAAA,OAAO,uBAAA;AAAA,IACT;AAAA,GACF;AAAA,EACA,KAAA,EAAO;AACT,CAAA;AAEO,MAAM,uBAAA,GAA0B;AAAA,EACrC,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM;AAAA,GACR;AAAA,EACA,KAAA,EAAO,gBAAA;AAAA,EACP,OAAA,EAAS;AAAA;AAAA,IAEP,WAAA,EAAa;AAAA;AAEjB;;;;"}
|
package/dist/rule-types.d.mts
CHANGED
|
@@ -51,6 +51,43 @@ declare namespace PreferCanonicalLengthConstrainedTuple {
|
|
|
51
51
|
type Options = NonNullable<RuleOptionsOf<'prefer-canonical-length-constrained-tuple'>[0]>;
|
|
52
52
|
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* @description Replace `Mutable<Record<K, V>>` (and `Mutable` applied to `ReadonlyRecord` / `MutableRecord`) with the canonical ts-type-forge `MutableRecord<K, V>`.
|
|
56
|
+
*
|
|
57
|
+
* ```md
|
|
58
|
+
* | key | value |
|
|
59
|
+
* | :--------- | :--------- |
|
|
60
|
+
* | type | suggestion |
|
|
61
|
+
* | deprecated | false |
|
|
62
|
+
* | fixable | code |
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare namespace PreferCanonicalMutableRecord {
|
|
66
|
+
/**
|
|
67
|
+
* ### schema
|
|
68
|
+
*
|
|
69
|
+
* ```json
|
|
70
|
+
* [
|
|
71
|
+
* {
|
|
72
|
+
* "type": "object",
|
|
73
|
+
* "properties": {
|
|
74
|
+
* "importStyle": {
|
|
75
|
+
* "type": "string",
|
|
76
|
+
* "enum": [
|
|
77
|
+
* "global",
|
|
78
|
+
* "named"
|
|
79
|
+
* ],
|
|
80
|
+
* "description": "How the ts-type-forge type is brought into scope. 'named' (default) makes the autofix add the corresponding `import { type … } from 'ts-type-forge';` when the name is not imported yet; 'global' assumes the ambient globals of `ts-type-forge/global` and never touches imports."
|
|
81
|
+
* }
|
|
82
|
+
* },
|
|
83
|
+
* "additionalProperties": false
|
|
84
|
+
* }
|
|
85
|
+
* ]
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
type Options = NonNullable<RuleOptionsOf<'prefer-canonical-mutable-record'>[0]>;
|
|
89
|
+
type RuleEntry = 'off' | Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]>;
|
|
90
|
+
}
|
|
54
91
|
/**
|
|
55
92
|
* @description Replace the built-in `Record` with the ts-type-forge `ReadonlyRecord` or `MutableRecord`, so that the mutability of the record is stated rather than implied.
|
|
56
93
|
*
|
|
@@ -127,11 +164,13 @@ declare namespace PreferStrictOrRelaxedUtilityType {
|
|
|
127
164
|
}
|
|
128
165
|
export type EslintTsTypeForgeRules = Readonly<{
|
|
129
166
|
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.RuleEntry;
|
|
167
|
+
'ts-type-forge/prefer-canonical-mutable-record': PreferCanonicalMutableRecord.RuleEntry;
|
|
130
168
|
'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.RuleEntry;
|
|
131
169
|
'ts-type-forge/prefer-strict-or-relaxed-utility-type': PreferStrictOrRelaxedUtilityType.RuleEntry;
|
|
132
170
|
}>;
|
|
133
171
|
export type EslintTsTypeForgeRulesOption = Readonly<{
|
|
134
172
|
'ts-type-forge/prefer-canonical-length-constrained-tuple': PreferCanonicalLengthConstrainedTuple.Options;
|
|
173
|
+
'ts-type-forge/prefer-canonical-mutable-record': PreferCanonicalMutableRecord.Options;
|
|
135
174
|
'ts-type-forge/prefer-readonly-or-mutable-record': PreferReadonlyOrMutableRecord.Options;
|
|
136
175
|
'ts-type-forge/prefer-strict-or-relaxed-utility-type': PreferStrictOrRelaxedUtilityType.Options;
|
|
137
176
|
}>;
|
|
@@ -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,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;;;;;;;;;;GAUG;AACH,kBAAU,6BAA6B,CAAC;IACtC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAY,OAAO,GAAG,WAAW,CAC/B,aAAa,CAAC,mCAAmC,CAAC,CAAC,CAAC,CAAC,CACtD,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;;;;;;;;;;GAUG;AACH,kBAAU,gCAAgC,CAAC;IACzC;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAY,OAAO,GAAG,WAAW,CAC/B,aAAa,CAAC,uCAAuC,CAAC,CAAC,CAAC,CAAC,CAC1D,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;IAC3G,iDAAiD,EAAE,6BAA6B,CAAC,SAAS,CAAC;IAC3F,qDAAqD,EAAE,gCAAgC,CAAC,SAAS,CAAC;CACnG,CAAC,CAAC;AAEH,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC;IAClD,yDAAyD,EAAE,qCAAqC,CAAC,OAAO,CAAC;IACzG,iDAAiD,EAAE,6BAA6B,CAAC,OAAO,CAAC;IACzF,qDAAqD,EAAE,gCAAgC,CAAC,OAAO,CAAC;CACjG,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":"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"}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
/**
|
|
3
|
+
* Whether `name` resolves to a declaration in this file (a type alias, an
|
|
4
|
+
* interface, a class, an import, …) rather than to the ambient standard-library
|
|
5
|
+
* type of the same name.
|
|
6
|
+
*
|
|
7
|
+
* The `defs` check is what separates the two: ESLint materializes an *implicit*
|
|
8
|
+
* global variable for every unresolved reference, so `findVariable` also
|
|
9
|
+
* succeeds for `Exclude` in a file that never declares it. Only a variable with
|
|
10
|
+
* at least one definition was actually written down somewhere in this file.
|
|
11
|
+
*/
|
|
12
|
+
export declare const isLocallyBound: (sourceCode: TSESLint.SourceCode, node: TSESTree.Node, name: string) => boolean;
|
|
2
13
|
export type UniformTupleShape = Readonly<{
|
|
3
14
|
/** The node an autofix must replace: the tuple, or the `readonly` operator. */
|
|
4
15
|
node: TSESTree.Node;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAKlC;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,eACb,QAAQ,CAAC,UAAU,QACzB,QAAQ,CAAC,IAAI,QACb,MAAM,KACX,OAIF,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACvC,+EAA+E;IAC/E,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2DAA2D;IAC3D,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,UACvB,QAAQ,CAAC,WAAW,cACf,QAAQ,CAAC,UAAU,KAC9B,iBAAiB,GAAG,SAmEtB,CAAC"}
|
package/dist/rules/ast-utils.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
-
import { Arr } from 'ts-data-forge';
|
|
1
|
+
import { ASTUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr, isRecord, hasKey, isNonNullObject } from 'ts-data-forge';
|
|
3
3
|
|
|
4
|
+
const isLocallyBound = (sourceCode, node, name) => {
|
|
5
|
+
const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);
|
|
6
|
+
return variable !== null && Arr.isNonEmpty(variable.defs);
|
|
7
|
+
};
|
|
4
8
|
const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
5
9
|
if (isInConditionalTypeExtendsClause(tuple)) return void 0;
|
|
6
10
|
const { parent } = tuple;
|
|
@@ -24,26 +28,96 @@ const analyzeUniformTuple = (tuple, sourceCode) => {
|
|
|
24
28
|
...Arr.tail(leading),
|
|
25
29
|
...restElementType === void 0 ? [] : [restElementType]
|
|
26
30
|
];
|
|
27
|
-
|
|
28
|
-
(element) => normalizeWhitespace(sourceCode.getText(element))
|
|
29
|
-
)
|
|
31
|
+
if (others.some(
|
|
32
|
+
(element) => normalizeWhitespace(sourceCode.getText(element)) !== expected
|
|
33
|
+
)) {
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
return definesRecursiveTypeAlias(tuple, sourceCode) ? void 0 : {
|
|
30
37
|
node: isReadonly ? parent : tuple,
|
|
31
38
|
isReadonly,
|
|
32
39
|
elementText,
|
|
33
40
|
fixedCount: leading.length,
|
|
34
41
|
hasRest
|
|
35
|
-
}
|
|
42
|
+
};
|
|
36
43
|
};
|
|
37
44
|
const isInConditionalTypeExtendsClause = (node) => {
|
|
38
45
|
if (node.type === AST_NODE_TYPES.Program) return false;
|
|
39
46
|
const { parent } = node;
|
|
40
47
|
return parent.type === AST_NODE_TYPES.TSConditionalType && parent.extendsType === node || isInConditionalTypeExtendsClause(parent);
|
|
41
48
|
};
|
|
49
|
+
const definesRecursiveTypeAlias = (tuple, sourceCode) => {
|
|
50
|
+
const alias = enclosingTypeAlias(tuple);
|
|
51
|
+
if (alias === void 0) return false;
|
|
52
|
+
const references = aliasReferenceGraph(programOf(alias), sourceCode);
|
|
53
|
+
const mut_seen = /* @__PURE__ */ new Set();
|
|
54
|
+
const mut_queue = tuple.elementTypes.flatMap(
|
|
55
|
+
(element) => Array.from(referencedTypeNames(element, sourceCode))
|
|
56
|
+
);
|
|
57
|
+
while (true) {
|
|
58
|
+
const next = mut_queue.pop();
|
|
59
|
+
if (next === void 0) return false;
|
|
60
|
+
if (next === alias.id.name) return true;
|
|
61
|
+
if (mut_seen.has(next)) continue;
|
|
62
|
+
mut_seen.add(next);
|
|
63
|
+
const referenced = references.get(next);
|
|
64
|
+
if (referenced !== void 0) {
|
|
65
|
+
mut_queue.push(...referenced);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const enclosingTypeAlias = (node) => {
|
|
70
|
+
if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) return node;
|
|
71
|
+
if (node.type === AST_NODE_TYPES.Program) return void 0;
|
|
72
|
+
return enclosingTypeAlias(node.parent);
|
|
73
|
+
};
|
|
74
|
+
const aliasReferenceGraph = (program, sourceCode) => {
|
|
75
|
+
const cached = aliasReferenceGraphCache.get(program);
|
|
76
|
+
if (cached !== void 0) return cached;
|
|
77
|
+
const mut_graph = /* @__PURE__ */ new Map();
|
|
78
|
+
walk(program, sourceCode, (visited) => {
|
|
79
|
+
if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {
|
|
80
|
+
mut_graph.set(
|
|
81
|
+
visited.id.name,
|
|
82
|
+
referencedTypeNames(visited.typeAnnotation, sourceCode)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
aliasReferenceGraphCache.set(program, mut_graph);
|
|
87
|
+
return mut_graph;
|
|
88
|
+
};
|
|
89
|
+
const aliasReferenceGraphCache = /* @__PURE__ */ new WeakMap();
|
|
90
|
+
const programOf = (node) => node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);
|
|
91
|
+
const referencedTypeNames = (node, sourceCode) => {
|
|
92
|
+
const mut_names = /* @__PURE__ */ new Set();
|
|
93
|
+
walk(node, sourceCode, (visited) => {
|
|
94
|
+
if (visited.type === AST_NODE_TYPES.TSTypeReference && visited.typeName.type === AST_NODE_TYPES.Identifier) {
|
|
95
|
+
mut_names.add(visited.typeName.name);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return mut_names;
|
|
99
|
+
};
|
|
100
|
+
const walk = (node, sourceCode, visit) => {
|
|
101
|
+
visit(node);
|
|
102
|
+
if (!isRecord(node)) return;
|
|
103
|
+
const keys = sourceCode.visitorKeys[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
const value = hasKey(node, key) ? node[key] : void 0;
|
|
106
|
+
if (Arr.isArray(value)) {
|
|
107
|
+
for (const element of value) {
|
|
108
|
+
if (isNode(element)) walk(element, sourceCode, visit);
|
|
109
|
+
}
|
|
110
|
+
} else if (isNode(value)) {
|
|
111
|
+
walk(value, sourceCode, visit);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const isNode = (value) => isNonNullObject(value) && typeof value.type === "string";
|
|
42
116
|
const getArrayElementType = (node) => {
|
|
43
117
|
if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;
|
|
44
118
|
return node.type === AST_NODE_TYPES.TSTypeOperator && node.operator === "readonly" && node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType ? node.typeAnnotation.elementType : void 0;
|
|
45
119
|
};
|
|
46
120
|
const normalizeWhitespace = (text) => text.replaceAll(/\s+/gu, " ").trim();
|
|
47
121
|
|
|
48
|
-
export { analyzeUniformTuple };
|
|
122
|
+
export { analyzeUniformTuple, isLocallyBound };
|
|
49
123
|
//# 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';\nimport { Arr } from 'ts-data-forge';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n * - A tuple inside the `extends` clause of a conditional type is a match\n * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n if (isInConditionalTypeExtendsClause(tuple)) return undefined;\n\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ? Arr.butLast(elements) : elements;\n\n if (\n Arr.isEmpty(leading) ||\n leading.some(\n (element) =>\n element.type === AST_NODE_TYPES.TSRestType ||\n element.type === AST_NODE_TYPES.TSOptionalType ||\n element.type === AST_NODE_TYPES.TSNamedTupleMember,\n )\n ) {\n return undefined;\n }\n\n const restElementType =\n last !== undefined && hasRest\n ? getArrayElementType(last.typeAnnotation)\n : undefined;\n\n if (hasRest && restElementType === undefined) return undefined;\n\n const [head] = leading;\n\n if (head === undefined) return undefined;\n\n const elementText = sourceCode.getText(head);\n\n const expected = normalizeWhitespace(elementText);\n\n const others = [\n ...Arr.tail(leading),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n return others.every(\n (element) => normalizeWhitespace(sourceCode.getText(element)) === expected,\n )\n ? {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n }\n : undefined;\n};\n\n/**\n * Whether `node` sits inside the `extends` clause of a conditional type, where\n * a tuple is a *match pattern* rather than a type.\n *\n * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are\n * still generic. The canonical spellings resolve through a mapped type\n * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and\n * against one of those the checker defers the whole conditional instead —\n * yielding `boolean` where the tuple pattern yielded `true` or `false`, which\n * silently widens everything downstream. So those positions are left alone.\n */\nconst isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {\n // The root has no parent, so it terminates the walk.\n if (node.type === AST_NODE_TYPES.Program) return false;\n\n const { parent } = node;\n\n return (\n (parent.type === AST_NODE_TYPES.TSConditionalType &&\n parent.extendsType === node) ||\n isInConditionalTypeExtendsClause(parent)\n );\n};\n\n/** Unwraps `V[]` and `readonly V[]` to `V`. */\nconst getArrayElementType = (\n node: TSESTree.TypeNode,\n): TSESTree.TypeNode | undefined => {\n if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;\n\n return node.type === AST_NODE_TYPES.TSTypeOperator &&\n node.operator === 'readonly' &&\n node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType\n ? node.typeAnnotation.elementType\n : undefined;\n};\n\nconst normalizeWhitespace = (text: string): string =>\n text.replaceAll(/\\s+/gu, ' ').trim();\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ast-utils.mjs","sources":["../../src/rules/ast-utils.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n ASTUtils,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr, hasKey, isNonNullObject, isRecord } from 'ts-data-forge';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/**\n * Whether `name` resolves to a declaration in this file (a type alias, an\n * interface, a class, an import, …) rather than to the ambient standard-library\n * type of the same name.\n *\n * The `defs` check is what separates the two: ESLint materializes an *implicit*\n * global variable for every unresolved reference, so `findVariable` also\n * succeeds for `Exclude` in a file that never declares it. Only a variable with\n * at least one definition was actually written down somewhere in this file.\n */\nexport const isLocallyBound = (\n sourceCode: TSESLint.SourceCode,\n node: TSESTree.Node,\n name: string,\n): boolean => {\n const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);\n\n return variable !== null && Arr.isNonEmpty(variable.defs);\n};\n\nexport type UniformTupleShape = Readonly<{\n /** The node an autofix must replace: the tuple, or the `readonly` operator. */\n node: TSESTree.Node;\n isReadonly: boolean;\n /** Source text of the (single, shared) element type. */\n elementText: string;\n /** Number of leading fixed-position elements. */\n fixedCount: number;\n /** Whether a trailing `...V[]` rest element is present. */\n hasRest: boolean;\n}>;\n\n/**\n * Recognizes a tuple whose every position has the *same* element type — the\n * hand-rolled spelling of ts-type-forge's length-constrained tuple family:\n *\n * | shape | meaning |\n * | :------------------- | :------------------------- |\n * | `[V, ..., V]` | `FixedLengthTuple<N, V>` |\n * | `[V, ..., V, ...V[]]`| `MinLengthTuple<N, V>` |\n *\n * Returns `undefined` for anything else. Deliberately narrow:\n *\n * - Element types are compared by normalized source text, so heterogeneous\n * tuples are never matched.\n * - Optional (`[V?]`) and labelled (`[head: V]`) members are skipped, since\n * rewriting them would drop information.\n * - A tuple that is only a rest element (`[...V[]]`) is not a length\n * constraint at all.\n * - A tuple inside the `extends` clause of a conditional type is a match\n * pattern, not a type (see {@link isInConditionalTypeExtendsClause}).\n */\nexport const analyzeUniformTuple = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): UniformTupleShape | undefined => {\n if (isInConditionalTypeExtendsClause(tuple)) return undefined;\n\n const { parent } = tuple;\n\n const isReadonly =\n parent.type === AST_NODE_TYPES.TSTypeOperator &&\n parent.operator === 'readonly';\n\n const elements = tuple.elementTypes;\n\n const last = elements.at(-1);\n\n const hasRest = last?.type === AST_NODE_TYPES.TSRestType;\n\n const leading = hasRest ? Arr.butLast(elements) : elements;\n\n if (\n Arr.isEmpty(leading) ||\n leading.some(\n (element) =>\n element.type === AST_NODE_TYPES.TSRestType ||\n element.type === AST_NODE_TYPES.TSOptionalType ||\n element.type === AST_NODE_TYPES.TSNamedTupleMember,\n )\n ) {\n return undefined;\n }\n\n const restElementType =\n last !== undefined && hasRest\n ? getArrayElementType(last.typeAnnotation)\n : undefined;\n\n if (hasRest && restElementType === undefined) return undefined;\n\n const [head] = leading;\n\n if (head === undefined) return undefined;\n\n const elementText = sourceCode.getText(head);\n\n const expected = normalizeWhitespace(elementText);\n\n const others = [\n ...Arr.tail(leading),\n ...(restElementType === undefined ? [] : [restElementType]),\n ];\n\n if (\n others.some(\n (element) =>\n normalizeWhitespace(sourceCode.getText(element)) !== expected,\n )\n ) {\n return undefined;\n }\n\n return definesRecursiveTypeAlias(tuple, sourceCode)\n ? undefined\n : {\n node: isReadonly ? parent : tuple,\n isReadonly,\n elementText,\n fixedCount: leading.length,\n hasRest,\n };\n};\n\n/**\n * Whether `node` sits inside the `extends` clause of a conditional type, where\n * a tuple is a *match pattern* rather than a type.\n *\n * `[A, B] extends [true, true]` matches element-wise even while `A` and `B` are\n * still generic. The canonical spellings resolve through a mapped type\n * (`MutableFixedLengthTuple<N, V>` is `Mutable<FixedLengthTuple<N, V>>`), and\n * against one of those the checker defers the whole conditional instead —\n * yielding `boolean` where the tuple pattern yielded `true` or `false`, which\n * silently widens everything downstream. So those positions are left alone.\n */\nconst isInConditionalTypeExtendsClause = (node: TSESTree.Node): boolean => {\n // The root has no parent, so it terminates the walk.\n if (node.type === AST_NODE_TYPES.Program) return false;\n\n const { parent } = node;\n\n return (\n (parent.type === AST_NODE_TYPES.TSConditionalType &&\n parent.extendsType === node) ||\n isInConditionalTypeExtendsClause(parent)\n );\n};\n\n/**\n * Whether the tuple spells out an alias that its own element type leads back\n * to.\n *\n * A tuple literal is how TypeScript's cycle-breaking works: `type T = readonly\n * [T, T]` resolves, because the element positions are deferred. Rewriting it to\n * `FixedLengthTuple<2, T>` routes the same cycle through a mapped type, which\n * the checker will not compute — `T` becomes an error type, and every use of it\n * silently reads as `any`. `tsc` reports the alias once; the typed linter then\n * reports each use, which is how this was found.\n *\n * The cycle is often indirect. `LambdaApplication = readonly [LambdaTerm,\n * LambdaTerm]` looks fine on its own, and only recurses because `LambdaTerm` is\n * a union that includes `LambdaApplication`. So this follows references through\n * the other aliases in the file rather than looking for the enclosing name in\n * the element text.\n *\n * **Any cycle counts, which leaves some rewritable tuples alone.** Not every\n * cycle actually breaks: TypeScript resolves an object type, an array, a tuple\n * and a function type without resolving what is inside them, so `type Pair =\n * FixedLengthTuple<2, Foo>` alongside `type Foo = { p: Pair }` compiles, and so\n * does `type Tree = { kids: FixedLengthTuple<2, Tree> }`. Skipping those\n * constructs while following references was tried and does not hold up — the\n * deferral is undone by anything that asks for the structure, and that can sit\n * anywhere in the file:\n *\n * ```ts\n * type Pair = readonly [Foo, Foo]; // rewriting this breaks Pair and Foo\n * type Foo = Wrapper[number]; // …because of the indexed access here\n * type Wrapper = readonly Pair[];\n * ```\n *\n * Deciding it properly means knowing every use of every alias, which no\n * syntactic rule can do — so a cycle of any shape suppresses the rewrite. The\n * cost is a tuple left spelled out; the alternative is an autofix that turns a\n * type into `any`.\n *\n * A cycle closed through an `import` is not visible here and is not detected.\n */\nconst definesRecursiveTypeAlias = (\n tuple: TSESTree.TSTupleType,\n sourceCode: TSESLint.SourceCode,\n): boolean => {\n const alias = enclosingTypeAlias(tuple);\n\n if (alias === undefined) return false;\n\n const references = aliasReferenceGraph(programOf(alias), sourceCode);\n\n const mut_seen = new Set<string>();\n\n const mut_queue: string[] = tuple.elementTypes.flatMap((element) =>\n Array.from(referencedTypeNames(element, sourceCode)),\n );\n\n while (true) {\n const next = mut_queue.pop();\n\n if (next === undefined) return false;\n\n if (next === alias.id.name) return true;\n\n if (mut_seen.has(next)) continue;\n\n mut_seen.add(next);\n\n const referenced = references.get(next);\n\n if (referenced !== undefined) {\n mut_queue.push(...referenced);\n }\n }\n};\n\n/** The `type X = …` declaration `node` sits in, if any. */\nconst enclosingTypeAlias = (\n node: TSESTree.Node,\n): TSESTree.TSTypeAliasDeclaration | undefined => {\n if (node.type === AST_NODE_TYPES.TSTypeAliasDeclaration) return node;\n\n if (node.type === AST_NODE_TYPES.Program) return undefined;\n\n return enclosingTypeAlias(node.parent);\n};\n\n/**\n * Which other type names each `type X = …` in the file refers to, keyed by `X`.\n *\n * Built once per file and cached on the `Program` node: the search below runs\n * per candidate tuple, and rebuilding this for each of them made the rule cost\n * `O(tuples × file size)` — a full second on the largest generated files here.\n */\nconst aliasReferenceGraph = (\n program: TSESTree.Program,\n sourceCode: TSESLint.SourceCode,\n): ReadonlyMap<string, ReadonlySet<string>> => {\n const cached = aliasReferenceGraphCache.get(program);\n\n if (cached !== undefined) return cached;\n\n const mut_graph = new Map<string, ReadonlySet<string>>();\n\n walk(program, sourceCode, (visited) => {\n if (visited.type === AST_NODE_TYPES.TSTypeAliasDeclaration) {\n mut_graph.set(\n visited.id.name,\n referencedTypeNames(visited.typeAnnotation, sourceCode),\n );\n }\n });\n\n aliasReferenceGraphCache.set(program, mut_graph);\n\n return mut_graph;\n};\n\nconst aliasReferenceGraphCache = new WeakMap<\n TSESTree.Program,\n ReadonlyMap<string, ReadonlySet<string>>\n>();\n\nconst programOf = (node: TSESTree.Node): TSESTree.Program =>\n node.type === AST_NODE_TYPES.Program ? node : programOf(node.parent);\n\n/** The names every `X` / `X<…>` reference in the subtree points at. */\nconst referencedTypeNames = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n): ReadonlySet<string> => {\n const mut_names = new Set<string>();\n\n walk(node, sourceCode, (visited) => {\n if (\n visited.type === AST_NODE_TYPES.TSTypeReference &&\n visited.typeName.type === AST_NODE_TYPES.Identifier\n ) {\n mut_names.add(visited.typeName.name);\n }\n });\n\n return mut_names;\n};\n\n/**\n * Visits `node` and its descendants.\n *\n * Children are found through the parser's visitor keys, so only the properties\n * that actually hold one are read. Reading every own property instead works\n * too, and is the fallback for a parser that declares no keys for a node type,\n * but it allocates an entry pair per property and walks `loc` / `range` as\n * well — on the generated rule-type files that alone was most of the cost.\n */\nconst walk = (\n node: TSESTree.Node,\n sourceCode: TSESLint.SourceCode,\n visit: (visited: TSESTree.Node) => void,\n): void => {\n visit(node);\n\n if (!isRecord(node)) return;\n\n const keys =\n sourceCode.visitorKeys[node.type] ??\n Object.keys(node).filter((key) => key !== 'parent');\n\n for (const key of keys) {\n const value: unknown = hasKey(node, key) ? node[key] : undefined;\n\n if (Arr.isArray(value)) {\n for (const element of value) {\n if (isNode(element)) walk(element, sourceCode, visit);\n }\n } else if (isNode(value)) {\n walk(value, sourceCode, visit);\n }\n }\n};\n\nconst isNode = (value: unknown): value is TSESTree.Node =>\n isNonNullObject(value) &&\n typeof (value as Readonly<{ type?: unknown }>).type === 'string';\n\n/** Unwraps `V[]` and `readonly V[]` to `V`. */\nconst getArrayElementType = (\n node: TSESTree.TypeNode,\n): TSESTree.TypeNode | undefined => {\n if (node.type === AST_NODE_TYPES.TSArrayType) return node.elementType;\n\n return node.type === AST_NODE_TYPES.TSTypeOperator &&\n node.operator === 'readonly' &&\n node.typeAnnotation?.type === AST_NODE_TYPES.TSArrayType\n ? node.typeAnnotation.elementType\n : undefined;\n};\n\nconst normalizeWhitespace = (text: string): string =>\n text.replaceAll(/\\s+/gu, ' ').trim();\n"],"names":[],"mappings":";;;AAoBO,MAAM,cAAA,GAAiB,CAC5B,UAAA,EACA,IAAA,EACA,IAAA,KACY;AACZ,EAAA,MAAM,WAAW,QAAA,CAAS,YAAA,CAAa,WAAW,QAAA,CAAS,IAAI,GAAG,IAAI,CAAA;AAEtE,EAAA,OAAO,QAAA,KAAa,IAAA,IAAQ,GAAA,CAAI,UAAA,CAAW,SAAS,IAAI,CAAA;AAC1D;AAkCO,MAAM,mBAAA,GAAsB,CACjC,KAAA,EACA,UAAA,KACkC;AAClC,EAAA,IAAI,gCAAA,CAAiC,KAAK,CAAA,EAAG,OAAO,MAAA;AAEpD,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,OAAA,GAAU,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,QAAQ,CAAA,GAAI,QAAA;AAElD,EAAA,IACE,GAAA,CAAI,OAAA,CAAQ,OAAO,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,GAAA,CAAI,IAAA,CAAK,OAAO,CAAA;AAAA,IACnB,GAAI,eAAA,KAAoB,MAAA,GAAY,EAAC,GAAI,CAAC,eAAe;AAAA,GAC3D;AAEA,EAAA,IACE,MAAA,CAAO,IAAA;AAAA,IACL,CAAC,OAAA,KACC,mBAAA,CAAoB,WAAW,OAAA,CAAQ,OAAO,CAAC,CAAA,KAAM;AAAA,GACzD,EACA;AACA,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,yBAAA,CAA0B,KAAA,EAAO,UAAU,CAAA,GAC9C,MAAA,GACA;AAAA,IACE,IAAA,EAAM,aAAa,MAAA,GAAS,KAAA;AAAA,IAC5B,UAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAY,OAAA,CAAQ,MAAA;AAAA,IACpB;AAAA,GACF;AACN;AAaA,MAAM,gCAAA,GAAmC,CAAC,IAAA,KAAiC;AAEzE,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,OAAA,EAAS,OAAO,KAAA;AAEjD,EAAA,MAAM,EAAE,QAAO,GAAI,IAAA;AAEnB,EAAA,OACG,MAAA,CAAO,SAAS,cAAA,CAAe,iBAAA,IAC9B,OAAO,WAAA,KAAgB,IAAA,IACzB,iCAAiC,MAAM,CAAA;AAE3C,CAAA;AAyCA,MAAM,yBAAA,GAA4B,CAChC,KAAA,EACA,UAAA,KACY;AACZ,EAAA,MAAM,KAAA,GAAQ,mBAAmB,KAAK,CAAA;AAEtC,EAAA,IAAI,KAAA,KAAU,QAAW,OAAO,KAAA;AAEhC,EAAA,MAAM,UAAA,GAAa,mBAAA,CAAoB,SAAA,CAAU,KAAK,GAAG,UAAU,CAAA;AAEnE,EAAA,MAAM,QAAA,uBAAe,GAAA,EAAY;AAEjC,EAAA,MAAM,SAAA,GAAsB,MAAM,YAAA,CAAa,OAAA;AAAA,IAAQ,CAAC,OAAA,KACtD,KAAA,CAAM,KAAK,mBAAA,CAAoB,OAAA,EAAS,UAAU,CAAC;AAAA,GACrD;AAEA,EAAA,OAAO,IAAA,EAAM;AACX,IAAA,MAAM,IAAA,GAAO,UAAU,GAAA,EAAI;AAE3B,IAAA,IAAI,IAAA,KAAS,QAAW,OAAO,KAAA;AAE/B,IAAA,IAAI,IAAA,KAAS,KAAA,CAAM,EAAA,CAAG,IAAA,EAAM,OAAO,IAAA;AAEnC,IAAA,IAAI,QAAA,CAAS,GAAA,CAAI,IAAI,CAAA,EAAG;AAExB,IAAA,QAAA,CAAS,IAAI,IAAI,CAAA;AAEjB,IAAA,MAAM,UAAA,GAAa,UAAA,CAAW,GAAA,CAAI,IAAI,CAAA;AAEtC,IAAA,IAAI,eAAe,MAAA,EAAW;AAC5B,MAAA,SAAA,CAAU,IAAA,CAAK,GAAG,UAAU,CAAA;AAAA,IAC9B;AAAA,EACF;AACF,CAAA;AAGA,MAAM,kBAAA,GAAqB,CACzB,IAAA,KACgD;AAChD,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,sBAAA,EAAwB,OAAO,IAAA;AAEhE,EAAA,IAAI,IAAA,CAAK,IAAA,KAAS,cAAA,CAAe,OAAA,EAAS,OAAO,MAAA;AAEjD,EAAA,OAAO,kBAAA,CAAmB,KAAK,MAAM,CAAA;AACvC,CAAA;AASA,MAAM,mBAAA,GAAsB,CAC1B,OAAA,EACA,UAAA,KAC6C;AAC7C,EAAA,MAAM,MAAA,GAAS,wBAAA,CAAyB,GAAA,CAAI,OAAO,CAAA;AAEnD,EAAA,IAAI,MAAA,KAAW,QAAW,OAAO,MAAA;AAEjC,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAiC;AAEvD,EAAA,IAAA,CAAK,OAAA,EAAS,UAAA,EAAY,CAAC,OAAA,KAAY;AACrC,IAAA,IAAI,OAAA,CAAQ,IAAA,KAAS,cAAA,CAAe,sBAAA,EAAwB;AAC1D,MAAA,SAAA,CAAU,GAAA;AAAA,QACR,QAAQ,EAAA,CAAG,IAAA;AAAA,QACX,mBAAA,CAAoB,OAAA,CAAQ,cAAA,EAAgB,UAAU;AAAA,OACxD;AAAA,IACF;AAAA,EACF,CAAC,CAAA;AAED,EAAA,wBAAA,CAAyB,GAAA,CAAI,SAAS,SAAS,CAAA;AAE/C,EAAA,OAAO,SAAA;AACT,CAAA;AAEA,MAAM,wBAAA,uBAA+B,OAAA,EAGnC;AAEF,MAAM,SAAA,GAAY,CAAC,IAAA,KACjB,IAAA,CAAK,IAAA,KAAS,eAAe,OAAA,GAAU,IAAA,GAAO,SAAA,CAAU,IAAA,CAAK,MAAM,CAAA;AAGrE,MAAM,mBAAA,GAAsB,CAC1B,IAAA,EACA,UAAA,KACwB;AACxB,EAAA,MAAM,SAAA,uBAAgB,GAAA,EAAY;AAElC,EAAA,IAAA,CAAK,IAAA,EAAM,UAAA,EAAY,CAAC,OAAA,KAAY;AAClC,IAAA,IACE,OAAA,CAAQ,SAAS,cAAA,CAAe,eAAA,IAChC,QAAQ,QAAA,CAAS,IAAA,KAAS,eAAe,UAAA,EACzC;AACA,MAAA,SAAA,CAAU,GAAA,CAAI,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA;AAAA,IACrC;AAAA,EACF,CAAC,CAAA;AAED,EAAA,OAAO,SAAA;AACT,CAAA;AAWA,MAAM,IAAA,GAAO,CACX,IAAA,EACA,UAAA,EACA,KAAA,KACS;AACT,EAAA,KAAA,CAAM,IAAI,CAAA;AAEV,EAAA,IAAI,CAAC,QAAA,CAAS,IAAI,CAAA,EAAG;AAErB,EAAA,MAAM,IAAA,GACJ,UAAA,CAAW,WAAA,CAAY,IAAA,CAAK,IAAI,CAAA,IAChC,MAAA,CAAO,IAAA,CAAK,IAAI,CAAA,CAAE,MAAA,CAAO,CAAC,GAAA,KAAQ,QAAQ,QAAQ,CAAA;AAEpD,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,MAAM,QAAiB,MAAA,CAAO,IAAA,EAAM,GAAG,CAAA,GAAI,IAAA,CAAK,GAAG,CAAA,GAAI,MAAA;AAEvD,IAAA,IAAI,GAAA,CAAI,OAAA,CAAQ,KAAK,CAAA,EAAG;AACtB,MAAA,KAAA,MAAW,WAAW,KAAA,EAAO;AAC3B,QAAA,IAAI,OAAO,OAAO,CAAA,EAAG,IAAA,CAAK,OAAA,EAAS,YAAY,KAAK,CAAA;AAAA,MACtD;AAAA,IACF,CAAA,MAAA,IAAW,MAAA,CAAO,KAAK,CAAA,EAAG;AACxB,MAAA,IAAA,CAAK,KAAA,EAAO,YAAY,KAAK,CAAA;AAAA,IAC/B;AAAA,EACF;AACF,CAAA;AAEA,MAAM,MAAA,GAAS,CAAC,KAAA,KACd,eAAA,CAAgB,KAAK,CAAA,IACrB,OAAQ,MAAuC,IAAA,KAAS,QAAA;AAG1D,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,7 +1,7 @@
|
|
|
1
1
|
import { analyzeUniformTuple } from './ast-utils.mjs';
|
|
2
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
3
|
import { DEFAULT_IMPORT_STYLE, IMPORT_STYLE_SCHEMA_PROPERTY } from './import-style.mjs';
|
|
4
|
-
import {
|
|
4
|
+
import { reportTypeRewrites } from './rewrite-rule-utils.mjs';
|
|
5
5
|
|
|
6
6
|
const MIN_FIXED_LENGTH = 2;
|
|
7
7
|
const preferCanonicalLengthConstrainedTuple = {
|
|
@@ -53,7 +53,7 @@ const preferCanonicalLengthConstrainedTuple = {
|
|
|
53
53
|
});
|
|
54
54
|
},
|
|
55
55
|
"Program:exit": () => {
|
|
56
|
-
|
|
56
|
+
reportTypeRewrites(
|
|
57
57
|
context,
|
|
58
58
|
mut_rewrites,
|
|
59
59
|
"useCanonicalTuple",
|
|
@@ -1 +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} from './import-style.mjs';\nimport {
|
|
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} from './import-style.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-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: TypeRewrite[] = [];\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 reportTypeRewrites(\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":";;;;;AA8BA,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,eAA8B,EAAC;AAErC,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,kBAAA;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;;;;"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
type Options = readonly [
|
|
3
|
+
Readonly<{
|
|
4
|
+
importStyle?: 'global' | 'named';
|
|
5
|
+
}>?
|
|
6
|
+
];
|
|
7
|
+
type MessageIds = 'useMutableRecord';
|
|
8
|
+
export declare const preferCanonicalMutableRecord: TSESLint.RuleModule<MessageIds, Options>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=prefer-canonical-mutable-record.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-canonical-mutable-record.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-canonical-mutable-record.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAalC,KAAK,OAAO,GAAG,SAAS;IACtB,QAAQ,CAAC;QACP,WAAW,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;KAClC,CAAC,CAAC;CACJ,CAAC;AAEF,KAAK,UAAU,GAAG,kBAAkB,CAAC;AAiBrC,eAAO,MAAM,4BAA4B,EAAE,QAAQ,CAAC,UAAU,CAC5D,UAAU,EACV,OAAO,CA4GC,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from '@typescript-eslint/utils';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import { isLocallyBound } from './ast-utils.mjs';
|
|
4
|
+
import { DEFAULT_IMPORT_STYLE, IMPORT_STYLE_SCHEMA_PROPERTY } from './import-style.mjs';
|
|
5
|
+
import { getTsTypeForgeImports, getImportedLocalName } from './import-utils.mjs';
|
|
6
|
+
import { reportTypeRewrites } from './rewrite-rule-utils.mjs';
|
|
7
|
+
|
|
8
|
+
const WRAPPED_RECORD_NAMES = [
|
|
9
|
+
"Record",
|
|
10
|
+
"ReadonlyRecord",
|
|
11
|
+
"MutableRecord"
|
|
12
|
+
];
|
|
13
|
+
const MUTABLE_TYPE_NAME = "Mutable";
|
|
14
|
+
const MUTABLE_RECORD_TYPE_NAME = "MutableRecord";
|
|
15
|
+
const preferCanonicalMutableRecord = {
|
|
16
|
+
meta: {
|
|
17
|
+
type: "suggestion",
|
|
18
|
+
docs: {
|
|
19
|
+
description: "Replace `Mutable<Record<K, V>>` (and `Mutable` applied to `ReadonlyRecord` / `MutableRecord`) with the canonical ts-type-forge `MutableRecord<K, V>`."
|
|
20
|
+
},
|
|
21
|
+
fixable: "code",
|
|
22
|
+
schema: [
|
|
23
|
+
{
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
importStyle: IMPORT_STYLE_SCHEMA_PROPERTY
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
messages: {
|
|
32
|
+
useMutableRecord: "Replace `{{original}}` with `{{replacement}}` from ts-type-forge."
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
create: (context) => {
|
|
36
|
+
const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;
|
|
37
|
+
const sourceCode = context.sourceCode;
|
|
38
|
+
const tsTypeForgeImports = getTsTypeForgeImports(sourceCode.ast);
|
|
39
|
+
const referencesTsTypeForgeType = (node, canonicalName) => {
|
|
40
|
+
const typeName = node.typeName;
|
|
41
|
+
if (typeName.type !== AST_NODE_TYPES.Identifier) return false;
|
|
42
|
+
return typeName.name === getImportedLocalName(tsTypeForgeImports, canonicalName) || typeName.name === canonicalName && !isLocallyBound(sourceCode, node, canonicalName);
|
|
43
|
+
};
|
|
44
|
+
const mut_rewrites = [];
|
|
45
|
+
return {
|
|
46
|
+
TSTypeReference: (node) => {
|
|
47
|
+
if (!referencesTsTypeForgeType(node, MUTABLE_TYPE_NAME)) return;
|
|
48
|
+
const wrapped = node.typeArguments?.params;
|
|
49
|
+
if (wrapped === void 0 || !Arr.isFixedLengthArray(1, wrapped)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const record = wrapped[0];
|
|
53
|
+
if (record.type !== AST_NODE_TYPES.TSTypeReference || WRAPPED_RECORD_NAMES.every(
|
|
54
|
+
(name) => !referencesTsTypeForgeType(record, name)
|
|
55
|
+
)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const recordTypeArgs = record.typeArguments?.params;
|
|
59
|
+
if (recordTypeArgs === void 0 || !Arr.isFixedLengthArray(2, recordTypeArgs)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const [K, V] = recordTypeArgs;
|
|
63
|
+
mut_rewrites.push({
|
|
64
|
+
node,
|
|
65
|
+
canonicalName: MUTABLE_RECORD_TYPE_NAME,
|
|
66
|
+
typeArgs: `${sourceCode.getText(K)}, ${sourceCode.getText(V)}`
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
"Program:exit": () => {
|
|
70
|
+
reportTypeRewrites(
|
|
71
|
+
context,
|
|
72
|
+
mut_rewrites,
|
|
73
|
+
"useMutableRecord",
|
|
74
|
+
importStyle
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }]
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export { preferCanonicalMutableRecord };
|
|
83
|
+
//# sourceMappingURL=prefer-canonical-mutable-record.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prefer-canonical-mutable-record.mjs","sources":["../../src/rules/prefer-canonical-mutable-record.mts"],"sourcesContent":["import {\n AST_NODE_TYPES,\n type TSESLint,\n type TSESTree,\n} from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { isLocallyBound } from './ast-utils.mjs';\nimport {\n DEFAULT_IMPORT_STYLE,\n IMPORT_STYLE_SCHEMA_PROPERTY,\n} from './import-style.mjs';\nimport {\n getImportedLocalName,\n getTsTypeForgeImports,\n} from './import-utils.mjs';\nimport { reportTypeRewrites, type TypeRewrite } from './rewrite-rule-utils.mjs';\n\ntype Options = readonly [\n Readonly<{\n importStyle?: 'global' | 'named';\n }>?,\n];\n\ntype MessageIds = 'useMutableRecord';\n\n/**\n * The record spellings that `Mutable<…>` collapses to `MutableRecord<K, V>`.\n * `Mutable` strips the `readonly` modifier from every property, so applied to\n * any of these it produces exactly the type `MutableRecord` spells directly.\n */\nconst WRAPPED_RECORD_NAMES = [\n 'Record',\n 'ReadonlyRecord',\n 'MutableRecord',\n] as const;\n\nconst MUTABLE_TYPE_NAME = 'Mutable';\n\nconst MUTABLE_RECORD_TYPE_NAME = 'MutableRecord';\n\nexport const preferCanonicalMutableRecord: TSESLint.RuleModule<\n MessageIds,\n Options\n> = {\n meta: {\n type: 'suggestion',\n docs: {\n description:\n 'Replace `Mutable<Record<K, V>>` (and `Mutable` applied to `ReadonlyRecord` / `MutableRecord`) with the canonical ts-type-forge `MutableRecord<K, V>`.',\n },\n fixable: 'code',\n schema: [\n {\n type: 'object',\n properties: {\n importStyle: IMPORT_STYLE_SCHEMA_PROPERTY,\n },\n additionalProperties: false,\n },\n ],\n messages: {\n useMutableRecord:\n 'Replace `{{original}}` with `{{replacement}}` from ts-type-forge.',\n },\n },\n\n create: (context) => {\n const importStyle = context.options[0]?.importStyle ?? DEFAULT_IMPORT_STYLE;\n\n const sourceCode = context.sourceCode;\n\n const tsTypeForgeImports = getTsTypeForgeImports(sourceCode.ast);\n\n /**\n * Whether this reference denotes the ts-type-forge type `canonicalName` —\n * either through an import from ts-type-forge (aliases included) or as the\n * bare ambient global. A name bound to anything else in this file (a local\n * alias, an import from another module) denotes that declaration instead.\n * The built-in `Record` goes through the same check: ts-type-forge exports\n * no `Record`, so only the unbound global spelling matches.\n */\n const referencesTsTypeForgeType = (\n // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types\n node: TSESTree.TSTypeReference,\n canonicalName: string,\n ): boolean => {\n const typeName = node.typeName;\n\n // `ns.Mutable` / `import('…').Mutable` never denote the ts-type-forge type.\n if (typeName.type !== AST_NODE_TYPES.Identifier) return false;\n\n return (\n typeName.name ===\n getImportedLocalName(tsTypeForgeImports, canonicalName) ||\n (typeName.name === canonicalName &&\n !isLocallyBound(sourceCode, node, canonicalName))\n );\n };\n\n const mut_rewrites: TypeRewrite[] = [];\n\n return {\n TSTypeReference: (node) => {\n if (!referencesTsTypeForgeType(node, MUTABLE_TYPE_NAME)) return;\n\n const wrapped = node.typeArguments?.params;\n\n if (wrapped === undefined || !Arr.isFixedLengthArray(1, wrapped)) {\n return;\n }\n\n const record = wrapped[0];\n\n if (\n record.type !== AST_NODE_TYPES.TSTypeReference ||\n WRAPPED_RECORD_NAMES.every(\n (name) => !referencesTsTypeForgeType(record, name),\n )\n ) {\n return;\n }\n\n const recordTypeArgs = record.typeArguments?.params;\n\n if (\n recordTypeArgs === undefined ||\n !Arr.isFixedLengthArray(2, recordTypeArgs)\n ) {\n return;\n }\n\n const [K, V] = recordTypeArgs;\n\n mut_rewrites.push({\n node,\n canonicalName: MUTABLE_RECORD_TYPE_NAME,\n typeArgs: `${sourceCode.getText(K)}, ${sourceCode.getText(V)}`,\n });\n },\n 'Program:exit': () => {\n reportTypeRewrites(\n context,\n mut_rewrites,\n 'useMutableRecord',\n importStyle,\n );\n },\n };\n },\n defaultOptions: [{ importStyle: DEFAULT_IMPORT_STYLE }],\n} as const;\n"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { type ImportStyle } from './import-style.mjs';
|
|
3
|
-
|
|
3
|
+
/** One deterministic replacement of a type node by a named ts-type-forge type. */
|
|
4
|
+
export type TypeRewrite = Readonly<{
|
|
4
5
|
node: TSESTree.Node;
|
|
5
6
|
canonicalName: string;
|
|
6
7
|
/** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */
|
|
@@ -15,5 +16,5 @@ export type TupleRewrite = Readonly<{
|
|
|
15
16
|
* span — so spreading them across reports makes the fixes conflict and only one
|
|
16
17
|
* survives per pass.
|
|
17
18
|
*/
|
|
18
|
-
export declare const
|
|
19
|
-
//# sourceMappingURL=
|
|
19
|
+
export declare const reportTypeRewrites: <MessageIds extends string>(context: TSESLint.RuleContext<MessageIds, readonly unknown[]>, rewrites: readonly TypeRewrite[], messageId: MessageIds, importStyle: ImportStyle) => void;
|
|
20
|
+
//# sourceMappingURL=rewrite-rule-utils.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/rewrite-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKvE,kFAAkF;AAClF,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,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,kBAAkB,GAAI,UAAU,SAAS,MAAM,WACjD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,YACnD,SAAS,WAAW,EAAE,aACrB,UAAU,eACR,WAAW,KACvB,IAuCF,CAAC"}
|
|
@@ -2,7 +2,7 @@ import { Arr } from 'ts-data-forge';
|
|
|
2
2
|
import { resolveTypeName } from './import-style.mjs';
|
|
3
3
|
import { buildTypeImportFix } from './import-utils.mjs';
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const reportTypeRewrites = (context, rewrites, messageId, importStyle) => {
|
|
6
6
|
const sourceCode = context.sourceCode;
|
|
7
7
|
const program = sourceCode.ast;
|
|
8
8
|
const resolved = rewrites.flatMap((rewrite) => {
|
|
@@ -30,5 +30,5 @@ const reportTupleRewrites = (context, rewrites, messageId, importStyle) => {
|
|
|
30
30
|
}
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export {
|
|
34
|
-
//# sourceMappingURL=
|
|
33
|
+
export { reportTypeRewrites };
|
|
34
|
+
//# sourceMappingURL=rewrite-rule-utils.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rewrite-rule-utils.mjs","sources":["../../src/rules/rewrite-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\n/** One deterministic replacement of a type node by a named ts-type-forge type. */\nexport type TypeRewrite = Readonly<{\n node: TSESTree.Node;\n canonicalName: string;\n /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */\n typeArgs: string;\n}>;\n\n/**\n * Emits one report per rewrite, resolving each target name and attaching every\n * required import to the *first* report.\n *\n * All import fixes must live on a single report: `insertTextBefore(program, …)`\n * produces overlapping ranges, and ESLint merges each report's fixes into one\n * span — so spreading them across reports makes the fixes conflict and only one\n * survives per pass.\n */\nexport const reportTypeRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TypeRewrite[],\n messageId: MessageIds,\n importStyle: ImportStyle,\n): void => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const resolved = rewrites.flatMap((rewrite) => {\n const target = resolveTypeName(program, rewrite.canonicalName, importStyle);\n\n // The name is already bound to something else in this file, so the rewrite\n // would resolve to the wrong declaration.\n return target === undefined ? [] : [{ rewrite, target }];\n });\n\n const importNames = Array.from(\n new Set(\n resolved\n .filter(({ target }) => target.needsImport)\n .map(({ rewrite }) => rewrite.canonicalName),\n ),\n );\n\n for (const [index, { rewrite, target }] of resolved.entries()) {\n const replacement = `${target.localName}<${rewrite.typeArgs}>`;\n\n context.report({\n node: rewrite.node,\n messageId,\n data: { original: sourceCode.getText(rewrite.node), replacement },\n fix: (fixer) =>\n Arr.toPushed(\n index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : [],\n fixer.replaceText(rewrite.node, replacement),\n ),\n });\n }\n};\n"],"names":[],"mappings":";;;;AAwBO,MAAM,kBAAA,GAAqB,CAChC,OAAA,EACA,QAAA,EACA,WACA,WAAA,KACS;AACT,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,EAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC7C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,eAAe,WAAW,CAAA;AAI1E,IAAA,OAAO,MAAA,KAAW,SAAY,EAAC,GAAI,CAAC,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA;AAAA,IACxB,IAAI,GAAA;AAAA,MACF,QAAA,CACG,MAAA,CAAO,CAAC,EAAE,QAAO,KAAM,MAAA,CAAO,WAAW,CAAA,CACzC,IAAI,CAAC,EAAE,OAAA,EAAQ,KAAM,QAAQ,aAAa;AAAA;AAC/C,GACF;AAEA,EAAA,KAAA,MAAW,CAAC,OAAO,EAAE,OAAA,EAAS,QAAQ,CAAA,IAAK,QAAA,CAAS,OAAA,EAAQ,EAAG;AAC7D,IAAA,MAAM,cAAc,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,CAAA,EAAI,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAE3D,IAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,MACb,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA;AAAA,MACA,IAAA,EAAM,EAAE,QAAA,EAAU,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAI,GAAG,WAAA,EAAY;AAAA,MAChE,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,QACF,KAAA,KAAU,IACN,WAAA,CAAY,OAAA;AAAA,UAAQ,CAAC,IAAA,KACnB,kBAAA,CAAmB,KAAA,EAAO,SAAS,IAAI;AAAA,YAEzC,EAAC;AAAA,QACL,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,WAAW;AAAA;AAC7C,KACH,CAAA;AAAA,EACH;AACF;;;;"}
|
package/dist/rules/rules.d.mts
CHANGED
|
@@ -3,6 +3,9 @@ export declare const tsTypeForgeRules: {
|
|
|
3
3
|
importStyle?: 'global' | 'named';
|
|
4
4
|
maxLength?: number;
|
|
5
5
|
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
6
|
+
readonly 'prefer-canonical-mutable-record': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useMutableRecord", readonly [(Readonly<{
|
|
7
|
+
importStyle?: 'global' | 'named';
|
|
8
|
+
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
6
9
|
readonly 'prefer-readonly-or-mutable-record': import("@typescript-eslint/utils/ts-eslint").RuleModule<"replaceWith" | "useReadonlyOrMutableRecord", readonly [(Readonly<{
|
|
7
10
|
importStyle?: 'global' | 'named';
|
|
8
11
|
}> | undefined)?], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.d.mts","sourceRoot":"","sources":["../../src/rules/rules.mts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;CAMa,CAAC"}
|
package/dist/rules/rules.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
|
|
2
|
+
import { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';
|
|
2
3
|
import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
|
|
3
4
|
import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
|
|
4
5
|
|
|
5
6
|
const tsTypeForgeRules = {
|
|
6
7
|
"prefer-canonical-length-constrained-tuple": preferCanonicalLengthConstrainedTuple,
|
|
8
|
+
"prefer-canonical-mutable-record": preferCanonicalMutableRecord,
|
|
7
9
|
"prefer-readonly-or-mutable-record": preferReadonlyOrMutableRecord,
|
|
8
10
|
"prefer-strict-or-relaxed-utility-type": preferStrictOrRelaxedUtilityType
|
|
9
11
|
};
|
package/dist/rules/rules.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.mjs","sources":["../../src/rules/rules.mts"],"sourcesContent":["import { type ESLintPlugin } from '../types.mjs';\nimport { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';\nimport { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';\nimport { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n 'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,\n 'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":"
|
|
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';\nimport { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';\nimport { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';\nimport { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';\n\nexport const tsTypeForgeRules = {\n 'prefer-canonical-length-constrained-tuple':\n preferCanonicalLengthConstrainedTuple,\n 'prefer-canonical-mutable-record': preferCanonicalMutableRecord,\n 'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,\n 'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,\n} as const satisfies ESLintPlugin['rules'];\n"],"names":[],"mappings":";;;;;AAMO,MAAM,gBAAA,GAAmB;AAAA,EAC9B,2CAAA,EACE,qCAAA;AAAA,EACF,iCAAA,EAAmC,4BAAA;AAAA,EACnC,mCAAA,EAAqC,6BAAA;AAAA,EACrC,uCAAA,EAAyC;AAC3C;;;;"}
|