eslint-plugin-ts-type-forge 0.6.6 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +23 -0
  2. package/dist/plugin.d.mts +4 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +39 -0
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rules/ast-utils.d.mts +11 -0
  9. package/dist/rules/ast-utils.d.mts.map +1 -1
  10. package/dist/rules/ast-utils.mjs +81 -7
  11. package/dist/rules/ast-utils.mjs.map +1 -1
  12. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs +2 -2
  13. package/dist/rules/prefer-canonical-length-constrained-tuple.mjs.map +1 -1
  14. package/dist/rules/prefer-canonical-mutable-record.d.mts +10 -0
  15. package/dist/rules/prefer-canonical-mutable-record.d.mts.map +1 -0
  16. package/dist/rules/prefer-canonical-mutable-record.mjs +83 -0
  17. package/dist/rules/prefer-canonical-mutable-record.mjs.map +1 -0
  18. package/dist/rules/{tuple-rule-utils.d.mts → rewrite-rule-utils.d.mts} +4 -3
  19. package/dist/rules/rewrite-rule-utils.d.mts.map +1 -0
  20. package/dist/rules/{tuple-rule-utils.mjs → rewrite-rule-utils.mjs} +3 -3
  21. package/dist/rules/rewrite-rule-utils.mjs.map +1 -0
  22. package/dist/rules/rules.d.mts +3 -0
  23. package/dist/rules/rules.d.mts.map +1 -1
  24. package/dist/rules/rules.mjs +2 -0
  25. package/dist/rules/rules.mjs.map +1 -1
  26. package/dist/rules/std-type-rule-utils.d.mts.map +1 -1
  27. package/dist/rules/std-type-rule-utils.mjs +2 -5
  28. package/dist/rules/std-type-rule-utils.mjs.map +1 -1
  29. package/package.json +7 -7
  30. package/src/plugin.mts +1 -0
  31. package/src/rule-types.mts +46 -0
  32. package/src/rules/ast-utils.mts +217 -7
  33. package/src/rules/prefer-canonical-length-constrained-tuple.mts +3 -3
  34. package/src/rules/prefer-canonical-length-constrained-tuple.test.mts +71 -0
  35. package/src/rules/prefer-canonical-mutable-record.mts +151 -0
  36. package/src/rules/prefer-canonical-mutable-record.test.mts +164 -0
  37. package/src/rules/{tuple-rule-utils.mts → rewrite-rule-utils.mts} +4 -3
  38. package/src/rules/rules.mts +2 -0
  39. package/src/rules/std-type-rule-utils.mts +1 -21
  40. package/dist/rules/tuple-rule-utils.d.mts.map +0 -1
  41. package/dist/rules/tuple-rule-utils.mjs.map +0 -1
@@ -0,0 +1,164 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: { ecmaVersion: 2020, sourceType: 'module' },
10
+ },
11
+ });
12
+
13
+ describe('prefer-canonical-mutable-record', () => {
14
+ tester.run('prefer-canonical-mutable-record', preferCanonicalMutableRecord, {
15
+ valid: [
16
+ {
17
+ name: 'accepts the canonical spelling',
18
+ code: 'type A = MutableRecord<string, number>;',
19
+ },
20
+ {
21
+ name: 'ignores `Mutable` applied to a non-record type',
22
+ code: 'type A = Mutable<{ a: number }>;',
23
+ },
24
+ {
25
+ name: 'ignores a bare `Record` (prefer-readonly-or-mutable-record owns it)',
26
+ code: 'type A = Record<string, number>;',
27
+ },
28
+ {
29
+ name: 'ignores a qualified `Mutable`',
30
+ code: 'type A = Lib.Mutable<Record<string, number>>;',
31
+ },
32
+ {
33
+ name: 'ignores a locally declared `Mutable`',
34
+ code: dedent`
35
+ type Mutable<T> = { -readonly [P in keyof T]?: T[P] };
36
+ type A = Mutable<Record<string, number>>;
37
+ `,
38
+ },
39
+ {
40
+ name: 'ignores a `Mutable` imported from another module',
41
+ code: dedent`
42
+ import { type Mutable } from './my-types.mjs';
43
+ type A = Mutable<Record<string, number>>;
44
+ `,
45
+ },
46
+ {
47
+ name: 'ignores a wrapped `Record` that is declared locally',
48
+ code: dedent`
49
+ type Record<K extends PropertyKey, V> = { readonly [P in K]: V };
50
+ type A = Mutable<Record<string, number>>;
51
+ `,
52
+ },
53
+ {
54
+ name: 'stays silent when `MutableRecord` is bound to another module',
55
+ code: dedent`
56
+ import { type MutableRecord } from './my-types.mjs';
57
+ type A = Mutable<Record<string, number>>;
58
+ `,
59
+ },
60
+ ],
61
+ invalid: [
62
+ {
63
+ name: 'rewrites `Mutable<Record<K, V>>`',
64
+ code: 'type A = Mutable<Record<string, number>>;',
65
+ output: dedent`
66
+ import { type MutableRecord } from 'ts-type-forge';
67
+ type A = MutableRecord<string, number>;
68
+ `,
69
+ errors: [
70
+ {
71
+ messageId: 'useMutableRecord',
72
+ data: {
73
+ original: 'Mutable<Record<string, number>>',
74
+ replacement: 'MutableRecord<string, number>',
75
+ },
76
+ },
77
+ ],
78
+ },
79
+ {
80
+ name: 'rewrites `Mutable<ReadonlyRecord<K, V>>`',
81
+ code: "type A = Mutable<ReadonlyRecord<'a' | 'b', number>>;",
82
+ output: dedent`
83
+ import { type MutableRecord } from 'ts-type-forge';
84
+ type A = MutableRecord<'a' | 'b', number>;
85
+ `,
86
+ errors: [{ messageId: 'useMutableRecord' }],
87
+ },
88
+ {
89
+ name: 'unwraps the redundant `Mutable<MutableRecord<K, V>>`',
90
+ code: 'type A = Mutable<MutableRecord<string, number>>;',
91
+ output: dedent`
92
+ import { type MutableRecord } from 'ts-type-forge';
93
+ type A = MutableRecord<string, number>;
94
+ `,
95
+ errors: [{ messageId: 'useMutableRecord' }],
96
+ },
97
+ {
98
+ name: 'touches no import under the `global` import style',
99
+ options: [{ importStyle: 'global' }],
100
+ code: 'type A = Mutable<Record<string, number>>;',
101
+ output: 'type A = MutableRecord<string, number>;',
102
+ errors: [{ messageId: 'useMutableRecord' }],
103
+ },
104
+ {
105
+ name: 'adds the import once for multiple rewrites',
106
+ code: dedent`
107
+ type A = Mutable<Record<string, number>>;
108
+ type B = Mutable<ReadonlyRecord<string, boolean>>;
109
+ `,
110
+ output: dedent`
111
+ import { type MutableRecord } from 'ts-type-forge';
112
+ type A = MutableRecord<string, number>;
113
+ type B = MutableRecord<string, boolean>;
114
+ `,
115
+ errors: [
116
+ { messageId: 'useMutableRecord' },
117
+ { messageId: 'useMutableRecord' },
118
+ ],
119
+ },
120
+ {
121
+ name: 'follows ts-type-forge import aliases on both sides',
122
+ code: dedent`
123
+ import { type Mutable as Mut, type MutableRecord as MRecord } from 'ts-type-forge';
124
+ type A = Mut<Record<string, number>>;
125
+ `,
126
+ output: dedent`
127
+ import { type Mutable as Mut, type MutableRecord as MRecord } from 'ts-type-forge';
128
+ type A = MRecord<string, number>;
129
+ `,
130
+ errors: [
131
+ {
132
+ messageId: 'useMutableRecord',
133
+ data: {
134
+ original: 'Mut<Record<string, number>>',
135
+ replacement: 'MRecord<string, number>',
136
+ },
137
+ },
138
+ ],
139
+ },
140
+ {
141
+ name: 'rewrites the outer occurrence first when nested',
142
+ options: [{ importStyle: 'global' }],
143
+ code: 'type A = Mutable<Record<string, Mutable<Record<string, number>>>>;',
144
+ // The two fixes overlap, so the first pass applies only the outer one
145
+ // and the next pass reaches the fixed point.
146
+ output: [
147
+ 'type A = MutableRecord<string, Mutable<Record<string, number>>>;',
148
+ 'type A = MutableRecord<string, MutableRecord<string, number>>;',
149
+ ],
150
+ errors: [
151
+ { messageId: 'useMutableRecord' },
152
+ { messageId: 'useMutableRecord' },
153
+ ],
154
+ },
155
+ {
156
+ name: 'rewrites inside a larger type',
157
+ options: [{ importStyle: 'global' }],
158
+ code: 'type A = readonly Mutable<Record<string, number>>[];',
159
+ output: 'type A = readonly MutableRecord<string, number>[];',
160
+ errors: [{ messageId: 'useMutableRecord' }],
161
+ },
162
+ ],
163
+ });
164
+ });
@@ -5,7 +5,8 @@ import { buildTypeImportFix } from './import-utils.mjs';
5
5
 
6
6
  /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
7
7
 
8
- export type TupleRewrite = Readonly<{
8
+ /** One deterministic replacement of a type node by a named ts-type-forge type. */
9
+ export type TypeRewrite = Readonly<{
9
10
  node: TSESTree.Node;
10
11
  canonicalName: string;
11
12
  /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */
@@ -21,9 +22,9 @@ export type TupleRewrite = Readonly<{
21
22
  * span — so spreading them across reports makes the fixes conflict and only one
22
23
  * survives per pass.
23
24
  */
24
- export const reportTupleRewrites = <MessageIds extends string>(
25
+ export const reportTypeRewrites = <MessageIds extends string>(
25
26
  context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,
26
- rewrites: readonly TupleRewrite[],
27
+ rewrites: readonly TypeRewrite[],
27
28
  messageId: MessageIds,
28
29
  importStyle: ImportStyle,
29
30
  ): void => {
@@ -1,11 +1,13 @@
1
1
  import { type ESLintPlugin } from '../types.mjs';
2
2
  import { preferCanonicalLengthConstrainedTuple } from './prefer-canonical-length-constrained-tuple.mjs';
3
+ import { preferCanonicalMutableRecord } from './prefer-canonical-mutable-record.mjs';
3
4
  import { preferReadonlyOrMutableRecord } from './prefer-readonly-or-mutable-record.mjs';
4
5
  import { preferStrictOrRelaxedUtilityType } from './prefer-strict-or-relaxed-utility-type.mjs';
5
6
 
6
7
  export const tsTypeForgeRules = {
7
8
  'prefer-canonical-length-constrained-tuple':
8
9
  preferCanonicalLengthConstrainedTuple,
10
+ 'prefer-canonical-mutable-record': preferCanonicalMutableRecord,
9
11
  'prefer-readonly-or-mutable-record': preferReadonlyOrMutableRecord,
10
12
  'prefer-strict-or-relaxed-utility-type': preferStrictOrRelaxedUtilityType,
11
13
  } as const satisfies ESLintPlugin['rules'];
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  AST_NODE_TYPES,
3
- ASTUtils,
4
3
  type TSESLint,
5
4
  type TSESTree,
6
5
  } from '@typescript-eslint/utils';
7
6
  import { Arr } from 'ts-data-forge';
7
+ import { isLocallyBound } from './ast-utils.mjs';
8
8
  import { resolveTypeName, type ImportStyle } from './import-style.mjs';
9
9
  import { buildTypeImportFix } from './import-utils.mjs';
10
10
 
@@ -81,23 +81,3 @@ export const reportStdTypeReference = <MessageIds extends string>(
81
81
  /** `['StrictPick', 'RelaxedPick']` → `` '`StrictPick` or `RelaxedPick`' ``. */
82
82
  const formatAlternatives = (candidates: readonly string[]): string =>
83
83
  candidates.map((candidate) => `\`${candidate}\``).join(' or ');
84
-
85
- /**
86
- * Whether `name` resolves to a declaration in this file (a type alias, an
87
- * interface, a class, an import, …) rather than to the ambient standard-library
88
- * type of the same name.
89
- *
90
- * The `defs` check is what separates the two: ESLint materializes an *implicit*
91
- * global variable for every unresolved reference, so `findVariable` also
92
- * succeeds for `Exclude` in a file that never declares it. Only a variable with
93
- * at least one definition was actually written down somewhere in this file.
94
- */
95
- const isLocallyBound = (
96
- sourceCode: TSESLint.SourceCode,
97
- node: TSESTree.Node,
98
- name: string,
99
- ): boolean => {
100
- const variable = ASTUtils.findVariable(sourceCode.getScope(node), name);
101
-
102
- return variable !== null && Arr.isNonEmpty(variable.defs);
103
- };
@@ -1 +0,0 @@
1
- {"version":3,"file":"tuple-rule-utils.d.mts","sourceRoot":"","sources":["../../src/rules/tuple-rule-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAExE,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAKvE,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,SAAS,MAAM,WAClD,QAAQ,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,OAAO,EAAE,CAAC,YACnD,SAAS,YAAY,EAAE,aACtB,UAAU,eACR,WAAW,KACvB,IAuCF,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"tuple-rule-utils.mjs","sources":["../../src/rules/tuple-rule-utils.mts"],"sourcesContent":["import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';\nimport { Arr } from 'ts-data-forge';\nimport { resolveTypeName, type ImportStyle } from './import-style.mjs';\nimport { buildTypeImportFix } from './import-utils.mjs';\n\n/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */\n\nexport type TupleRewrite = Readonly<{\n node: TSESTree.Node;\n canonicalName: string;\n /** Text inside the `<...>` of the replacement, e.g. `'2, string'`. */\n typeArgs: string;\n}>;\n\n/**\n * Emits one report per rewrite, resolving each target name and attaching every\n * required import to the *first* report.\n *\n * All import fixes must live on a single report: `insertTextBefore(program, …)`\n * produces overlapping ranges, and ESLint merges each report's fixes into one\n * span — so spreading them across reports makes the fixes conflict and only one\n * survives per pass.\n */\nexport const reportTupleRewrites = <MessageIds extends string>(\n context: TSESLint.RuleContext<MessageIds, readonly unknown[]>,\n rewrites: readonly TupleRewrite[],\n messageId: MessageIds,\n importStyle: ImportStyle,\n): void => {\n const sourceCode = context.sourceCode;\n\n const program = sourceCode.ast;\n\n const resolved = rewrites.flatMap((rewrite) => {\n const target = resolveTypeName(program, rewrite.canonicalName, importStyle);\n\n // The name is already bound to something else in this file, so the rewrite\n // would resolve to the wrong declaration.\n return target === undefined ? [] : [{ rewrite, target }];\n });\n\n const importNames = Array.from(\n new Set(\n resolved\n .filter(({ target }) => target.needsImport)\n .map(({ rewrite }) => rewrite.canonicalName),\n ),\n );\n\n for (const [index, { rewrite, target }] of resolved.entries()) {\n const replacement = `${target.localName}<${rewrite.typeArgs}>`;\n\n context.report({\n node: rewrite.node,\n messageId,\n data: { original: sourceCode.getText(rewrite.node), replacement },\n fix: (fixer) =>\n Arr.toPushed(\n index === 0\n ? importNames.flatMap((name) =>\n buildTypeImportFix(fixer, program, name),\n )\n : [],\n fixer.replaceText(rewrite.node, replacement),\n ),\n });\n }\n};\n"],"names":[],"mappings":";;;;AAuBO,MAAM,mBAAA,GAAsB,CACjC,OAAA,EACA,QAAA,EACA,WACA,WAAA,KACS;AACT,EAAA,MAAM,aAAa,OAAA,CAAQ,UAAA;AAE3B,EAAA,MAAM,UAAU,UAAA,CAAW,GAAA;AAE3B,EAAA,MAAM,QAAA,GAAW,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC7C,IAAA,MAAM,MAAA,GAAS,eAAA,CAAgB,OAAA,EAAS,OAAA,CAAQ,eAAe,WAAW,CAAA;AAI1E,IAAA,OAAO,MAAA,KAAW,SAAY,EAAC,GAAI,CAAC,EAAE,OAAA,EAAS,QAAQ,CAAA;AAAA,EACzD,CAAC,CAAA;AAED,EAAA,MAAM,cAAc,KAAA,CAAM,IAAA;AAAA,IACxB,IAAI,GAAA;AAAA,MACF,QAAA,CACG,MAAA,CAAO,CAAC,EAAE,QAAO,KAAM,MAAA,CAAO,WAAW,CAAA,CACzC,IAAI,CAAC,EAAE,OAAA,EAAQ,KAAM,QAAQ,aAAa;AAAA;AAC/C,GACF;AAEA,EAAA,KAAA,MAAW,CAAC,OAAO,EAAE,OAAA,EAAS,QAAQ,CAAA,IAAK,QAAA,CAAS,OAAA,EAAQ,EAAG;AAC7D,IAAA,MAAM,cAAc,CAAA,EAAG,MAAA,CAAO,SAAS,CAAA,CAAA,EAAI,QAAQ,QAAQ,CAAA,CAAA,CAAA;AAE3D,IAAA,OAAA,CAAQ,MAAA,CAAO;AAAA,MACb,MAAM,OAAA,CAAQ,IAAA;AAAA,MACd,SAAA;AAAA,MACA,IAAA,EAAM,EAAE,QAAA,EAAU,UAAA,CAAW,QAAQ,OAAA,CAAQ,IAAI,GAAG,WAAA,EAAY;AAAA,MAChE,GAAA,EAAK,CAAC,KAAA,KACJ,GAAA,CAAI,QAAA;AAAA,QACF,KAAA,KAAU,IACN,WAAA,CAAY,OAAA;AAAA,UAAQ,CAAC,IAAA,KACnB,kBAAA,CAAmB,KAAA,EAAO,SAAS,IAAI;AAAA,YAEzC,EAAC;AAAA,QACL,KAAA,CAAM,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,WAAW;AAAA;AAC7C,KACH,CAAA;AAAA,EACH;AACF;;;;"}