ts-codemod-lib 1.0.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 (149) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +33 -0
  3. package/dist/cmd/convert-to-readonly.d.mts +3 -0
  4. package/dist/cmd/convert-to-readonly.d.mts.map +1 -0
  5. package/dist/cmd/convert-to-readonly.mjs +137 -0
  6. package/dist/cmd/convert-to-readonly.mjs.map +1 -0
  7. package/dist/entry-point.d.mts +2 -0
  8. package/dist/entry-point.d.mts.map +1 -0
  9. package/dist/entry-point.mjs +19 -0
  10. package/dist/entry-point.mjs.map +1 -0
  11. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +7 -0
  12. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -0
  13. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +83 -0
  14. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -0
  15. package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts +29 -0
  16. package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts.map +1 -0
  17. package/dist/functions/ast-transformers/convert-to-readonly-type.mjs +811 -0
  18. package/dist/functions/ast-transformers/convert-to-readonly-type.mjs.map +1 -0
  19. package/dist/functions/ast-transformers/index.d.mts +7 -0
  20. package/dist/functions/ast-transformers/index.d.mts.map +1 -0
  21. package/dist/functions/ast-transformers/index.mjs +9 -0
  22. package/dist/functions/ast-transformers/index.mjs.map +1 -0
  23. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts +3 -0
  24. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts.map +1 -0
  25. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +22 -0
  26. package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -0
  27. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts +2 -0
  28. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts.map +1 -0
  29. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +15 -0
  30. package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -0
  31. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +21 -0
  32. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -0
  33. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +72 -0
  34. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -0
  35. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts +5 -0
  36. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts.map +1 -0
  37. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -0
  38. package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -0
  39. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +37 -0
  40. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -0
  41. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +19 -0
  42. package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -0
  43. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +7 -0
  44. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -0
  45. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +173 -0
  46. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -0
  47. package/dist/functions/ast-transformers/transform-source-code.d.mts +3 -0
  48. package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -0
  49. package/dist/functions/ast-transformers/transform-source-code.mjs +27 -0
  50. package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -0
  51. package/dist/functions/ast-transformers/types.d.mts +3 -0
  52. package/dist/functions/ast-transformers/types.d.mts.map +1 -0
  53. package/dist/functions/ast-transformers/types.mjs +2 -0
  54. package/dist/functions/ast-transformers/types.mjs.map +1 -0
  55. package/dist/functions/constants/ignore-comment-text.d.mts +3 -0
  56. package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -0
  57. package/dist/functions/constants/ignore-comment-text.mjs +5 -0
  58. package/dist/functions/constants/ignore-comment-text.mjs.map +1 -0
  59. package/dist/functions/constants/index.d.mts +2 -0
  60. package/dist/functions/constants/index.d.mts.map +1 -0
  61. package/dist/functions/constants/index.mjs +2 -0
  62. package/dist/functions/constants/index.mjs.map +1 -0
  63. package/dist/functions/functions/has-disable-next-line-comment.d.mts +10 -0
  64. package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -0
  65. package/dist/functions/functions/has-disable-next-line-comment.mjs +47 -0
  66. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -0
  67. package/dist/functions/functions/index.d.mts +9 -0
  68. package/dist/functions/functions/index.d.mts.map +1 -0
  69. package/dist/functions/functions/index.mjs +9 -0
  70. package/dist/functions/functions/index.mjs.map +1 -0
  71. package/dist/functions/functions/is-as-const-node.d.mts +10 -0
  72. package/dist/functions/functions/is-as-const-node.d.mts.map +1 -0
  73. package/dist/functions/functions/is-as-const-node.mjs +30 -0
  74. package/dist/functions/functions/is-as-const-node.mjs.map +1 -0
  75. package/dist/functions/functions/is-primitive-type-node.d.mts +15 -0
  76. package/dist/functions/functions/is-primitive-type-node.d.mts.map +1 -0
  77. package/dist/functions/functions/is-primitive-type-node.mjs +46 -0
  78. package/dist/functions/functions/is-primitive-type-node.mjs.map +1 -0
  79. package/dist/functions/functions/is-readonly-node.d.mts +21 -0
  80. package/dist/functions/functions/is-readonly-node.d.mts.map +1 -0
  81. package/dist/functions/functions/is-readonly-node.mjs +30 -0
  82. package/dist/functions/functions/is-readonly-node.mjs.map +1 -0
  83. package/dist/functions/functions/is-spread-parameter-node.d.mts +4 -0
  84. package/dist/functions/functions/is-spread-parameter-node.d.mts.map +1 -0
  85. package/dist/functions/functions/is-spread-parameter-node.mjs +9 -0
  86. package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -0
  87. package/dist/functions/functions/remove-parentheses.d.mts +3 -0
  88. package/dist/functions/functions/remove-parentheses.d.mts.map +1 -0
  89. package/dist/functions/functions/remove-parentheses.mjs +9 -0
  90. package/dist/functions/functions/remove-parentheses.mjs.map +1 -0
  91. package/dist/functions/functions/unwrap-readonly.d.mts +3 -0
  92. package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -0
  93. package/dist/functions/functions/unwrap-readonly.mjs +8 -0
  94. package/dist/functions/functions/unwrap-readonly.mjs.map +1 -0
  95. package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -0
  96. package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -0
  97. package/dist/functions/functions/wrap-with-parentheses.mjs +4 -0
  98. package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -0
  99. package/dist/functions/index.d.mts +5 -0
  100. package/dist/functions/index.d.mts.map +1 -0
  101. package/dist/functions/index.mjs +19 -0
  102. package/dist/functions/index.mjs.map +1 -0
  103. package/dist/functions/utils/index.d.mts +2 -0
  104. package/dist/functions/utils/index.d.mts.map +1 -0
  105. package/dist/functions/utils/index.mjs +2 -0
  106. package/dist/functions/utils/index.mjs.map +1 -0
  107. package/dist/functions/utils/replace-with-debug.d.mts +3 -0
  108. package/dist/functions/utils/replace-with-debug.d.mts.map +1 -0
  109. package/dist/functions/utils/replace-with-debug.mjs +7 -0
  110. package/dist/functions/utils/replace-with-debug.mjs.map +1 -0
  111. package/dist/globals.d.mts +1 -0
  112. package/dist/index.d.mts +2 -0
  113. package/dist/index.d.mts.map +1 -0
  114. package/dist/index.mjs +19 -0
  115. package/dist/index.mjs.map +1 -0
  116. package/dist/tsconfig.json +1 -0
  117. package/dist/types.d.mts +2 -0
  118. package/package.json +134 -0
  119. package/src/cmd/convert-to-readonly.mts +195 -0
  120. package/src/entry-point.mts +1 -0
  121. package/src/functions/ast-transformers/convert-interface-to-type.mts +119 -0
  122. package/src/functions/ast-transformers/convert-interface-to-type.test.mts +295 -0
  123. package/src/functions/ast-transformers/convert-to-readonly-type.mts +1391 -0
  124. package/src/functions/ast-transformers/convert-to-readonly-type.test.mts +3653 -0
  125. package/src/functions/ast-transformers/index.mts +6 -0
  126. package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +24 -0
  127. package/src/functions/ast-transformers/readonly-transformer-helpers/constants.mts +12 -0
  128. package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +152 -0
  129. package/src/functions/ast-transformers/readonly-transformer-helpers/index.mts +4 -0
  130. package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +65 -0
  131. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +238 -0
  132. package/src/functions/ast-transformers/transform-source-code.mts +38 -0
  133. package/src/functions/ast-transformers/types.mts +6 -0
  134. package/src/functions/constants/ignore-comment-text.mts +3 -0
  135. package/src/functions/constants/index.mts +1 -0
  136. package/src/functions/functions/has-disable-next-line-comment.mts +56 -0
  137. package/src/functions/functions/index.mts +8 -0
  138. package/src/functions/functions/is-as-const-node.mts +47 -0
  139. package/src/functions/functions/is-primitive-type-node.mts +301 -0
  140. package/src/functions/functions/is-readonly-node.mts +247 -0
  141. package/src/functions/functions/is-spread-parameter-node.mts +13 -0
  142. package/src/functions/functions/remove-parentheses.mts +7 -0
  143. package/src/functions/functions/unwrap-readonly.mts +7 -0
  144. package/src/functions/functions/wrap-with-parentheses.mts +2 -0
  145. package/src/functions/index.mts +4 -0
  146. package/src/functions/utils/index.mts +1 -0
  147. package/src/functions/utils/replace-with-debug.mts +10 -0
  148. package/src/globals.d.mts +1 -0
  149. package/src/index.mts +1 -0
@@ -0,0 +1,3653 @@
1
+ /* eslint-disable import-x/namespace */
2
+ /* eslint-disable tree-shakable/import-star */
3
+ /* eslint-disable vitest/expect-expect */
4
+ import dedent from 'dedent';
5
+ import * as parserTypeScript from 'prettier/parser-typescript';
6
+ import * as prettierPluginEstree from 'prettier/plugins/estree';
7
+ import * as prettier from 'prettier/standalone';
8
+ import {
9
+ convertToReadonlyTypeTransformer,
10
+ type ReadonlyTransformerOptions,
11
+ } from './convert-to-readonly-type.mjs';
12
+ import { transformSourceCode } from './transform-source-code.mjs';
13
+
14
+ const testFn = async ({
15
+ source,
16
+ expected,
17
+ debug,
18
+ options,
19
+ isTsx = false,
20
+ }: Readonly<{
21
+ source: string;
22
+ expected: string;
23
+ debug?: boolean;
24
+ options?: ReadonlyTransformerOptions;
25
+ isTsx?: boolean;
26
+ }>): Promise<void> => {
27
+ if (debug !== true) {
28
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
29
+ vi.spyOn(console, 'debug').mockImplementation(() => {});
30
+
31
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
32
+ vi.spyOn(console, 'log').mockImplementation(() => {});
33
+
34
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
35
+ vi.spyOn(console, 'trace').mockImplementation(() => {});
36
+ }
37
+
38
+ const transformedCodeRaw = await formatter(
39
+ transformSourceCode(source, isTsx, [
40
+ convertToReadonlyTypeTransformer(options),
41
+ ]),
42
+ );
43
+
44
+ const expectedFormatted = await formatter(expected);
45
+
46
+ expect(transformedCodeRaw).toBe(expectedFormatted);
47
+ };
48
+
49
+ /**
50
+ * Normalizes whitespace in a code string, primarily for comparing AST structure outputs.
51
+ * - Preserves newlines immediately following line comments (`//`).
52
+ * - Collapses other sequences of whitespace (including newlines) into a single space.
53
+ * - WARNING: Does NOT handle Automatic Semicolon Insertion (ASI) correctly.
54
+ * Code relying on ASI may break if this function is used for general transformation.
55
+ * - WARNING: Does NOT preserve formatting within block comments or template literals.
56
+ *
57
+ * @param code The input TypeScript code string.
58
+ * @returns The code string with normalized whitespace.
59
+ */
60
+ const normalizeWhitespaceForComparison = (code: string): string => {
61
+ // Use a placeholder unlikely to appear in the code
62
+ const placeholder = '___LINE_COMMENT_NEWLINE_PLACEHOLDER___';
63
+
64
+ // 1. Protect newlines immediately following line comments
65
+ const protectedCode = code.replaceAll(/(\/\/.*?)\r?\n/gu, `$1${placeholder}`);
66
+
67
+ // 2. Collapse multiple whitespace characters (including unprotected newlines) into a single space
68
+ const collapsedCode = protectedCode.replaceAll(/\s+/gu, ' ');
69
+
70
+ // 3. Restore the newlines after line comments
71
+ const finalCode = collapsedCode.replaceAll(placeholder, '\n');
72
+
73
+ // 4. Trim leading/trailing whitespace
74
+ return finalCode.trim();
75
+ };
76
+
77
+ const formatter = async (code: string): Promise<string> => {
78
+ const formatOnce = await prettier.format(code, {
79
+ parser: 'typescript',
80
+ plugins: [parserTypeScript, prettierPluginEstree],
81
+ });
82
+
83
+ const whitespaceNormalized = normalizeWhitespaceForComparison(formatOnce);
84
+
85
+ return prettier.format(whitespaceNormalized, {
86
+ parser: 'typescript',
87
+ plugins: [parserTypeScript, prettierPluginEstree],
88
+ });
89
+ };
90
+
91
+ describe(convertToReadonlyTypeTransformer, () => {
92
+ describe('TypeReferences', () => {
93
+ describe('Sets', () => {
94
+ test.each([
95
+ {
96
+ name: 'Type alias',
97
+ source: dedent`
98
+ type Foo = Set<string>
99
+ `,
100
+ expected: dedent`
101
+ type Foo = ReadonlySet<string>
102
+ `,
103
+ },
104
+ {
105
+ name: 'Variable declaration with type annotation',
106
+ source: dedent`
107
+ const foo: Set<string> = new Set()
108
+ `,
109
+ expected: dedent`
110
+ const foo: ReadonlySet<string> = new Set()
111
+ `,
112
+ },
113
+ {
114
+ name: 'Variable declaration without type annotation',
115
+ source: dedent`
116
+ const foo = new Set<string>()
117
+ `,
118
+ expected: dedent`
119
+ const foo = new Set<string>()
120
+ `,
121
+ },
122
+ {
123
+ name: 'Type assertion with as',
124
+ source: dedent`
125
+ const foo = new Set() as Set<string>
126
+ `,
127
+ expected: dedent`
128
+ const foo = new Set() as ReadonlySet<string>
129
+ `,
130
+ },
131
+ {
132
+ name: 'Satisfies operator',
133
+ source: dedent`
134
+ const foo = new Set() satisfies Set<string>
135
+ `,
136
+ expected: dedent`
137
+ const foo = new Set() satisfies ReadonlySet<string>
138
+ `,
139
+ },
140
+ {
141
+ name: 'In function args',
142
+ source: dedent`
143
+ function foo(a: Set<number>, b: Promise<Set<number>>) {}
144
+ `,
145
+ expected: dedent`
146
+ function foo(a: ReadonlySet<number>, b: Promise<ReadonlySet<number>>) {}
147
+ `,
148
+ },
149
+ ])('$name', testFn);
150
+ });
151
+
152
+ describe('Maps', () => {
153
+ test.each([
154
+ {
155
+ name: 'Type alias',
156
+ source: dedent`
157
+ type Foo = Map<string, string>
158
+ `,
159
+ expected: dedent`
160
+ type Foo = ReadonlyMap<string, string>
161
+ `,
162
+ },
163
+ {
164
+ name: 'Variable declaration with type annotation',
165
+ source: dedent`
166
+ const foo: Map<string, string> = new Map()
167
+ `,
168
+ expected: dedent`
169
+ const foo: ReadonlyMap<string, string> = new Map()
170
+ `,
171
+ },
172
+ {
173
+ name: 'Variable declaration without type annotation',
174
+ source: dedent`
175
+ const foo = new Map<string, string>()
176
+ `,
177
+ expected: dedent`
178
+ const foo = new Map<string, string>()
179
+ `,
180
+ },
181
+ {
182
+ name: 'Type assertion with as',
183
+ source: dedent`
184
+ const foo = new Map() as Map<string, string>
185
+ `,
186
+ expected: dedent`
187
+ const foo = new Map() as ReadonlyMap<string, string>
188
+ `,
189
+ },
190
+ {
191
+ name: 'Satisfies operator',
192
+ source: dedent`
193
+ const foo = new Map() satisfies Map<string, string>
194
+ `,
195
+ expected: dedent`
196
+ const foo = new Map() satisfies ReadonlyMap<string, string>;
197
+ `,
198
+ },
199
+ {
200
+ name: 'In function args',
201
+ source: dedent`
202
+ function foo(a: Map<string, number>, b: Promise<Map<string, number>>) {}
203
+ `,
204
+ expected: dedent`
205
+ function foo(a: ReadonlyMap<string, number>, b: Promise<ReadonlyMap<string, number>>) {}
206
+ `,
207
+ },
208
+ ])('$name', testFn);
209
+ });
210
+
211
+ describe('Normalize `Readonly` wrappers', () => {
212
+ test.each([
213
+ {
214
+ name: 'Array type wrapped with `Readonly`',
215
+ source: dedent`
216
+ type T = Readonly<1[]>
217
+ `,
218
+ expected: dedent`
219
+ type T = readonly 1[];
220
+ `, // Readonly<T[]> -> readonly T[]
221
+ },
222
+ {
223
+ name: 'Readonly array type wrapped with `Readonly`',
224
+ source: dedent`
225
+ type T = Readonly<readonly 2[]>
226
+ `,
227
+ expected: dedent`
228
+ type T = readonly 2[];
229
+ `, // Readonly<readonly T[]> -> readonly T[]
230
+ },
231
+ {
232
+ name: 'Tuple type wrapped with `Readonly`',
233
+ source: dedent`
234
+ type T = Readonly<[1, 2]>
235
+ `,
236
+ expected: dedent`
237
+ type T = readonly [1, 2];
238
+ `, // Readonly<[T]> -> readonly [T]
239
+ },
240
+ {
241
+ name: 'Readonly tuple type wrapped with `Readonly`',
242
+ source: dedent`
243
+ type T = Readonly<readonly [1, 2, 3]>
244
+ `,
245
+ expected: dedent`
246
+ type T = readonly [1, 2, 3];
247
+ `, // Readonly<readonly [T]> -> readonly [T]
248
+ },
249
+ {
250
+ name: 'Nested Readonly wrapper',
251
+ source: dedent`
252
+ type T = Readonly<Readonly<{ x: 3 }>>
253
+ `,
254
+ expected: dedent`
255
+ type T = Readonly<{ x: 3 }>;
256
+ `, // Readonly<Readonly<T>> -> Readonly<T>
257
+ },
258
+ {
259
+ name: 'Deeply nested Readonly wrapper',
260
+ source: dedent`
261
+ type T = Readonly<Readonly<Readonly<{ x: 2 }>>>
262
+ `,
263
+ expected: dedent`
264
+ type T = Readonly<{ x: 2 }>
265
+ `,
266
+ },
267
+ {
268
+ name: 'Deeply nested Readonly wrapper with array/parens',
269
+ source: dedent`
270
+ type T = Readonly<((Readonly<Readonly<(({ x: 4 })[])>>))>
271
+ `,
272
+ // Readonly<( (Readonly<Readonly<({ x: 4 }[])>>) )>
273
+ // -> Readonly<Readonly<Readonly<({ x: 4 }[])>>> (remove ParenthesizedTypeNode)
274
+ // -> Readonly<Readonly<({ x: 4 }[])>> (Readonly<Readonly<T>> -> Readonly<T>)
275
+ // -> Readonly<({ x: 4 }[])> (Readonly<Readonly<T>> -> Readonly<T>)
276
+ // -> readonly { x: 4 }[] (Readonly<T[]> -> readonly T[])
277
+ expected: dedent`
278
+ type T = readonly Readonly<{ x: 4 }>[]
279
+ `,
280
+ },
281
+ {
282
+ name: 'Intersection within readonly array',
283
+ source: dedent`
284
+ type T = readonly (Readonly<{ x: 1 }> & Readonly<{ y: 2 }>)[];
285
+ `,
286
+ // readonly (Readonly<A> & Readonly<B>)[]
287
+ // -> readonly Readonly<A & B>[] (Inner Intersection Collapse)
288
+ expected: dedent`
289
+ type T = readonly Readonly<{ x: 1 } & { y: 2 }>[]
290
+ `,
291
+ },
292
+ {
293
+ name: 'Union within readonly array',
294
+ source: dedent`
295
+ type T = readonly (Readonly<{ x: 1 }> | Readonly<{ y: 2 }>)[];
296
+ `,
297
+ // readonly (Readonly<A> | Readonly<B>)[]
298
+ // -> readonly Readonly<A | B>[] (Inner Union Collapse)
299
+ expected: dedent`
300
+ type T = readonly Readonly<{ x: 1 } | { y: 2 }>[]
301
+ `,
302
+ },
303
+ ])('$name', testFn);
304
+ });
305
+
306
+ describe('Readonly wrapper edge cases', () => {
307
+ test.each([
308
+ {
309
+ name: 'Readonly wrapper on Promise<T[]>',
310
+ source: dedent`
311
+ type Test1 = Readonly<Promise<string[]>>;
312
+ type Test2 = Readonly<Promise<readonly number[]>>
313
+ `,
314
+ expected: dedent`
315
+ type Test1 = Readonly<Promise<readonly string[]>>;
316
+ type Test2 = Readonly<Promise<readonly number[]>>
317
+ `,
318
+ },
319
+ {
320
+ name: 'Readonly wrapper on CustomGeneric<T[]>',
321
+ source: dedent`
322
+ type Wrapper<T> = { data: T };
323
+ type Test3 = Readonly<Wrapper<Map<string, number[]>>>; // Map and Array are not targets
324
+ `,
325
+ expected: dedent`
326
+ type Wrapper<T> = Readonly<{ data: T }>;
327
+ type Test3 = Readonly<Wrapper<ReadonlyMap<string, readonly number[]>>>; // Map and Array are not targets
328
+ `,
329
+ },
330
+ {
331
+ name: 'Nested Readonly simplification with Promise',
332
+ source: dedent`
333
+ type Test4 = Readonly<Readonly<Promise<string[]>>>
334
+ `,
335
+ // Readonly<Readonly<T>> -> Readonly<T>
336
+ expected: dedent`
337
+ type Test4 = Readonly<Promise<readonly string[]>>
338
+ `,
339
+ },
340
+ ])('$name', testFn);
341
+ });
342
+
343
+ describe('DeepReadonly', () => {
344
+ test.each([
345
+ {
346
+ name: 'Basic DeepReadonly usage',
347
+ source: dedent`
348
+ type T = DeepReadonly<{ a: number; b: string[] }>
349
+ `,
350
+ expected: dedent`
351
+ type T = DeepReadonly<{ a: number; b: string[] }>
352
+ `,
353
+ },
354
+ {
355
+ name: 'DeepReadonly with nested objects',
356
+ source: dedent`
357
+ type T = DeepReadonly<{ a: number; b: { c: string[]; d: number } }>;
358
+ `,
359
+ expected: dedent`
360
+ type T = DeepReadonly<{ a: number; b: { c: string[]; d: number } }>;
361
+ `,
362
+ },
363
+ {
364
+ name: 'DeepReadonly with arrays',
365
+ source: dedent`
366
+ type T = DeepReadonly<string[]>
367
+ `,
368
+ expected: dedent`
369
+ type T = readonly string[]
370
+ `,
371
+ },
372
+ {
373
+ name: 'DeepReadonly with nested arrays',
374
+ source: dedent`
375
+ type T = DeepReadonly<string[][]>
376
+ `,
377
+ expected: dedent`
378
+ type T = DeepReadonly<string[][]>
379
+ `,
380
+ },
381
+ {
382
+ name: 'DeepReadonly with Map',
383
+ source: dedent`
384
+ type T = DeepReadonly<Map<string, number[]>>
385
+ `,
386
+ expected: dedent`
387
+ type T = DeepReadonly<ReadonlyMap<string, number[]>>
388
+ `,
389
+ },
390
+ {
391
+ name: 'DeepReadonly with Set',
392
+ source: dedent`
393
+ type T = DeepReadonly<Set<string[]>>
394
+ `,
395
+ expected: dedent`
396
+ type T = DeepReadonly<ReadonlySet<string[]>>
397
+ `,
398
+ },
399
+ {
400
+ name: 'DeepReadonly with redundant Readonly wrapper',
401
+ source: dedent`
402
+ type T = DeepReadonly<Readonly<{ a: number; b: string[] }>>
403
+ `,
404
+ expected: dedent`
405
+ type T = DeepReadonly<{ a: number; b: string[] }>
406
+ `,
407
+ },
408
+ {
409
+ name: 'DeepReadonly with redundant readonly array',
410
+ source: dedent`
411
+ type T = DeepReadonly<readonly string[]>
412
+ `,
413
+ expected: dedent`
414
+ type T = readonly string[]
415
+ `,
416
+ },
417
+ {
418
+ name: 'DeepReadonly with redundant readonly properties',
419
+ source: dedent`
420
+ type T = DeepReadonly<{ readonly a: number; readonly b: readonly string[] }>;
421
+ `,
422
+ expected: dedent`
423
+ type T = DeepReadonly<{ a: number; b: string[] }>
424
+ `,
425
+ },
426
+ {
427
+ name: 'Nested DeepReadonly',
428
+ source: dedent`
429
+ type T = DeepReadonly<DeepReadonly<{ a: number; b: string[] }>>;
430
+ `,
431
+ expected: dedent`
432
+ type T = DeepReadonly<{ a: number; b: string[] }>
433
+ `,
434
+ },
435
+ {
436
+ name: 'DeepReadonly with union types',
437
+ source: dedent`
438
+ type T = DeepReadonly<{ a: number } | { b: string[] }>
439
+ `,
440
+ expected: dedent`
441
+ type T = DeepReadonly<{ a: number } | { b: string[] }>;
442
+ `,
443
+ },
444
+ {
445
+ name: 'DeepReadonly with intersection types',
446
+ source: dedent`
447
+ type T = DeepReadonly<{ a: number } & { b: string[] }>
448
+ `,
449
+ expected: dedent`
450
+ type T = DeepReadonly<{ a: number } & { b: string[] }>;
451
+ `,
452
+ },
453
+ {
454
+ name: 'DeepReadonly with complex nested structure',
455
+ source: dedent`
456
+ type T = DeepReadonly<{ a: number; b: { c: string[]; d: Map<string, { e: number[] }> } }>;
457
+ `,
458
+ expected: dedent`
459
+ type T = DeepReadonly<{ a: number; b: { c: string[]; d: ReadonlyMap<string, { e: number[] }> } }>;
460
+ `,
461
+ },
462
+ {
463
+ name: 'DeepReadonly in function parameter',
464
+ source: dedent`
465
+ function process<T>(data: DeepReadonly<{ items: T[] }>): T { return data.items[0]; }
466
+ `,
467
+ expected: dedent`
468
+ function process<T>(data: DeepReadonly<{ items: T[] }>): T { return data.items[0]; }
469
+ `,
470
+ },
471
+ {
472
+ name: 'DeepReadonly in function return type',
473
+ source: dedent`
474
+ function getData(): DeepReadonly<{ config: string[]; data: number[] }> { return { config: [], data: [] }; }
475
+ `,
476
+ expected: dedent`
477
+ function getData(): DeepReadonly<{ config: string[]; data: number[] }> { return { config: [], data: [] }; }
478
+ `,
479
+ },
480
+ ])('$name', testFn);
481
+ });
482
+ });
483
+
484
+ describe('Arrays', () => {
485
+ test.each([
486
+ {
487
+ name: 'Mutable array types in type alias (non-generic)',
488
+ source: dedent`
489
+ type Foo = number[];
490
+ `,
491
+ expected: dedent`
492
+ type Foo = readonly number[];
493
+ `,
494
+ },
495
+ {
496
+ name: 'Mutable array types in function args (non-generic)',
497
+ source: dedent`
498
+ function foo(a: number[], b: Promise<number[]>) {}
499
+ `,
500
+ expected: dedent`
501
+ function foo(a: readonly number[], b: Promise<readonly number[]>) {}
502
+ `,
503
+ },
504
+ {
505
+ name: 'Mutable array types in function args (generic)',
506
+ source: dedent`
507
+ function foo(a: Array<number>, b: Promise<Array<number>>) {}
508
+ `,
509
+ expected: dedent`
510
+ function foo(a: readonly number[], b: Promise<readonly number[]>) {}
511
+ `,
512
+ },
513
+ {
514
+ name: 'Mutable array types in interface',
515
+ source: dedent`
516
+ interface SimpleInterface {
517
+ a: number[];
518
+ }
519
+ `,
520
+ expected: dedent`
521
+ interface SimpleInterface {
522
+ readonly a: readonly number[];
523
+ }
524
+ `,
525
+ },
526
+ {
527
+ name: 'Mutable nested array types (non-generic)',
528
+ source: dedent`
529
+ function foo(a: number[][], b: Promise<number[][]>) {}
530
+ `,
531
+ expected: dedent`
532
+ function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
533
+ `,
534
+ },
535
+ {
536
+ name: 'Mutable nested array types (generic)',
537
+ source: dedent`
538
+ function foo(a: Array<Array<number>>, b: Promise<Array<Array<number>>>) {}
539
+ `,
540
+ expected: dedent`
541
+ function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
542
+ `,
543
+ },
544
+ {
545
+ name: 'Mutable nested array types (generic & non-generic combined)',
546
+ source: dedent`
547
+ function foo(a: Array<number[]>, b: Promise<Array<number>[]>) {}
548
+ `,
549
+ expected: dedent`
550
+ function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
551
+ `,
552
+ },
553
+ {
554
+ name: 'Mutable nested array types (readonly & non-readonly combined)',
555
+ source: dedent`
556
+ function foo(a: readonly number[][], b: Promise< (readonly number[])[]>) {}
557
+ `,
558
+ expected: dedent`
559
+ function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
560
+ `,
561
+ },
562
+ {
563
+ name: 'Mutable arrays nested within objects',
564
+ source: dedent`
565
+ function foo(
566
+ a: { p: number[] }[],
567
+ b: Promise<number[][]>
568
+ ) {}
569
+ `,
570
+ expected: dedent`
571
+ function foo(
572
+ a: readonly Readonly<{ p: readonly number[] }>[],
573
+ b: Promise<readonly (readonly number[])[]>
574
+ ) {}
575
+ `,
576
+ },
577
+ {
578
+ name: 'No type annotations',
579
+ source: dedent`
580
+ const foo = [1, 2, 3];
581
+ function bar(param = [1, 2, 3]) {}
582
+ `,
583
+ expected: dedent`
584
+ const foo = [1, 2, 3];
585
+ function bar(param = [1, 2, 3]) {}
586
+ `,
587
+ },
588
+ {
589
+ name: 'Local types within function',
590
+ source: dedent`
591
+ function foo() {
592
+ type Foo = ReadonlyArray<string>;
593
+ type Bar = Array<string>;
594
+ }
595
+ `,
596
+ expected: dedent`
597
+ function foo() {
598
+ type Foo = readonly string[];
599
+ type Bar = readonly string[];
600
+ }
601
+ `,
602
+ },
603
+ {
604
+ name: 'Mutable variable declarations (generic)',
605
+ source: dedent`
606
+ const foo: Array<string> = []
607
+ `,
608
+ expected: dedent`
609
+ const foo: readonly string[] = []
610
+ `,
611
+ },
612
+ {
613
+ name: 'Mutable variable declarations (non-generic)',
614
+ source: dedent`
615
+ const foo: string[] = []
616
+ `,
617
+ expected: dedent`
618
+ const foo: readonly string[] = []
619
+ `,
620
+ },
621
+ {
622
+ name: 'Readonly variable declarations (generic)',
623
+ source: dedent`
624
+ const foo: ReadonlyArray<string> = []
625
+ `,
626
+ expected: dedent`
627
+ const foo: readonly string[] = []
628
+ `,
629
+ },
630
+ {
631
+ name: 'Readonly variable declarations (non-generic)',
632
+ source: dedent`
633
+ const foo: readonly string[] = []
634
+ `,
635
+ expected: dedent`
636
+ const foo: readonly string[] = []
637
+ `,
638
+ },
639
+ {
640
+ name: 'No type annotation',
641
+ source: dedent`
642
+ const foo = []
643
+ `,
644
+ expected: dedent`
645
+ const foo = []
646
+ `,
647
+ },
648
+ {
649
+ name: 'No type annotation with as const',
650
+ source: dedent`
651
+ const numbers = [1, 2, 3] as const
652
+ `,
653
+ expected: dedent`
654
+ const numbers = [1, 2, 3] as const
655
+ `,
656
+ },
657
+ ])('$name', testFn);
658
+ });
659
+
660
+ describe('Tuples', () => {
661
+ test.each([
662
+ {
663
+ name: 'Mutable tuples in type alias',
664
+ source: dedent`
665
+ type Foo = [string, string]
666
+ `,
667
+ expected: dedent`
668
+ type Foo = readonly [string, string]
669
+ `,
670
+ },
671
+ {
672
+ name: 'Variable declaration with type annotation',
673
+ source: dedent`
674
+ const foo: [string, string] = [foo, bar];
675
+ `,
676
+ expected: dedent`
677
+ const foo: readonly [string, string] = [foo, bar];
678
+ `,
679
+ },
680
+ {
681
+ name: 'Variable declaration without type annotation',
682
+ source: dedent`
683
+ const foo = [foo, bar];
684
+ `,
685
+ expected: dedent`
686
+ const foo = [foo, bar];
687
+ `,
688
+ },
689
+ {
690
+ name: 'Type assertion with as',
691
+ source: dedent`
692
+ const foo = [foo, bar] as [string, string];
693
+ `,
694
+ expected: dedent`
695
+ const foo = [foo, bar] as readonly [string, string];
696
+ `,
697
+ },
698
+ {
699
+ name: 'Satisfies operator',
700
+ source: dedent`
701
+ const foo = [foo, bar] satisfies [string, string];
702
+ `,
703
+ expected: dedent`
704
+ const foo = [foo, bar] satisfies readonly [string, string];
705
+ `,
706
+ },
707
+ {
708
+ name: 'Nested mutable tuple',
709
+ source: dedent`
710
+ const foo = (tuple: [number, string, [number[], { x: string }]]) => {
711
+ console.log(tuple);
712
+ }
713
+ `,
714
+ expected: dedent`
715
+ const foo = (tuple: readonly [number, string, readonly [readonly number[], Readonly<{ x: string }>]]) => {
716
+ console.log(tuple);
717
+ }
718
+ `,
719
+ },
720
+ {
721
+ name: 'Already readonly tuple',
722
+ source: dedent`
723
+ const foo = (tuple: readonly [number, string, readonly [number, string]]) => {
724
+ console.log(tuple);
725
+ }
726
+ `,
727
+ expected: dedent`
728
+ const foo = (tuple: readonly [number, string, readonly [number, string]]) => {
729
+ console.log(tuple);
730
+ }
731
+ `,
732
+ },
733
+ {
734
+ name: 'Tuple with optional element',
735
+ source: dedent`
736
+ type OptionalTuple = [string, number?]
737
+ `,
738
+ expected: dedent`
739
+ type OptionalTuple = readonly [string, number?]
740
+ `,
741
+ },
742
+ ])('$name', testFn);
743
+
744
+ describe('Rest types', () => {
745
+ test.each([
746
+ {
747
+ name: 'Tuple with rest element',
748
+ source: dedent`
749
+ type RestTuple = [string, ...number[]]
750
+ `,
751
+ expected: dedent`
752
+ type RestTuple = readonly [string, ...number[]];
753
+ `,
754
+ },
755
+ {
756
+ name: 'Tuple with rest element nested',
757
+ source: dedent`
758
+ type RestTuple = [string, ...[boolean, ...number[]]];
759
+ `,
760
+ expected: dedent`
761
+ type RestTuple = readonly [string, ...[boolean, ...number[]]];
762
+ `,
763
+ },
764
+ {
765
+ name: 'Tuple with rest element nested (already readonly)',
766
+ source: dedent`
767
+ type RestTuple = readonly [string, ...readonly [boolean, ...readonly number[]]];
768
+ `,
769
+ expected: dedent`
770
+ type RestTuple = readonly [string, ...[boolean, ...number[]]];
771
+ `,
772
+ },
773
+ {
774
+ name: 'Tuple with optional and rest elements',
775
+ source: dedent`
776
+ type OptionalRestTuple = [string?, ...Map<string, number>[]];
777
+ `,
778
+ expected: dedent`
779
+ type OptionalRestTuple = readonly [string?, ...ReadonlyMap<string, number>[]];
780
+ `,
781
+ },
782
+ ])('$name', testFn);
783
+ });
784
+
785
+ describe('Named tuples', () => {
786
+ test.each([
787
+ {
788
+ name: 'Named tuples',
789
+ source: dedent`
790
+ type T = [names: string[], values: number[]];
791
+ `,
792
+ expected: dedent`
793
+ type T = readonly [names: readonly string[], values: readonly number[]];
794
+ `,
795
+ },
796
+ {
797
+ name: 'NamedTupleMember with "mut_" prefix',
798
+ source: dedent`
799
+ type T = [names: string[], mut_values: number[]];
800
+ `,
801
+ expected: dedent`
802
+ type T = readonly [names: readonly string[], mut_values: number[]];
803
+ `,
804
+ },
805
+ ])('$name', testFn);
806
+ });
807
+ });
808
+
809
+ describe('TypeOperators', () => {
810
+ test.each([
811
+ {
812
+ name: 'Keyof type operator',
813
+ source: dedent`
814
+ type Foo = keyof { a: number[]; b: string[] };
815
+ `,
816
+ expected: dedent`
817
+ type Foo = keyof Readonly<{ a: readonly number[]; b: readonly string[] }>;
818
+ `,
819
+ },
820
+ {
821
+ name: 'Readonly type operator on nested array',
822
+ source: dedent`
823
+ type Foo = readonly number[][]
824
+ `,
825
+ expected: dedent`
826
+ type Foo = readonly (readonly number[])[]
827
+ `,
828
+ },
829
+ ])('$name', testFn);
830
+ });
831
+
832
+ describe('Type literals', () => {
833
+ test.each([
834
+ {
835
+ name: 'Type literal with one readonly member (unchanged)',
836
+ source: dedent`
837
+ type T = { readonly a: number; b: string }
838
+ `,
839
+ expected: dedent`
840
+ type T = Readonly<{ a: number; b: string }>
841
+ `,
842
+ },
843
+ {
844
+ name: 'Type literal with no readonly members',
845
+ source: dedent`
846
+ type T = { a: number; b: string }
847
+ `,
848
+ expected: dedent`
849
+ type T = Readonly<{ a: number; b: string }>
850
+ `,
851
+ },
852
+ {
853
+ name: 'Type literal with all members readonly (normalized)',
854
+ source: dedent`
855
+ type T = { readonly a: number; readonly b: string }
856
+ `,
857
+ // All members readonly -> Normalized to Readonly<{...}> (inner readonly removed)
858
+ expected: dedent`
859
+ type T = Readonly<{ a: number; b: string }>
860
+ `,
861
+ },
862
+ {
863
+ name: 'Readonly type literal (canonical form, unchanged)',
864
+ source: dedent`
865
+ type T = Readonly<{ a: number; b: string }>
866
+ `,
867
+ expected: dedent`
868
+ type T = Readonly<{ a: number; b: string }>;
869
+ `, // Unchanged (already canonical form)
870
+ },
871
+ {
872
+ name: 'In function args',
873
+ source: dedent`
874
+ function foo(a: { p: number[], readonly q: boolean[] }) {}
875
+ `,
876
+ expected: dedent`
877
+ function foo(a: Readonly<{ p: readonly number[], q: readonly boolean[] }>) {}
878
+ `,
879
+ },
880
+ {
881
+ name: 'Nested, in function args',
882
+ source: dedent`
883
+ function foo(a: { readonly p: string[], q: bigint[] }[]) {}
884
+ `,
885
+ expected: dedent`
886
+ function foo(a: readonly Readonly<{ p: readonly string[], q: readonly bigint[] }>[]) {}
887
+ `,
888
+ },
889
+ {
890
+ name: 'In type alias',
891
+ source: dedent`
892
+ type TypeAlias = {
893
+ a: number[]
894
+ };
895
+ `,
896
+ expected: dedent`
897
+ type TypeAlias = Readonly<{
898
+ a: readonly number[]
899
+ }>;
900
+ `,
901
+ },
902
+ {
903
+ name: 'Type literals without readonly modifiers',
904
+ source: dedent`
905
+ let foo: {
906
+ a: number,
907
+ b: ReadonlyArray<string>,
908
+ c: () => string,
909
+ d: { readonly [key: string]: string[] },
910
+ [key: string]: string[],
911
+ readonly e: {
912
+ a: number,
913
+ b: ReadonlyArray<string>,
914
+ c: () => string,
915
+ d: { readonly [key: string]: string[] },
916
+ [key: string]: string[],
917
+ }
918
+ };
919
+ `,
920
+ expected: dedent`
921
+ let foo: Readonly<{
922
+ a: number,
923
+ b: readonly string[],
924
+ c: () => string,
925
+ d: Readonly<{ [key: string]: readonly string[] }>,
926
+ [key: string]: readonly string[],
927
+ e: Readonly<{
928
+ a: number,
929
+ b: readonly string[],
930
+ c: () => string,
931
+ d: Readonly<{ [key: string]: readonly string[] }>,
932
+ [key: string]: readonly string[],
933
+ }>
934
+ }>;
935
+ `,
936
+ },
937
+ {
938
+ name: 'Type literal elements with a readonly modifier in an array',
939
+ source: dedent`
940
+ type foo = ReadonlyArray<{ readonly type: string, readonly code: string }>;
941
+ `,
942
+ expected: dedent`
943
+ type foo = readonly Readonly<{ type: string, code: string }>[];
944
+ `,
945
+ },
946
+ {
947
+ name: 'Type literals with readonly on members',
948
+ source: dedent`
949
+ let foo: {
950
+ readonly a: number,
951
+ readonly b: ReadonlyArray<string>,
952
+ readonly c: () => string,
953
+ readonly d: { readonly [key: string]: string[] },
954
+ readonly [key: string]: string[]
955
+ };
956
+ `,
957
+ expected: dedent`
958
+ let foo: Readonly<{
959
+ a: number,
960
+ b: readonly string[],
961
+ c: () => string,
962
+ d: Readonly<{ [key: string]: readonly string[] }>,
963
+ [key: string]: readonly string[]
964
+ }>;
965
+ `,
966
+ },
967
+ {
968
+ name: 'Empty type literal',
969
+ source: dedent`
970
+ type foo = {};
971
+ type bar = Readonly<{}>;
972
+ `,
973
+ expected: dedent`
974
+ type foo = {};
975
+ type bar = Readonly<{}>;
976
+ `,
977
+ },
978
+ {
979
+ name: 'Empty type literal with ignoreEmptyObjectTypes = false',
980
+ source: dedent`
981
+ type foo = {};
982
+ type bar = Readonly<{}>;
983
+ `,
984
+ expected: dedent`
985
+ type foo = Readonly<{}>;
986
+ type bar = Readonly<{}>;
987
+ `,
988
+ options: {
989
+ ignoreEmptyObjectTypes: false,
990
+ },
991
+ },
992
+ ])('$name', testFn);
993
+ });
994
+
995
+ describe('Index signatures', () => {
996
+ test.each([
997
+ {
998
+ name: 'Index signature with array value',
999
+ source: dedent`
1000
+ interface Foo {
1001
+ [key: string]: string[]
1002
+ }
1003
+ `,
1004
+ expected: dedent`
1005
+ interface Foo {
1006
+ readonly [key: string]: readonly string[]
1007
+ }
1008
+ `,
1009
+ },
1010
+ {
1011
+ name: 'Index signature with readonly key modifier',
1012
+ source: dedent`
1013
+ interface Bar {
1014
+ readonly [key: string]: string[]
1015
+ }
1016
+ `,
1017
+ expected: dedent`
1018
+ interface Bar {
1019
+ readonly [key: string]: readonly string[]
1020
+ }
1021
+ `,
1022
+ },
1023
+ {
1024
+ name: 'Index signature with readonly key and mutable object value',
1025
+ source: dedent`
1026
+ interface Foo {
1027
+ readonly [key: string]: {
1028
+ a: Array<string>;
1029
+ readonly b: Promise<Array<string>>;
1030
+ };
1031
+ }
1032
+ `,
1033
+ expected: dedent`
1034
+ interface Foo {
1035
+ readonly [key: string]: Readonly<{
1036
+ a: readonly string[];
1037
+ b: Promise<readonly string[]>;
1038
+ }>;
1039
+ }
1040
+ `,
1041
+ },
1042
+ {
1043
+ name: 'Index signature in type alias',
1044
+ source: dedent`
1045
+ type Foo = {
1046
+ readonly [key: string]: string[]
1047
+ }
1048
+ `,
1049
+ expected: dedent`
1050
+ type Foo = Readonly<{
1051
+ [key: string]: readonly string[]
1052
+ }>
1053
+ `,
1054
+ },
1055
+ {
1056
+ name: 'Nested index signature in interface',
1057
+ source: dedent`
1058
+ interface NestedIndexSignatureInterface {
1059
+ [key: string]: {
1060
+ [subkey: string]: string[]
1061
+ }
1062
+ }
1063
+ `,
1064
+ expected: dedent`
1065
+ interface NestedIndexSignatureInterface {
1066
+ readonly [key: string]: Readonly<{
1067
+ [subkey: string]: readonly string[]
1068
+ }>
1069
+ }
1070
+ `,
1071
+ },
1072
+ {
1073
+ name: 'Nested index signature in type alias',
1074
+ source: dedent`
1075
+ type NestedIndexSignatureTypeAlias = {
1076
+ [key: string]: {
1077
+ [subkey: string]: string[]
1078
+ }
1079
+ }
1080
+ `,
1081
+ expected: dedent`
1082
+ type NestedIndexSignatureTypeAlias = Readonly<{
1083
+ [key: string]: Readonly<{
1084
+ [subkey: string]: readonly string[]
1085
+ }>
1086
+ }>
1087
+ `,
1088
+ },
1089
+ {
1090
+ name: 'Variable declaration with index signature type',
1091
+ source: dedent`
1092
+ let foo: { readonly [key: string]: number }
1093
+ `,
1094
+ expected: dedent`
1095
+ let foo: Readonly<{ [key: string]: number }>
1096
+ `,
1097
+ },
1098
+ ])('$name', testFn);
1099
+ });
1100
+
1101
+ describe('Mapped types', () => {
1102
+ test.each([
1103
+ {
1104
+ name: 'Mapped type in type alias',
1105
+ source: dedent`
1106
+ type T = { [key in string]: number[] }
1107
+ `,
1108
+ expected: dedent`
1109
+ type T = Readonly<{ [key in string]: readonly number[] }>
1110
+ `,
1111
+ },
1112
+ {
1113
+ name: 'Mapped type in type alias (PlusToken)',
1114
+ source: dedent`
1115
+ type T = {+readonly [key in string]: number[] }
1116
+ `,
1117
+ expected: dedent`
1118
+ type T = Readonly<{ [key in string]: readonly number[] }>
1119
+ `,
1120
+ },
1121
+ {
1122
+ name: 'Mapped type in type alias (MinusToken)',
1123
+ source: dedent`
1124
+ type T = { -readonly [key in string]-?: number[] }
1125
+ `,
1126
+ expected: dedent`
1127
+ type T = Readonly<{ [key in string]-?: readonly number[] }>
1128
+ `,
1129
+ },
1130
+ {
1131
+ name: 'Mapped type in type alias (ReadonlyKeyword)',
1132
+ source: dedent`
1133
+ type T = { readonly [key in string]?: number[] }
1134
+ `,
1135
+ expected: dedent`
1136
+ type T = Readonly<{ [key in string]?: readonly number[] }>
1137
+ `,
1138
+ },
1139
+ {
1140
+ name: 'Mapped type in function arguments with readonly',
1141
+ source: dedent`
1142
+ const func = (x: { readonly [key in string]: number[] }) => {}
1143
+ `,
1144
+ expected: dedent`
1145
+ const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
1146
+ `,
1147
+ },
1148
+ {
1149
+ name: 'Mapped type in function arguments without readonly',
1150
+ source: dedent`
1151
+ const func = (x: { [key in string]: number[] }) => {}
1152
+ `,
1153
+ expected: dedent`
1154
+ const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
1155
+ `,
1156
+ },
1157
+ {
1158
+ name: 'Mapped type in function arguments with Readonly<*>',
1159
+ source: dedent`
1160
+ const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
1161
+ `,
1162
+ expected: dedent`
1163
+ const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
1164
+ `,
1165
+ },
1166
+ ])('$name', testFn);
1167
+ });
1168
+
1169
+ describe('Interfaces', () => {
1170
+ test.each([
1171
+ {
1172
+ name: 'Mutable & readonly properties',
1173
+ source: dedent`
1174
+ interface Foo {
1175
+ bar: Array<string>;
1176
+ readonly baz: Promise<string[]>;
1177
+ }
1178
+ `,
1179
+ expected: dedent`
1180
+ interface Foo {
1181
+ readonly bar: readonly string[];
1182
+ readonly baz: Promise<readonly string[]>;
1183
+ }
1184
+ `,
1185
+ },
1186
+ {
1187
+ name: 'Interface with various readonly members',
1188
+ source: dedent`
1189
+ interface Foo {
1190
+ readonly a: number,
1191
+ readonly b: ReadonlyArray<string>,
1192
+ readonly c: () => string,
1193
+ readonly d: { readonly [key: string]: string[] },
1194
+ readonly [key: string]: string[],
1195
+ }
1196
+ `,
1197
+ expected: dedent`
1198
+ interface Foo {
1199
+ readonly a: number,
1200
+ readonly b: readonly string[],
1201
+ readonly c: () => string,
1202
+ readonly d: Readonly<{ [key: string]: readonly string[] }>,
1203
+ readonly [key: string]: readonly string[],
1204
+ }
1205
+ `,
1206
+ },
1207
+ {
1208
+ name: 'Interface with nested readonly members',
1209
+ source: dedent`
1210
+ interface Foo {
1211
+ readonly a: number,
1212
+ readonly b: ReadonlyArray<string>,
1213
+ readonly c: () => string,
1214
+ readonly d: { readonly [key: string]: readonly string[] },
1215
+ readonly [key: string]: readonly string[],
1216
+ readonly e: {
1217
+ readonly a: number,
1218
+ readonly b: ReadonlyArray<string>,
1219
+ readonly c: () => string,
1220
+ readonly d: { readonly [key: string]: readonly string[] },
1221
+ readonly [key: string]: readonly string[],
1222
+ }
1223
+ }
1224
+ `,
1225
+ expected: dedent`
1226
+ interface Foo {
1227
+ readonly a: number,
1228
+ readonly b: readonly string[],
1229
+ readonly c: () => string,
1230
+ readonly d: Readonly<{ [key: string]: readonly string[] }>,
1231
+ readonly [key: string]: readonly string[],
1232
+ readonly e: Readonly<{
1233
+ a: number,
1234
+ b: readonly string[],
1235
+ c: () => string,
1236
+ d: Readonly<{ [key: string]: readonly string[] }>,
1237
+ [key: string]: readonly string[],
1238
+ }>
1239
+ }
1240
+ `,
1241
+ },
1242
+ {
1243
+ name: 'Interface with nested non-readonly members',
1244
+ source: dedent`
1245
+ interface Foo {
1246
+ a: number,
1247
+ b: ReadonlyArray<string>,
1248
+ c: () => string,
1249
+ d: { [key: string]: string[] },
1250
+ [key: string]: string[],
1251
+ e: {
1252
+ a: number,
1253
+ b: ReadonlyArray<string>,
1254
+ c: () => string,
1255
+ d: { [key: string]: string[] },
1256
+ [key: string]: string[],
1257
+ }
1258
+ }
1259
+ `,
1260
+ expected: dedent`
1261
+ interface Foo {
1262
+ readonly a: number,
1263
+ readonly b: readonly string[],
1264
+ readonly c: () => string,
1265
+ readonly d: Readonly<{ [key: string]: readonly string[] }>,
1266
+ readonly [key: string]: readonly string[],
1267
+ readonly e: Readonly<{
1268
+ a: number,
1269
+ b: readonly string[],
1270
+ c: () => string,
1271
+ d: Readonly<{ [key: string]: readonly string[] }>,
1272
+ [key: string]: readonly string[],
1273
+ }>
1274
+ }
1275
+ `,
1276
+ },
1277
+ {
1278
+ name: 'Interface with call signatures and method signatures',
1279
+ source: dedent`
1280
+ interface Foo {
1281
+ (): void;
1282
+ foo(): void;
1283
+ }
1284
+ `,
1285
+ expected: dedent`
1286
+ interface Foo {
1287
+ (): void;
1288
+ foo(): void;
1289
+ }
1290
+ `,
1291
+ },
1292
+ {
1293
+ name: 'Interface extends clause',
1294
+ source: dedent`
1295
+ interface Foo extends Box<[X[]]> {}
1296
+ `,
1297
+ expected: dedent`
1298
+ interface Foo extends Box<readonly [readonly X[]]> {}
1299
+ `,
1300
+ },
1301
+ {
1302
+ name: 'Interface multiple extends',
1303
+ source: dedent`
1304
+ interface A { a: string[]; }
1305
+ interface B { b: number[]; }
1306
+ interface C extends A, B { c: boolean[]; }
1307
+ `,
1308
+ expected: dedent`
1309
+ interface A { readonly a: readonly string[]; }
1310
+ interface B { readonly b: readonly number[]; }
1311
+ interface C extends A, B { readonly c: readonly boolean[]; }
1312
+ `,
1313
+ },
1314
+ {
1315
+ name: 'Interface with call/construct signatures',
1316
+ source: dedent`
1317
+ interface CallableConstructable {
1318
+ (arg: number[]): string[];
1319
+ new (arg: Map<string, number>): Set<boolean[]>;
1320
+ prop: string[];
1321
+ }
1322
+ `,
1323
+ expected: dedent`
1324
+ interface CallableConstructable {
1325
+ (arg: readonly number[]): readonly string[];
1326
+ new (arg: ReadonlyMap<string, number>): ReadonlySet<readonly boolean[]>;
1327
+ readonly prop: readonly string[];
1328
+ }
1329
+ `,
1330
+ },
1331
+
1332
+ {
1333
+ name: 'Symbol',
1334
+ source: dedent`
1335
+ interface FileSystemDirectoryHandle {
1336
+ [Symbol.asyncIterator](): FileSystemDirectoryHandleAsyncIterator<[string, FileSystemHandle]>;
1337
+ entries(): FileSystemDirectoryHandleAsyncIterator<[string, FileSystemHandle]>;
1338
+ keys(): FileSystemDirectoryHandleAsyncIterator<string>;
1339
+ values(): FileSystemDirectoryHandleAsyncIterator<FileSystemHandle>;
1340
+ }
1341
+ `,
1342
+ expected: dedent`
1343
+ interface FileSystemDirectoryHandle {
1344
+ [Symbol.asyncIterator](): FileSystemDirectoryHandleAsyncIterator<readonly [string, FileSystemHandle]>;
1345
+ entries(): FileSystemDirectoryHandleAsyncIterator<readonly [string, FileSystemHandle]>;
1346
+ keys(): FileSystemDirectoryHandleAsyncIterator<string>;
1347
+ values(): FileSystemDirectoryHandleAsyncIterator<FileSystemHandle>;
1348
+ }
1349
+ `,
1350
+ },
1351
+ ])('$name', testFn);
1352
+ });
1353
+
1354
+ describe('Classes', () => {
1355
+ describe('Property declarations', () => {
1356
+ test.each([
1357
+ {
1358
+ name: 'Basic property types',
1359
+ source: dedent`
1360
+ class Foo {
1361
+ a: number[];
1362
+ b: Array<string>;
1363
+ c: readonly boolean[];
1364
+ d: ReadonlyArray<bigint>;
1365
+ }
1366
+ `,
1367
+ expected: dedent`
1368
+ class Foo {
1369
+ readonly a: readonly number[];
1370
+ readonly b: readonly string[];
1371
+ readonly c: readonly boolean[];
1372
+ readonly d: readonly bigint[];
1373
+ }
1374
+ `,
1375
+ },
1376
+ {
1377
+ name: 'Properties with various modifiers',
1378
+ source: dedent`
1379
+ class Klass {
1380
+ foo: number;
1381
+ private bar: number;
1382
+ static baz: number;
1383
+ protected static qux: number;
1384
+ }
1385
+ `,
1386
+ expected: dedent`
1387
+ class Klass {
1388
+ readonly foo: number;
1389
+ private readonly bar: number;
1390
+ static readonly baz: number;
1391
+ protected static readonly qux: number;
1392
+ }
1393
+ `,
1394
+ },
1395
+ {
1396
+ name: 'Static property',
1397
+ source: dedent`
1398
+ class Foo {
1399
+ static a: number[];
1400
+ }
1401
+ `,
1402
+ expected: dedent`
1403
+ class Foo {
1404
+ static readonly a: readonly number[];
1405
+ }
1406
+ `,
1407
+ },
1408
+ {
1409
+ name: 'Class private identifier field',
1410
+ source: dedent`
1411
+ class HashField {
1412
+ #data: string[] = [];
1413
+ getData() { return this.#data; }
1414
+ }
1415
+ `,
1416
+ expected: dedent`
1417
+ class HashField {
1418
+ readonly #data: readonly string[] = [];
1419
+ getData() { return this.#data; }
1420
+ }
1421
+ `,
1422
+ },
1423
+ ])('$name', testFn);
1424
+ });
1425
+
1426
+ describe('Methods', () => {
1427
+ test.each([
1428
+ {
1429
+ name: 'Mutable arrays in class method body',
1430
+ source: dedent`
1431
+ class Foo {
1432
+ a() {
1433
+ const b: number[] = [];
1434
+ console.log(b);
1435
+ }
1436
+ }
1437
+ `,
1438
+ expected: dedent`
1439
+ class Foo {
1440
+ a() {
1441
+ const b: readonly number[] = [];
1442
+ console.log(b);
1443
+ }
1444
+ }
1445
+ `,
1446
+ },
1447
+ {
1448
+ name: 'Mutable arrays in class method parameters',
1449
+ source: dedent`
1450
+ class Foo {
1451
+ a(s: string[], p: { x: number }) {}
1452
+ }
1453
+ `,
1454
+ expected: dedent`
1455
+ class Foo {
1456
+ a(s: readonly string[], p: Readonly<{ x: number }>) {}
1457
+ }
1458
+ `,
1459
+ },
1460
+ {
1461
+ name: 'Getter and Setter types',
1462
+ source: dedent`
1463
+ class Foo {
1464
+ get a(): number[] { return []; }
1465
+ set a(s: string[], p: { x: number }) {}
1466
+ }
1467
+ `,
1468
+ expected: dedent`
1469
+ class Foo {
1470
+ get a(): readonly number[] { return []; }
1471
+ set a(s: readonly string[], p: Readonly<{ x: number }>) {}
1472
+ }
1473
+ `,
1474
+ },
1475
+ {
1476
+ name: 'Class static method return/params',
1477
+ source: dedent`
1478
+ class Util {
1479
+ static process(data: Map<string, number[]>): Set<string>[] { return []; }
1480
+ }
1481
+ `,
1482
+ expected: dedent`
1483
+ class Util {
1484
+ static process(data: ReadonlyMap<string, readonly number[]>): readonly ReadonlySet<string>[] { return []; }
1485
+ }
1486
+ `,
1487
+ },
1488
+ ])('$name', testFn);
1489
+ });
1490
+
1491
+ describe('Parameter properties', () => {
1492
+ test.each([
1493
+ {
1494
+ name: 'Non-readonly class parameter properties',
1495
+ source: dedent`
1496
+ class Klass {
1497
+ constructor (
1498
+ prop: string,
1499
+ public publicProp: string,
1500
+ protected protectedProp: string,
1501
+ private privateProp: string,
1502
+ ) { }
1503
+ }
1504
+ `,
1505
+ expected: dedent`
1506
+ class Klass {
1507
+ constructor (
1508
+ prop: string,
1509
+ public readonly publicProp: string,
1510
+ protected readonly protectedProp: string,
1511
+ private readonly privateProp: string,
1512
+ ) { }
1513
+ }
1514
+ `,
1515
+ },
1516
+ {
1517
+ name: 'Already readonly class parameter properties',
1518
+ source: dedent`
1519
+ class Klass {
1520
+ constructor (
1521
+ readonly prop: string,
1522
+ public readonly publicProp: string,
1523
+ protected readonly protectedProp: string,
1524
+ private readonly privateProp: string,
1525
+ ) { }
1526
+ }
1527
+ `,
1528
+ expected: dedent`
1529
+ class Klass {
1530
+ constructor (
1531
+ readonly prop: string,
1532
+ public readonly publicProp: string,
1533
+ protected readonly protectedProp: string,
1534
+ private readonly privateProp: string,
1535
+ ) { }
1536
+ }
1537
+ `,
1538
+ },
1539
+ {
1540
+ name: 'Object or array parameter properties',
1541
+ source: dedent`
1542
+ class Klass {
1543
+ constructor (
1544
+ prop: string,
1545
+ public publicProp: Array<string>,
1546
+ protected protectedProp: { a: string },
1547
+ private privateProp: string[],
1548
+ ) { }
1549
+ }
1550
+ `,
1551
+ expected: dedent`
1552
+ class Klass {
1553
+ constructor (
1554
+ prop: string,
1555
+ public readonly publicProp: readonly string[],
1556
+ protected readonly protectedProp: Readonly<{ a: string }>,
1557
+ private readonly privateProp: readonly string[],
1558
+ ) { }
1559
+ }
1560
+ `,
1561
+ },
1562
+ ])('$name', testFn);
1563
+ });
1564
+
1565
+ describe('Index signature', () => {
1566
+ test.each([
1567
+ {
1568
+ name: 'Index signature in class',
1569
+ source: dedent`
1570
+ class Klass {
1571
+ [key: string]: string[]
1572
+ }
1573
+ `,
1574
+ expected: dedent`
1575
+ class Klass {
1576
+ readonly [key: string]: readonly string[]
1577
+ }
1578
+ `,
1579
+ },
1580
+ {
1581
+ name: 'Class with property and index signature',
1582
+ source: dedent`
1583
+ class Klass {
1584
+ x: number[] = [];
1585
+ [key: string]: string[] | number[]
1586
+ }
1587
+ `,
1588
+ expected: dedent`
1589
+ class Klass {
1590
+ readonly x: readonly number[] = [];
1591
+ readonly [key: string]: readonly string[] | readonly number[]
1592
+ }
1593
+ `,
1594
+ },
1595
+ ])('$name', testFn);
1596
+ });
1597
+
1598
+ describe('Type Assertions in Initializers', () => {
1599
+ test.each([
1600
+ {
1601
+ name: 'Type Assertions in Property Initializers',
1602
+ source: dedent`
1603
+ class Foo {
1604
+ a: number[] = [] as number[];
1605
+ b: Array<string> = [] as Array<string>;
1606
+ c: readonly boolean[] = [] as readonly boolean[];
1607
+ d: ReadonlyArray<bigint> = [] as ReadonlyArray<bigint>;
1608
+ }
1609
+ `,
1610
+ expected: dedent`
1611
+ class Foo {
1612
+ readonly a: readonly number[] = [] as readonly number[];
1613
+ readonly b: readonly string[] = [] as readonly string[];
1614
+ readonly c: readonly boolean[] = [] as readonly boolean[];
1615
+ readonly d: readonly bigint[] = [] as readonly bigint[];
1616
+ }
1617
+ `,
1618
+ },
1619
+ ])('$name', testFn);
1620
+ });
1621
+ });
1622
+
1623
+ describe('Functions', () => {
1624
+ describe('Spread syntax (Rest parameters)', () => {
1625
+ test.each([
1626
+ {
1627
+ name: 'Rest parameter with explicit ReadonlyArray type',
1628
+ source: dedent`
1629
+ function foo(...a: ReadonlyArray<number>) {}
1630
+ `,
1631
+ expected: dedent`
1632
+ function foo(...a: readonly number[]) {}
1633
+ `,
1634
+ },
1635
+ {
1636
+ name: 'Rest parameter with explicit readonly array type',
1637
+ source: dedent`
1638
+ const foo = (...a: readonly number[]) => {}
1639
+ `,
1640
+ expected: dedent`
1641
+ const foo = (...a: readonly number[]) => {}
1642
+ `,
1643
+ },
1644
+ {
1645
+ name: 'Rest parameter with mutable array type',
1646
+ source: dedent`
1647
+ const foo = (...a: unknown[]) => {}
1648
+ `,
1649
+ expected: dedent`
1650
+ const foo = (...a: readonly unknown[]) => {}
1651
+ `,
1652
+ },
1653
+ {
1654
+ name: 'Rest parameter with Readonly',
1655
+ source: dedent`
1656
+ const foo = (...a: Readonly<unknown[]>) => {}
1657
+ `,
1658
+ expected: dedent`
1659
+ const foo = (...a: readonly unknown[]) => {}
1660
+ `,
1661
+ },
1662
+ ])('$name', testFn);
1663
+ });
1664
+
1665
+ describe('Return types', () => {
1666
+ test.each([
1667
+ {
1668
+ name: 'Mutable return types (function declaration)',
1669
+ source: dedent`
1670
+ declare function f1(...numbers: ReadonlyArray<number>): Array<number>;
1671
+ declare function f2(...numbers: readonly number[]): number[];
1672
+ declare function f3(...numbers: ReadonlyArray<number>): Promise<Array<number>>;
1673
+ declare function f4(...numbers: number[]): Promise<number[]>;
1674
+ declare function f5(...numbers: Array<number>): Promise<Foo<Array<number>>>;
1675
+ declare function f6(...numbers: Readonly<number[]>): Promise<Foo<number[]>>;
1676
+ `,
1677
+ expected: dedent`
1678
+ declare function f1(...numbers: readonly number[]): readonly number[];
1679
+ declare function f2(...numbers: readonly number[]): readonly number[];
1680
+ declare function f3(...numbers: readonly number[]): Promise<readonly number[]>;
1681
+ declare function f4(...numbers: readonly number[]): Promise<readonly number[]>;
1682
+ declare function f5(...numbers: readonly number[]): Promise<Foo<readonly number[]>>;
1683
+ declare function f6(...numbers: readonly number[]): Promise<Foo<readonly number[]>>;
1684
+ `,
1685
+ },
1686
+ {
1687
+ name: 'Mutable return types (function expression)',
1688
+ source: dedent`
1689
+ const foo = (...numbers: ReadonlyArray<number>): Array<number> => {}
1690
+ const bar = (...numbers: readonly number[]): number[] => {}
1691
+ `,
1692
+ expected: dedent`
1693
+ const foo = (...numbers: readonly number[]): readonly number[] => {}
1694
+ const bar = (...numbers: readonly number[]): readonly number[] => {}
1695
+ `,
1696
+ },
1697
+ {
1698
+ name: 'Mutable return types (class method)',
1699
+ source: dedent`
1700
+ class Foo {
1701
+ foo(...numbers: ReadonlyArray<number>): Array<number> {}
1702
+ }
1703
+ class Bar {
1704
+ foo(...numbers: number[]): number[] {}
1705
+ }
1706
+ `,
1707
+ expected: dedent`
1708
+ class Foo {
1709
+ foo(...numbers: readonly number[]): readonly number[] {}
1710
+ }
1711
+ class Bar {
1712
+ foo(...numbers: readonly number[]): readonly number[] {}
1713
+ }
1714
+ `,
1715
+ },
1716
+ {
1717
+ name: 'Mutable return types (intersection)',
1718
+ source: dedent`
1719
+ declare function foo(...numbers: ReadonlyArray<number>): { readonly a: Array<number> } & { readonly b: string[] }
1720
+ `,
1721
+ expected: dedent`
1722
+ declare function foo(...numbers: readonly number[]): Readonly<{ a: readonly number[] } & { b: readonly string[] }>
1723
+ `,
1724
+ },
1725
+ {
1726
+ name: 'Mutable return types (union)',
1727
+ source: dedent`
1728
+ declare function foo(...numbers: ReadonlyArray<number>): { readonly a: Array<number> } | { readonly b: string[] }
1729
+ `,
1730
+ expected: dedent`
1731
+ declare function foo(...numbers: readonly number[]): Readonly<{ a: readonly number[] } | { b: readonly string[] }>
1732
+ `,
1733
+ },
1734
+ {
1735
+ name: 'Mutable return types (wrapped in another type)',
1736
+ source: dedent`
1737
+ type Foo<T> = { readonly x: T; };
1738
+ declare function func1(...numbers: ReadonlyArray<number>): Promise<Foo<Array<number>>>
1739
+ declare function func2(...numbers: ReadonlyArray<number>): Promise<Foo<number[]>>
1740
+ `,
1741
+ expected: dedent`
1742
+ type Foo<T> = Readonly<{ x: T; }>;
1743
+ declare function func1(...numbers: readonly number[]): Promise<Foo<readonly number[]>>
1744
+ declare function func2(...numbers: readonly number[]): Promise<Foo<readonly number[]>>
1745
+ `,
1746
+ },
1747
+ {
1748
+ name: 'Mutable return types (conditional)',
1749
+ source: dedent`
1750
+ declare function foo<T>(x: T): T extends Array<number> ? string : number[]
1751
+ `,
1752
+ expected: dedent`
1753
+ declare function foo<T>(x: T): T extends readonly number[] ? string : readonly number[]
1754
+ `,
1755
+ },
1756
+ {
1757
+ name: 'Mutable return type with type assertion',
1758
+ source: dedent`
1759
+ function foo(bar: string): { baz: number } {
1760
+ return {} as { baz: number };
1761
+ }
1762
+ `,
1763
+ expected: dedent`
1764
+ function foo(bar: string): Readonly<{ baz: number }> {
1765
+ return {} as Readonly<{ baz: number }>;
1766
+ }
1767
+ `,
1768
+ },
1769
+ {
1770
+ name: 'Already readonly return type (generic)',
1771
+ source: dedent`
1772
+ function foo(): ReadonlyArray<number> {
1773
+ return [1, 2, 3];
1774
+ }
1775
+ `,
1776
+ expected: dedent`
1777
+ function foo(): readonly number[] {
1778
+ return [1, 2, 3];
1779
+ }
1780
+ `,
1781
+ },
1782
+ {
1783
+ name: 'Already readonly return type (non-generic)',
1784
+ source: dedent`
1785
+ const foo = (): readonly number[] => {
1786
+ return [1, 2, 3];
1787
+ }
1788
+ `,
1789
+ expected: dedent`
1790
+ const foo = (): readonly number[] => {
1791
+ return [1, 2, 3];
1792
+ }
1793
+ `,
1794
+ },
1795
+ {
1796
+ name: 'Implicit readonly return type in function (as const)',
1797
+ source: dedent`
1798
+ function foo() {
1799
+ return [1, 2, 3] as const;
1800
+ }
1801
+ `,
1802
+ expected: dedent`
1803
+ function foo() {
1804
+ return [1, 2, 3] as const;
1805
+ }
1806
+ `,
1807
+ },
1808
+ {
1809
+ name: 'Implicit readonly return type in arrow function (as const)',
1810
+ source: dedent`
1811
+ const foo = () => {
1812
+ return [1, 2, 3] as const;
1813
+ };
1814
+ `,
1815
+ expected: dedent`
1816
+ const foo = () => {
1817
+ return [1, 2, 3] as const;
1818
+ };
1819
+ `,
1820
+ },
1821
+ ])('$name', testFn);
1822
+ });
1823
+
1824
+ describe('Local variables', () => {
1825
+ test.each([
1826
+ {
1827
+ name: 'Mutable local variables',
1828
+ source: dedent`
1829
+ function foo() {
1830
+ let foo: {
1831
+ a: number,
1832
+ b: ReadonlyArray<string>,
1833
+ c: () => string,
1834
+ d: { [key: string]: string[] },
1835
+ e: { [key: string]: string[] },
1836
+ readonly f: {
1837
+ a: number,
1838
+ b: ReadonlyArray<string>,
1839
+ c: () => string,
1840
+ d: { [key: string]: string[] },
1841
+ [key: string]: string[],
1842
+ }
1843
+ }
1844
+ };
1845
+ `,
1846
+ expected: dedent`
1847
+ function foo() {
1848
+ let foo: Readonly<{
1849
+ a: number,
1850
+ b: readonly string[],
1851
+ c: () => string,
1852
+ d: Readonly<{ [key: string]: readonly string[] }>,
1853
+ e: Readonly<{ [key: string]: readonly string[] }>,
1854
+ f: Readonly<{
1855
+ a: number,
1856
+ b: readonly string[],
1857
+ c: () => string,
1858
+ d: Readonly<{ [key: string]: readonly string[] }>,
1859
+ [key: string]: readonly string[],
1860
+ }>
1861
+ }>
1862
+ };
1863
+ `,
1864
+ },
1865
+ ])('$name', testFn);
1866
+ });
1867
+
1868
+ describe('Generics', () => {
1869
+ test.each([
1870
+ {
1871
+ name: 'Generic constraint',
1872
+ source: dedent`
1873
+ function process<T extends { data: string[] }>(input: T): T { return input; }
1874
+ `,
1875
+ expected: dedent`
1876
+ function process<T extends Readonly<{ data: readonly string[] }>>(input: T): T { return input; }
1877
+ `,
1878
+ },
1879
+ {
1880
+ name: 'Generic default type',
1881
+ source: dedent`
1882
+ type Container<T = Map<string, number[]>> = { item: T }; type E = Container;
1883
+ `,
1884
+ expected: dedent`
1885
+ type Container<T = ReadonlyMap<string, readonly number[]>> = Readonly<{ item: T }>; type E = Container;
1886
+ `,
1887
+ },
1888
+ {
1889
+ name: 'Generic function using type parameter in array',
1890
+ source: dedent`
1891
+ function wrapArray<T>(input: T): T[] { return [input]; }
1892
+ `,
1893
+ expected: dedent`
1894
+ function wrapArray<T>(input: T): readonly T[] { return [input]; }
1895
+ `,
1896
+ },
1897
+ {
1898
+ name: 'Generic function using type parameter in map/set',
1899
+ source: dedent`
1900
+ function wrapMap<T>(input: T): Map<string, T[]> { return new Map([["key", [input]]]); }
1901
+ `,
1902
+ expected: dedent`
1903
+ function wrapMap<T>(input: T): ReadonlyMap<string, readonly T[]> { return new Map([["key", [input]]]); }
1904
+ `,
1905
+ },
1906
+ {
1907
+ name: 'Generic constraint with conditional type',
1908
+ source: dedent`
1909
+ type Constrained<T extends string[] | number[]> = T extends string[] ? { s: T } : { n: T }; type G = Constrained<boolean[][]>;
1910
+ `,
1911
+ expected: dedent`
1912
+ type Constrained<T extends readonly string[] | readonly number[]> = T extends readonly string[] ? Readonly<{ s: T }> : Readonly<{ n: T }>; type G = Constrained<readonly (readonly boolean[])[]>;
1913
+ `,
1914
+ },
1915
+ ])('$name', testFn);
1916
+ });
1917
+ });
1918
+
1919
+ describe('Type predicate', () => {
1920
+ test.each([
1921
+ {
1922
+ name: 'Type predicate for array length',
1923
+ source: dedent`
1924
+ const isArrayOfLength1 = <A,>(
1925
+ array: A[],
1926
+ ): array is [A, ...A[]] => array.length >= 1;
1927
+ `,
1928
+ expected: dedent`
1929
+ const isArrayOfLength1 = <A,>(
1930
+ array: readonly A[],
1931
+ ): array is readonly [A, ...A[]] => array.length >= 1;
1932
+ `,
1933
+ },
1934
+ ])('$name', testFn);
1935
+ });
1936
+
1937
+ describe('Type parameter', () => {
1938
+ test.each([
1939
+ {
1940
+ name: 'Mapped type in complex generic function',
1941
+ source: dedent`
1942
+ const tupleMap = <const T extends unknown[], const B>(
1943
+ tpl: T,
1944
+ mapFn: (a: T[number], index: number) => B,
1945
+ ): { [K in keyof T]: B } =>
1946
+ tpl.map(mapFn as (a: unknown, index: number) => B) as {
1947
+ [K in keyof T]: B;
1948
+ };
1949
+ `,
1950
+ expected: dedent`
1951
+ const tupleMap = <const T extends readonly unknown[], const B>(
1952
+ tpl: T,
1953
+ mapFn: (a: T[number], index: number) => B,
1954
+ ): Readonly<{ [K in keyof T]: B }> =>
1955
+ tpl.map(mapFn as (a: unknown, index: number) => B) as Readonly<{
1956
+ [K in keyof T]: B;
1957
+ }>;
1958
+ `,
1959
+ },
1960
+ ])('$name', testFn);
1961
+ });
1962
+
1963
+ describe('Primitive and keyword types', () => {
1964
+ test.each([
1965
+ {
1966
+ name: 'Basic primitive types',
1967
+ source: dedent`
1968
+ type Primitives = { a: string; b: number; c: boolean; d: bigint; e: symbol; };
1969
+ `,
1970
+ expected: dedent`
1971
+ type Primitives = Readonly<{ a: string; b: number; c: boolean; d: bigint; e: symbol; }>;
1972
+ `,
1973
+ },
1974
+ {
1975
+ name: 'Null, undefined, void',
1976
+ source: dedent`
1977
+ type Special = { a: null; b: undefined; c: void }
1978
+ `,
1979
+ expected: dedent`
1980
+ type Special = Readonly<{ a: null; b: undefined; c: void }>;
1981
+ `,
1982
+ },
1983
+ {
1984
+ name: 'Any, unknown, never',
1985
+ source: dedent`
1986
+ type AnyNever = { a: any; b: unknown; c: never }
1987
+ `,
1988
+ expected: dedent`
1989
+ type AnyNever = Readonly<{ a: any; b: unknown; c: never }>
1990
+ `,
1991
+ },
1992
+ {
1993
+ name: 'Object keyword',
1994
+ source: dedent`
1995
+ type Obj = { a: object }
1996
+ `,
1997
+ expected: dedent`
1998
+ type Obj = Readonly<{ a: object }>
1999
+ `,
2000
+ },
2001
+ {
2002
+ name: 'This type in class context',
2003
+ source: dedent`
2004
+ class MyClass {
2005
+ value: number[];
2006
+ compare(other: this): boolean { return this.value.length === other.value.length; }
2007
+ }
2008
+ `,
2009
+ expected: dedent`
2010
+ class MyClass {
2011
+ readonly value: readonly number[];
2012
+ compare(other: this): boolean { return this.value.length === other.value.length; }
2013
+ }
2014
+ `,
2015
+ },
2016
+ ])('$name', testFn);
2017
+ });
2018
+
2019
+ describe('Union and intersection types', () => {
2020
+ test.each([
2021
+ ...toUnionAndIntersectionTestCase({
2022
+ title: (op) => `${op} of arrays`,
2023
+ testCase: (op) => ({
2024
+ source: `type Arr = string[] ${op} readonly number[];`,
2025
+ expected: `type Arr = readonly string[] ${op} readonly number[];`,
2026
+ }),
2027
+ }),
2028
+
2029
+ ...toUnionAndIntersectionTestCase({
2030
+ title: (op) => `${op} of objects`,
2031
+ testCase: (op) => ({
2032
+ source: `type Obj = { a: string[] } ${op} { b: readonly number[] };`,
2033
+ expected: `type Obj = Readonly<{ a: readonly string[] } ${op} { b: readonly number[] }>;`,
2034
+ }),
2035
+ }),
2036
+
2037
+ ...toUnionAndIntersectionTestCase({
2038
+ title: (op) => `${op} including non-object/array`,
2039
+ testCase: (op) => ({
2040
+ source: `type Obj = { a: readonly string[] } ${op} readonly number[];`,
2041
+ expected: `type Obj = Readonly<{ a: readonly string[] }> ${op} readonly number[];`,
2042
+ }),
2043
+ }),
2044
+
2045
+ ...toUnionAndIntersectionTestCase({
2046
+ title: (op) => `${op} where only one part is normalized`,
2047
+ testCase: (op) => ({
2048
+ source: `type PartialReadonlyType = Readonly<string[]> ${op} number[];`,
2049
+ expected: `type PartialReadonlyType = readonly string[] ${op} readonly number[];`,
2050
+ }),
2051
+ }),
2052
+
2053
+ ...toUnionAndIntersectionTestCase({
2054
+ title: (op) => `${op} of Readonly objects`,
2055
+ testCase: (op) => ({
2056
+ source: `type Obj = Readonly<{ a: string[] }> ${op} Readonly<{ b: readonly number[] }>;`,
2057
+ // Readonly<A> | Readonly<B> -> Readonly<A | B> (Collapse)
2058
+ // Inner types are not normalized (string[], readonly number[])
2059
+ expected: `type Obj = Readonly<{ a: readonly string[] } ${op} { b: readonly number[] }>;`,
2060
+ }),
2061
+ }),
2062
+
2063
+ {
2064
+ name: 'Union of generic and non-generic readonly objects',
2065
+ source: dedent`
2066
+ type IntersectMixed = { readonly a: string[] } & Readonly<{ b: readonly number[] }>;
2067
+ `,
2068
+ expected: dedent`
2069
+ type IntersectMixed = Readonly<{ a: readonly string[] } & { b: readonly number[] }>;
2070
+ `,
2071
+ },
2072
+ {
2073
+ name: 'Intersection of readonly object and non-readonly object',
2074
+ source: dedent`
2075
+ type IntersectMixed = { readonly a: string[] } & { b: readonly number[] };
2076
+ `,
2077
+ expected: dedent`
2078
+ type IntersectMixed = Readonly<{ a: readonly string[] } & { b: readonly number[] }>;
2079
+ `,
2080
+ },
2081
+ {
2082
+ name: 'Nested union/intersection',
2083
+ source: dedent`
2084
+ type Nested = (string[] | { x: Map<string, number[]> }) & { y: Set<boolean[]> };
2085
+ `,
2086
+ // Each part is not a normalization target, so unchanged
2087
+ expected: dedent`
2088
+ type Nested = (readonly string[] | Readonly<{ x: ReadonlyMap<string, readonly number[]> }>)
2089
+ & Readonly<{ y: ReadonlySet<readonly boolean[]> }>;
2090
+ `,
2091
+ },
2092
+ {
2093
+ name: 'Nested union/intersection 2',
2094
+ source: dedent`
2095
+ type Nested = ({ x: number[] } | { y: string[] }) & { z: boolean[] };
2096
+ `,
2097
+ // Each part is not a normalization target, so unchanged
2098
+ expected: dedent`
2099
+ type Nested = Readonly<
2100
+ ({ x: readonly number[] } | { y: readonly string[] }) & { z: readonly boolean[] }
2101
+ >;
2102
+ `,
2103
+ },
2104
+
2105
+ ...toUnionAndIntersectionTestCase({
2106
+ title: (op) => `${op} where only some become Readonly<*>`,
2107
+ testCase: (op) => ({
2108
+ source: `type PartialReadonly = Readonly<string[]> ${op} number[];`,
2109
+ expected: `type PartialReadonly = readonly string[] ${op} readonly number[];`,
2110
+ }),
2111
+ }),
2112
+
2113
+ ...toUnionAndIntersectionTestCase({
2114
+ title: (op) => `${op} collapse with Array types`,
2115
+ testCase: (op) => ({
2116
+ source: `type Arr = Readonly<string[]> ${op} Readonly<number[]>;`,
2117
+ // Readonly<A[]> | Readonly<B[]> -> readonly A[] | readonly B[]
2118
+ // Readonly<A[]> & Readonly<B[]> -> readonly A[] & readonly B[]
2119
+ expected: `type Arr = readonly string[] ${op} readonly number[];`,
2120
+ }),
2121
+ }),
2122
+
2123
+ ...toUnionAndIntersectionTestCase({
2124
+ title: (op) => `${op} of readonly arrays in Readonly`,
2125
+ testCase: (op) => ({
2126
+ source: `type Arr = Readonly<readonly string[] ${op} readonly number[]>;`,
2127
+ expected: `type Arr = readonly string[] ${op} readonly number[];`,
2128
+ }),
2129
+ }),
2130
+
2131
+ ...toUnionAndIntersectionTestCase({
2132
+ title: (op) => `${op} of primitives`,
2133
+ testCase: (op) => ({
2134
+ source: `type Arr = Readonly<number ${op} boolean ${op} string>;`,
2135
+ expected: `type Arr = number ${op} boolean ${op} string;`,
2136
+ }),
2137
+ }),
2138
+
2139
+ ...[
2140
+ (op: '|' | '&') => ({
2141
+ source: `type Mixed = Readonly<Readonly<{ x: string }> ${op} readonly number[]>;`,
2142
+ expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2143
+ }),
2144
+ (op: '|' | '&') => ({
2145
+ source: `type Mixed = Readonly<{ x: string } ${op} readonly number[]>;`,
2146
+ expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2147
+ }),
2148
+ (op: '|' | '&') => ({
2149
+ source: `type Mixed = Readonly<{ x: string }> ${op} Readonly<number[]>;`,
2150
+ expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2151
+ }),
2152
+ (op: '|' | '&') => ({
2153
+ source: `type Mixed = Readonly<{ x: string } ${op} Readonly<number[]>>;`,
2154
+ expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2155
+ }),
2156
+ (op: '|' | '&') => ({
2157
+ source: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2158
+ expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
2159
+ }),
2160
+ (op: '|' | '&') => ({
2161
+ source: `type Mixed = Readonly<{ x: string } ${op} readonly number[] ${op} { y: number } ${op} readonly string[]>;`,
2162
+ expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
2163
+ }),
2164
+ (op: '|' | '&') => ({
2165
+ source: `type Mixed = Readonly<{ x: string } ${op} readonly number[] ${op} { y: number } ${op} readonly string[]>;`,
2166
+ expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
2167
+ }),
2168
+ (op: '|' | '&') => ({
2169
+ source: `type Mixed = Readonly<{ x: string } ${op} number[] ${op} Readonly<{ y: number }> ${op} readonly string[]>;`,
2170
+ expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
2171
+ }),
2172
+ (op: '|' | '&') => ({
2173
+ source: `type Mixed = Readonly<unknown ${op} { x: string } ${op} number[] ${op} Readonly<{ y: number }> ${op} readonly string[]>;`,
2174
+ expected: `type Mixed = unknown ${op} Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
2175
+ }),
2176
+ (op: '|' | '&') => ({
2177
+ source: `type Primitive = Readonly<number ${op} boolean ${op} string>;`,
2178
+ expected: `type Primitive = number ${op} boolean ${op} string;`,
2179
+ }),
2180
+ ].flatMap((t, i) =>
2181
+ toUnionAndIntersectionTestCase({
2182
+ title: (op) => `${op} of mixed types ${i}`,
2183
+ testCase: t,
2184
+ }),
2185
+ ),
2186
+
2187
+ {
2188
+ name: 'Union with mixed array and object types',
2189
+ source: dedent`
2190
+ type MixedUnion = string[] | { a: number; }
2191
+ `,
2192
+ expected: dedent`
2193
+ type MixedUnion = readonly string[] | Readonly<{ a: number; }>;
2194
+ `,
2195
+ },
2196
+ {
2197
+ name: 'Intersection with mixed array and object types',
2198
+ source: dedent`
2199
+ type MixedIntersection = { id: string; } & { data: number[]; };
2200
+ `,
2201
+ expected: dedent`
2202
+ type MixedIntersection = Readonly<{ id: string; } & { data: readonly number[]; }>;
2203
+ `,
2204
+ },
2205
+ {
2206
+ name: 'Nested unions',
2207
+ source: dedent`
2208
+ type NestedUnion = (string[] | number[]) | { a: boolean[]; }
2209
+ `,
2210
+ expected: dedent`
2211
+ type NestedUnion = (readonly string[] | readonly number[]) | Readonly<{ a: readonly boolean[]; }>;
2212
+ `,
2213
+ },
2214
+ {
2215
+ name: 'Nested intersections',
2216
+ source: dedent`
2217
+ type NestedIntersection = ({ a: string; } & { b: number; }) & { c: boolean[]; };
2218
+ `,
2219
+ expected: dedent`
2220
+ type NestedIntersection = Readonly<({ a: string; } & { b: number; }) & { c: readonly boolean[]; }>;
2221
+ `,
2222
+ },
2223
+ {
2224
+ name: 'Union with generic types',
2225
+ source: dedent`
2226
+ type GenericUnion<T> = T[] | { value: T; }
2227
+ `,
2228
+ expected: dedent`
2229
+ type GenericUnion<T> = readonly T[] | Readonly<{ value: T; }>;
2230
+ `,
2231
+ },
2232
+ {
2233
+ name: 'Intersection with generic types',
2234
+ source: dedent`
2235
+ type GenericIntersection<T> = { id: string; } & { data: T[]; };
2236
+ `,
2237
+ expected: dedent`
2238
+ type GenericIntersection<T> = Readonly<{ id: string; } & { data: readonly T[]; }>;
2239
+ `,
2240
+ },
2241
+ {
2242
+ name: 'Union with DeepReadonly',
2243
+ source: dedent`
2244
+ type DeepReadonlyUnion = DeepReadonly<{ a: number; }> | string[];
2245
+ `,
2246
+ expected: dedent`
2247
+ type DeepReadonlyUnion = DeepReadonly<{ a: number; }> | readonly string[];
2248
+ `,
2249
+ },
2250
+ {
2251
+ name: 'Complex union with multiple types',
2252
+ source: dedent`
2253
+ type ComplexUnion = string | number[] | { a: boolean; } | Map<string, number[]>;
2254
+ `,
2255
+ expected: dedent`
2256
+ type ComplexUnion = string | readonly number[] | Readonly<{ a: boolean; }> | ReadonlyMap<string, readonly number[]>;
2257
+ `,
2258
+ },
2259
+ {
2260
+ name: 'Union with function types',
2261
+ source: dedent`
2262
+ type FunctionUnion = ((a: string[]) => void) | { handler: (b: number[]) => boolean; };
2263
+ `,
2264
+ expected: dedent`
2265
+ type FunctionUnion = ((a: readonly string[]) => void) | Readonly<{ handler: (b: readonly number[]) => boolean; }>;
2266
+ `,
2267
+ },
2268
+ {
2269
+ // The case where the entire intersection member directly under Readonly has been converted with replaceWithText
2270
+ name: 'Union with Readonly',
2271
+ source: dedent`
2272
+ type ReadonlyUnion = Readonly<{ a: string[]; } | { b: number[]; }>;
2273
+ `,
2274
+ expected: dedent`
2275
+ type ReadonlyUnion = Readonly<{ a: readonly string[]; } | { b: readonly number[]; }>;
2276
+ `,
2277
+ },
2278
+ ])('$name', testFn);
2279
+ });
2280
+
2281
+ describe('Parenthesized types', () => {
2282
+ test.each([
2283
+ {
2284
+ name: 'Parenthesized readonly array',
2285
+ source: dedent`
2286
+ type ParenReadonlyArr = (readonly string[])
2287
+ `,
2288
+ expected: dedent`
2289
+ type ParenReadonlyArr = readonly string[]
2290
+ `,
2291
+ },
2292
+ {
2293
+ name: 'Parenthesized nested readonly array',
2294
+ source: dedent`
2295
+ type ParenNestedReadonlyArr = readonly (readonly string[])[]
2296
+ `,
2297
+ expected: dedent`
2298
+ type ParenNestedReadonlyArr = readonly (readonly string[])[];
2299
+ `,
2300
+ },
2301
+ {
2302
+ name: 'Parenthesized readonly tuple',
2303
+ source: dedent`
2304
+ type ParenReadonlyTup = (readonly [string, number])
2305
+ `,
2306
+ expected: dedent`
2307
+ type ParenReadonlyTup = readonly [string, number]
2308
+ `,
2309
+ },
2310
+ {
2311
+ name: 'Parenthesized type literal',
2312
+ source: dedent`
2313
+ type ParenObj = ({ a: number[] })
2314
+ `,
2315
+ expected: dedent`
2316
+ type ParenObj = Readonly<{ a: readonly number[] }>
2317
+ `,
2318
+ },
2319
+ {
2320
+ name: 'Parenthesized type with union/intersection',
2321
+ source: dedent`
2322
+ type Paren = ({ a: string[] } | { b: number[] })[]
2323
+ `,
2324
+ expected: dedent`
2325
+ type Paren = readonly Readonly<{ a: readonly string[] } | { b: readonly number[] }>[]
2326
+ `,
2327
+ },
2328
+ {
2329
+ name: 'Nested Parentheses Removal',
2330
+ source: dedent`
2331
+ type NestedParen = ((readonly number[]))
2332
+ `,
2333
+ // ((readonly T[])) -> (readonly T[]) -> readonly T[]
2334
+ expected: dedent`
2335
+ type NestedParen = readonly number[]
2336
+ `,
2337
+ },
2338
+ {
2339
+ name: 'Parentheses around primitive',
2340
+ source: dedent`
2341
+ type ParenPrim = (number)
2342
+ `,
2343
+ expected: dedent`
2344
+ type ParenPrim = number;
2345
+ `, // Parentheses removed
2346
+ },
2347
+ {
2348
+ name: 'Parentheses around Readonly<T>',
2349
+ source: dedent`
2350
+ type ParenReadonly = (Readonly<{ a: number }>)
2351
+ `,
2352
+ // (Readonly<T>) -> Readonly<T> (Parentheses removed because inner type is TypeReference)
2353
+ expected: dedent`
2354
+ type ParenReadonly = Readonly<{ a: number }>
2355
+ `,
2356
+ },
2357
+ ])('$name', testFn);
2358
+ });
2359
+
2360
+ describe('IndexedAccessTypeNode', () => {
2361
+ test.each([
2362
+ {
2363
+ name: 'FlatArray',
2364
+ source: dedent`
2365
+ type FlatArray<Arr, Depth extends number> = {
2366
+ done: Arr;
2367
+ recur: Arr extends ReadonlyArray<infer InnerArr>
2368
+ ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
2369
+ : Arr;
2370
+ }[Depth extends -1 ? 'done' : 'recur'];
2371
+ `,
2372
+ expected: dedent`
2373
+ type FlatArray<Arr, Depth extends number> = {
2374
+ done: Arr;
2375
+ recur: Arr extends readonly (infer InnerArr)[]
2376
+ ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
2377
+ : Arr;
2378
+ }[Depth extends -1 ? 'done' : 'recur'];
2379
+ `,
2380
+ },
2381
+ {
2382
+ name: 'FlatArray (already readonly)',
2383
+ source: dedent`
2384
+ type FlatArray<Arr, Depth extends number> = Readonly<{
2385
+ done: Arr;
2386
+ recur: Arr extends ReadonlyArray<infer InnerArr>
2387
+ ? FlatArray<InnerArr, (readonly [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])[Depth]>
2388
+ : Arr;
2389
+ }>[Depth extends -1 ? 'done' : 'recur'];
2390
+ `,
2391
+ expected: dedent`
2392
+ type FlatArray<Arr, Depth extends number> = {
2393
+ done: Arr;
2394
+ recur: Arr extends readonly (infer InnerArr)[]
2395
+ ? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
2396
+ : Arr;
2397
+ }[Depth extends -1 ? 'done' : 'recur'];
2398
+ `,
2399
+ },
2400
+ {
2401
+ name: 'Indexed access type deeper',
2402
+ source: dedent`
2403
+ type DeepAccess = {
2404
+ a: { b: { c: number[] } }
2405
+ }["a"]["b"]["c"];
2406
+ `,
2407
+ expected: dedent`
2408
+ type DeepAccess = {
2409
+ a: { b: { c: readonly number[] } }
2410
+ }["a"]["b"]["c"];
2411
+ `,
2412
+ },
2413
+ {
2414
+ name: 'Indexed access type deeper (mixed)',
2415
+ source: dedent`
2416
+ type DeepAccess = {
2417
+ a: [{ b: number[] }, { c: number[] }]
2418
+ }["a"][1]["c"];
2419
+ `,
2420
+ expected: dedent`
2421
+ type DeepAccess = {
2422
+ a: [{ b: readonly number[] }, { c: readonly number[] }]
2423
+ }["a"][1]["c"];
2424
+ `,
2425
+ },
2426
+ {
2427
+ name: 'Indexed access type deeper (mixed, not sequent)',
2428
+ source: dedent`
2429
+ type DeepAccess = {
2430
+ a: {
2431
+ x: [{ b: number[] }, { c: number[] }][1]["c"]
2432
+ }
2433
+ }["a"];
2434
+ `,
2435
+ expected: dedent`
2436
+ type DeepAccess = {
2437
+ a: Readonly<{
2438
+ x: [{ b: readonly number[] }, { c: readonly number[] }][1]["c"]
2439
+ }>
2440
+ }["a"];
2441
+ `,
2442
+ },
2443
+ {
2444
+ name: 'Indexed access type deeper (mixed, not sequent 2)',
2445
+ source: dedent`
2446
+ type DeepAccess = {
2447
+ a: [
2448
+ { x: string[] },
2449
+ { y: { c: number[] }["c"] }["y"],
2450
+ ]
2451
+ }["a"][1];
2452
+ `,
2453
+ expected: dedent`
2454
+ type DeepAccess = {
2455
+ a: [
2456
+ Readonly<{ x: readonly string[] }>,
2457
+ { y: { c: readonly number[] }["c"] }["y"],
2458
+ ]
2459
+ }["a"][1];
2460
+ `,
2461
+ },
2462
+ {
2463
+ name: 'Array',
2464
+ source: dedent`
2465
+ type DeepAccess = {
2466
+ a: { y: { c: number[] }["c"] }["y"][]
2467
+ }["a"][1];
2468
+ `,
2469
+ expected: dedent`
2470
+ type DeepAccess = {
2471
+ a: { y: { c: readonly number[] }["c"] }["y"][]
2472
+ }["a"][1];
2473
+ `,
2474
+ },
2475
+ {
2476
+ name: 'MappedType',
2477
+ source: dedent`
2478
+ type Obj = { a: number, b: number, c: number };
2479
+ type IndexAccessOnMappedType = { [key in keyof Obj]: number[] }["a"];
2480
+ `,
2481
+ expected: dedent`
2482
+ type Obj = Readonly<{ a: number, b: number, c: number }>;
2483
+ type IndexAccessOnMappedType = { [key in keyof Obj]: readonly number[] }["a"];
2484
+ `,
2485
+ },
2486
+ // DeepReadonly pattern tests
2487
+ {
2488
+ name: 'DeepReadonly with IndexedAccessType',
2489
+ source: dedent`
2490
+ type DeepObj = DeepReadonly<{ a: { b: number[] } }>;
2491
+ type AccessDeep = DeepObj["a"]["b"];
2492
+ `,
2493
+ expected: dedent`
2494
+ type DeepObj = DeepReadonly<{ a: { b: number[] } }>;
2495
+ type AccessDeep = DeepObj["a"]["b"];
2496
+ `,
2497
+ },
2498
+ {
2499
+ name: 'DeepReadonly with nested IndexedAccessType',
2500
+ source: dedent`
2501
+ type Config = {
2502
+ settings: {
2503
+ options: { values: number[] }
2504
+ }
2505
+ };
2506
+ type DeepConfig = DeepReadonly<Config>;
2507
+ type AccessDeepNested = DeepConfig["settings"]["options"]["values"];
2508
+ `,
2509
+ expected: dedent`
2510
+ type Config = Readonly<{
2511
+ settings: Readonly<{
2512
+ options: Readonly<{ values: readonly number[] }>
2513
+ }>
2514
+ }>;
2515
+ type DeepConfig = DeepReadonly<Config>;
2516
+ type AccessDeepNested = DeepConfig["settings"]["options"]["values"];
2517
+ `,
2518
+ },
2519
+ // Union/Intersection with IndexedAccessType
2520
+ {
2521
+ name: 'Union with IndexedAccessType (inline)',
2522
+ source: dedent`
2523
+ type UnionAccess = ({ x: number[] } | { x: string[] })["x"];
2524
+ `,
2525
+ expected: dedent`
2526
+ type UnionAccess = ({ x: readonly number[] } | { x: readonly string[] })["x"];
2527
+ `,
2528
+ },
2529
+ {
2530
+ name: 'Intersection with IndexedAccessType (inline)',
2531
+ source: dedent`
2532
+ type IntersectionAccess = ({ x: number[]; shared: boolean[] } & { y: string[]; shared: boolean[] })["shared"];
2533
+ `,
2534
+ expected: dedent`
2535
+ type IntersectionAccess = ({ x: readonly number[]; shared: readonly boolean[] } & { y: readonly string[]; shared: readonly boolean[] })["shared"];
2536
+ `,
2537
+ },
2538
+ {
2539
+ name: 'Complex Union/Intersection with IndexedAccessType (inline)',
2540
+ source: dedent`
2541
+ type ComplexAccess = (({ x: number[] } | { y: string[] }) & ({ z: boolean[] } | { w: object[] }))["z" | "w"];
2542
+ `,
2543
+ expected: dedent`
2544
+ type ComplexAccess = (({ x: readonly number[] } | { y: readonly string[] }) & ({ z: readonly boolean[] } | { w: readonly object[] }))["z" | "w"];
2545
+ `,
2546
+ },
2547
+ // Named tuple with IndexedAccessType
2548
+ {
2549
+ name: 'Named tuple with IndexedAccessType (inline)',
2550
+ source: dedent`
2551
+ type AccessNamedTuple = [first: string, second: number[], third: { x: boolean[] }][1];
2552
+ `,
2553
+ expected: dedent`
2554
+ type AccessNamedTuple = [first: string, second: readonly number[], third: Readonly<{ x: readonly boolean[] }>][1];
2555
+ `,
2556
+ },
2557
+ {
2558
+ name: 'Complex named tuple with IndexedAccessType (inline)',
2559
+ source: dedent`
2560
+ type AccessComplexTuple = [
2561
+ id: string,
2562
+ data: { values: number[] }[],
2563
+ metadata: [owner: string, permissions: string[]]
2564
+ ][2][1];
2565
+ `,
2566
+ expected: dedent`
2567
+ type AccessComplexTuple = [
2568
+ id: string,
2569
+ data: Readonly<{ values: readonly number[] }>[],
2570
+ metadata: [owner: string, permissions: readonly string[]]
2571
+ ][2][1];
2572
+ `,
2573
+ },
2574
+ // Additional patterns
2575
+ {
2576
+ name: 'Conditional type with IndexedAccessType (inline)',
2577
+ source: dedent`
2578
+ type Result = ({ data: number[] } extends { data: infer U } ? U : never);
2579
+ `,
2580
+ expected: dedent`
2581
+ type Result = (Readonly<{ data: readonly number[] }> extends Readonly<{ data: infer U }> ? U : never);
2582
+ `,
2583
+ },
2584
+ {
2585
+ name: 'Template literal with IndexedAccessType (inline)',
2586
+ source: dedent`
2587
+ type UserParams = {
2588
+ "/users": { params: string[] },
2589
+ "/posts": { params: number[] }
2590
+ }["/users"]["params"];
2591
+ `,
2592
+ expected: dedent`
2593
+ type UserParams = {
2594
+ "/users": { params: readonly string[] },
2595
+ "/posts": { params: readonly number[] }
2596
+ }["/users"]["params"];
2597
+ `,
2598
+ },
2599
+ ])('$name', testFn);
2600
+ });
2601
+
2602
+ describe('Function and constructor types', () => {
2603
+ test.each([
2604
+ {
2605
+ name: 'Function type alias',
2606
+ source: dedent`
2607
+ type MyFunc = (arg: number[]) => string[]
2608
+ `,
2609
+ expected: dedent`
2610
+ type MyFunc = (arg: readonly number[]) => readonly string[];
2611
+ `,
2612
+ },
2613
+ {
2614
+ name: 'Function type variable',
2615
+ source: dedent`
2616
+ let fn: (map: Map<string, number>) => Set<string[]>
2617
+ `,
2618
+ expected: dedent`
2619
+ let fn: (map: ReadonlyMap<string, number>) => ReadonlySet<readonly string[]>;
2620
+ `,
2621
+ },
2622
+ {
2623
+ name: 'Constructor type alias',
2624
+ source: dedent`
2625
+ type MyConstructor = new (arg: string[]) => { prop: number[] };
2626
+ `,
2627
+ expected: dedent`
2628
+ type MyConstructor = new (arg: readonly string[]) => Readonly<{ prop: readonly number[] }>;
2629
+ `,
2630
+ },
2631
+ ])('$name', testFn);
2632
+ });
2633
+
2634
+ describe('Generic Types', () => {
2635
+ test.each([
2636
+ {
2637
+ name: 'Generic type parameter in interface',
2638
+ source: dedent`
2639
+ interface GenericInterface<T> { data: T[]; }
2640
+ `,
2641
+ expected: dedent`
2642
+ interface GenericInterface<T> { readonly data: readonly T[]; }
2643
+ `,
2644
+ },
2645
+ {
2646
+ name: 'Generic type parameter in type alias',
2647
+ source: dedent`
2648
+ type GenericType<T> = { data: T[]; }
2649
+ `,
2650
+ expected: dedent`
2651
+ type GenericType<T> = Readonly<{ data: readonly T[]; }>
2652
+ `,
2653
+ },
2654
+ {
2655
+ name: 'Multiple generic type parameters',
2656
+ source: dedent`
2657
+ type Pair<K, V> = { key: K; value: V[]; }
2658
+ `,
2659
+ expected: dedent`
2660
+ type Pair<K, V> = Readonly<{ key: K; value: readonly V[]; }>;
2661
+ `,
2662
+ },
2663
+ {
2664
+ name: 'Generic type with constraints',
2665
+ source: dedent`
2666
+ type NumberContainer<T extends number> = { values: T[]; }
2667
+ `,
2668
+ expected: dedent`
2669
+ type NumberContainer<T extends number> = Readonly<{ values: readonly T[]; }>;
2670
+ `,
2671
+ },
2672
+ {
2673
+ name: 'Generic type with default',
2674
+ source: dedent`
2675
+ type Container<T = string> = { items: T[]; }
2676
+ `,
2677
+ expected: dedent`
2678
+ type Container<T = string> = Readonly<{ items: readonly T[]; }>;
2679
+ `,
2680
+ },
2681
+ {
2682
+ name: 'Nested generic types',
2683
+ source: dedent`
2684
+ type NestedContainer<T> = { outer: Array<{ inner: T[] }>; }
2685
+ `,
2686
+ expected: dedent`
2687
+ type NestedContainer<T> = Readonly<{ outer: readonly Readonly<{ inner: readonly T[]; }>[]; }>;
2688
+ `,
2689
+ },
2690
+ {
2691
+ name: 'Generic type with union',
2692
+ source: dedent`
2693
+ type Result<T, E> = { data: T; } | { error: E; }
2694
+ `,
2695
+ expected: dedent`
2696
+ type Result<T, E> = Readonly<{ data: T; } | { error: E; }>
2697
+ `,
2698
+ },
2699
+ {
2700
+ name: 'Generic type with intersection',
2701
+ source: dedent`
2702
+ type WithId<T> = { id: string; } & T
2703
+ `,
2704
+ expected: dedent`
2705
+ type WithId<T> = Readonly<{ id: string; }> & T
2706
+ `,
2707
+ },
2708
+ {
2709
+ name: 'Generic DeepReadonly usage',
2710
+ source: dedent`
2711
+ type DeepReadonlyGeneric<T> = DeepReadonly<T>
2712
+ `,
2713
+ expected: dedent`
2714
+ type DeepReadonlyGeneric<T> = DeepReadonly<T>
2715
+ `,
2716
+ },
2717
+ {
2718
+ name: 'Complex generic with conditional types',
2719
+ source: dedent`
2720
+ type MaybeArray<T> = T extends any[] ? T : T[]
2721
+ `,
2722
+ expected: dedent`
2723
+ type MaybeArray<T> = T extends readonly any[] ? T : readonly T[];
2724
+ `,
2725
+ },
2726
+ ])('$name', testFn);
2727
+ });
2728
+
2729
+ describe('Canonical readonly forms are stable', () => {
2730
+ // Verify canonical forms are stable
2731
+ test.each([
2732
+ {
2733
+ name: 'readonly T[] is unchanged',
2734
+ source: dedent`
2735
+ type T = readonly string[]
2736
+ `,
2737
+ expected: dedent`
2738
+ type T = readonly string[]
2739
+ `,
2740
+ },
2741
+ {
2742
+ name: 'readonly [T1, T2] is unchanged',
2743
+ source: dedent`
2744
+ type T = readonly [string, number]
2745
+ `,
2746
+ expected: dedent`
2747
+ type T = readonly [string, number]
2748
+ `,
2749
+ },
2750
+ {
2751
+ name: 'readonly [T1, ...T2[]] is unchanged',
2752
+ source: dedent`
2753
+ type T = readonly [string, ...number[]]
2754
+ `,
2755
+ expected: dedent`
2756
+ type T = readonly [string, ...number[]]
2757
+ `,
2758
+ },
2759
+ {
2760
+ name: 'ReadonlySet<T> is unchanged',
2761
+ source: dedent`
2762
+ type T = ReadonlySet<number>
2763
+ `,
2764
+ expected: dedent`
2765
+ type T = ReadonlySet<number>
2766
+ `,
2767
+ },
2768
+ {
2769
+ name: 'ReadonlyMap<K, V> is unchanged',
2770
+ source: dedent`
2771
+ type T = ReadonlyMap<string, boolean>
2772
+ `,
2773
+ expected: dedent`
2774
+ type T = ReadonlyMap<string, boolean>
2775
+ `,
2776
+ },
2777
+ {
2778
+ name: 'Readonly<{ a: T }> is unchanged',
2779
+ source: dedent`
2780
+ type T = Readonly<{ a: string }>
2781
+ `,
2782
+ expected: dedent`
2783
+ type T = Readonly<{ a: string }>
2784
+ `,
2785
+ },
2786
+ {
2787
+ name: 'Interface with readonly member is unchanged // Interface itself should not be transformed',
2788
+ source: dedent`
2789
+ interface I { readonly prop: number; }
2790
+ `,
2791
+ expected: dedent`
2792
+ interface I { readonly prop: number; }
2793
+ `,
2794
+ },
2795
+ {
2796
+ name: 'Complex nested readonly is unchanged',
2797
+ source: dedent`
2798
+ type T = ReadonlyMap<string, readonly Readonly<{ p: readonly boolean[] }>[]>;
2799
+ `,
2800
+ expected: dedent`
2801
+ type T = ReadonlyMap<string, readonly Readonly<{ p: readonly boolean[] }>[]>;
2802
+ `,
2803
+ },
2804
+ ])('$name', testFn);
2805
+ });
2806
+
2807
+ describe('Other syntax elements', () => {
2808
+ test.each([
2809
+ {
2810
+ name: 'Typeof on array variable',
2811
+ source: dedent`
2812
+ const arr = [1, 2];
2813
+ type ArrType = typeof arr;
2814
+ `,
2815
+ expected: dedent`
2816
+ const arr = [1, 2];
2817
+ type ArrType = typeof arr;
2818
+ `,
2819
+ },
2820
+ {
2821
+ name: 'Typeof on object variable',
2822
+ source: dedent`
2823
+ const obj = { data: [1] };
2824
+ type ObjType = typeof obj;
2825
+ `,
2826
+ expected: dedent`
2827
+ const obj = { data: [1] };
2828
+ type ObjType = typeof obj;
2829
+ `,
2830
+ },
2831
+ {
2832
+ name: 'Indexed access type',
2833
+ source: dedent`
2834
+ type PropType<T, K extends keyof T> = T[K];
2835
+ type A = PropType<{ p: number[] }, "p">;
2836
+ `,
2837
+ expected: dedent`
2838
+ type PropType<T, K extends keyof T> = T[K];
2839
+ type A = PropType<Readonly<{ p: readonly number[] }>, "p">;
2840
+ `,
2841
+ },
2842
+ {
2843
+ name: 'Indexed access type with array/tuple',
2844
+ source: dedent`
2845
+ type ElementType<T extends any[]> = T[number]; type B = ElementType<string[][]>;
2846
+ `,
2847
+ expected: dedent`
2848
+ type ElementType<T extends readonly any[]> = T[number]; type B = ElementType<readonly (readonly string[])[]>;
2849
+ `,
2850
+ },
2851
+ {
2852
+ name: 'Conditional type',
2853
+ source: dedent`
2854
+ type Check<T> = T extends string[] ? { value: T } : { error: Error }; type C = Check<number[][]>;
2855
+ `,
2856
+ expected: dedent`
2857
+ type Check<T> = T extends readonly string[] ? Readonly<{ value: T }> : Readonly<{ error: Error }>; type C = Check<readonly (readonly number[])[]>;
2858
+ `,
2859
+ },
2860
+ {
2861
+ name: 'Conditional type with complex branches',
2862
+ source: dedent`
2863
+ type ComplexCondition<T> = T extends Map<string, number[]> ? Set<T>[] : Array<Map<string, T>>;
2864
+ `,
2865
+ expected: dedent`
2866
+ type ComplexCondition<T> = T extends ReadonlyMap<string, readonly number[]> ? readonly ReadonlySet<T>[] : readonly ReadonlyMap<string, T>[];
2867
+ `,
2868
+ },
2869
+ {
2870
+ name: 'Conditional type with infer',
2871
+ source: dedent`
2872
+ type InferArrayItem<T> = T extends (infer I)[] ? { item: I[] } : never; type D = InferArrayItem<Date[]>;
2873
+ `,
2874
+ expected: dedent`
2875
+ type InferArrayItem<T> = T extends readonly (infer I)[] ? Readonly<{ item: readonly I[] }> : never; type D = InferArrayItem<readonly Date[]>;
2876
+ `,
2877
+ },
2878
+ {
2879
+ name: 'Conditional type with infer in return position',
2880
+ source: dedent`
2881
+ type UnwrapArray<T> = T extends Array<infer U> ? U[] : T
2882
+ `,
2883
+
2884
+ expected: dedent`
2885
+ type UnwrapArray<T> = T extends readonly (infer U)[] ? readonly U[] : T;
2886
+ `,
2887
+ },
2888
+ {
2889
+ name: 'Conditional type with infer used in object',
2890
+ source: dedent`
2891
+ type InferToObject<T> = T extends Set<infer I> ? { item: I[] } : never;
2892
+ `,
2893
+ expected: dedent`
2894
+ type InferToObject<T> = T extends ReadonlySet<infer I> ? Readonly<{ item: readonly I[] }> : never;
2895
+ `,
2896
+ },
2897
+ {
2898
+ name: 'Type assertion <T>',
2899
+ source: dedent`
2900
+ let x = <Map<string, number[]>>{}
2901
+ `,
2902
+ expected: dedent`
2903
+ let x = <ReadonlyMap<string, readonly number[]>>{}
2904
+ `,
2905
+ },
2906
+ {
2907
+ name: 'Recursive type alias (linked list)',
2908
+ source: dedent`
2909
+ type List<T> = { value: T, next: List<T> | null };
2910
+ let list: List<number[]>;
2911
+ `,
2912
+ expected: dedent`
2913
+ type List<T> = Readonly<{ value: T, next: List<T> | null }>;
2914
+ let list: List<readonly number[]>;
2915
+ `,
2916
+ },
2917
+ {
2918
+ name: 'Recursive type alias (tree)',
2919
+ source: dedent`
2920
+ type Tree<T> = { value: T; children: Tree<T>[] }; let tree: Tree<{ id: number[] }>;
2921
+ `,
2922
+ expected: dedent`
2923
+ type Tree<T> = Readonly<{ value: T; children: readonly Tree<T>[] }>; let tree: Tree<Readonly<{ id: readonly number[] }>>;
2924
+ `,
2925
+ },
2926
+ {
2927
+ name: 'Code with comments near types',
2928
+ source: dedent`
2929
+ // Single line comment
2930
+ type /* Block Comment */ WithComments = {
2931
+ prop1: string[]; // Trailing comment
2932
+ /* Another block */
2933
+ prop2: /* member type comment */ Readonly< /* member type inner comment */ number[]>;
2934
+ /* End block */
2935
+ };
2936
+ `,
2937
+ expected: dedent`
2938
+ // Single line comment
2939
+ type /* Block Comment */ WithComments = Readonly<{
2940
+ prop1: readonly string[]; // Trailing comment
2941
+ /* Another block */
2942
+ prop2: /* member type comment */ readonly /* member type inner comment */ number[];
2943
+ /* End block */
2944
+ }>;
2945
+ `,
2946
+ },
2947
+
2948
+ {
2949
+ name: 'Class implements clause',
2950
+ source: dedent`
2951
+ interface IBox<T> { value: T[]; }
2952
+ class BoxImpl implements IBox<string[]> {
2953
+ value: string[];
2954
+ constructor() { this.value = []; }
2955
+ }
2956
+ `,
2957
+ expected: dedent`
2958
+ interface IBox<T> { readonly value: readonly T[]; }
2959
+ class BoxImpl implements IBox<readonly string[]> {
2960
+ readonly value: readonly string[];
2961
+ constructor() { this.value = []; }
2962
+ }
2963
+ `,
2964
+ },
2965
+ {
2966
+ name: 'Class extends with generics',
2967
+ source: dedent`
2968
+ class Base<T> { item: T[] }
2969
+ class Derived extends Base<string[]> {}
2970
+ `,
2971
+ expected: dedent`
2972
+ class Base<T> { readonly item: readonly T[] }
2973
+ class Derived extends Base<readonly string[]> {}
2974
+ `,
2975
+ },
2976
+ {
2977
+ name: 'Function type variable',
2978
+ source: dedent`
2979
+ let foo: () => number
2980
+ `,
2981
+ expected: dedent`
2982
+ let foo: () => number
2983
+ `,
2984
+ },
2985
+ {
2986
+ name: 'Destructuring assignment with tuple type',
2987
+ source: dedent`
2988
+ const [x, y]: [number[], number[]] = [[1, 2, 3], [4, 5, 6]]
2989
+ `,
2990
+ expected: dedent`
2991
+ const [x, y]: readonly [readonly number[], readonly number[]] = [[1, 2, 3], [4, 5, 6]];
2992
+ `,
2993
+ },
2994
+ {
2995
+ name: 'Namespace containing type alias',
2996
+ source: dedent`
2997
+ namespace X {
2998
+ type TypeAlias = {
2999
+ a: number[]
3000
+ };
3001
+ }
3002
+ `,
3003
+ expected: dedent`
3004
+ namespace X {
3005
+ type TypeAlias = Readonly<{
3006
+ a: readonly number[]
3007
+ }>;
3008
+ }
3009
+ `,
3010
+ },
3011
+ {
3012
+ name: 'Module containing type alias',
3013
+ source: dedent`
3014
+ module X {
3015
+ type TypeAlias = {
3016
+ a: number[]
3017
+ };
3018
+ }
3019
+ `,
3020
+ expected: dedent`
3021
+ module X {
3022
+ type TypeAlias = Readonly<{
3023
+ a: readonly number[]
3024
+ }>;
3025
+ }
3026
+ `,
3027
+ },
3028
+ ])('$name', testFn);
3029
+ });
3030
+
3031
+ describe('ignorePrefixes option', () => {
3032
+ test.each([
3033
+ {
3034
+ name: 'Variable declarations',
3035
+ source: dedent`
3036
+ const mut_foo: string[] = []
3037
+ `,
3038
+ expected: dedent`
3039
+ const mut_foo: string[] = []
3040
+ `,
3041
+ },
3042
+ {
3043
+ name: 'Function declarations',
3044
+ source: dedent`
3045
+ function mut_foo(a: readonly number[], b: Promise<number[]>) {}
3046
+ `,
3047
+ expected: dedent`
3048
+ function mut_foo(a: readonly number[], b: Promise<number[]>) {}
3049
+ `,
3050
+ },
3051
+ {
3052
+ name: 'Type aliases',
3053
+ source: dedent`
3054
+ type mut_Foo = number[]
3055
+ `,
3056
+ expected: dedent`
3057
+ type mut_Foo = number[]
3058
+ `,
3059
+ },
3060
+ {
3061
+ name: 'Ignore variable with prefix',
3062
+ source: dedent`
3063
+ type T = { mut_value: number[]; normalValue: string[] }
3064
+ `,
3065
+ expected: dedent`
3066
+ type T = Readonly<{
3067
+ mut_value: number[];
3068
+ normalValue: readonly string[];
3069
+ }>;
3070
+ `,
3071
+ options: { ignorePrefixes: ['mut_'] },
3072
+ },
3073
+ {
3074
+ name: 'Multiple ignore prefixes',
3075
+ source: dedent`
3076
+ type T = { mut_value: number[]; mutable_data: string[]; normalValue: boolean[] };
3077
+ `,
3078
+ expected: dedent`
3079
+ type T = Readonly<{
3080
+ mut_value: number[];
3081
+ mutable_data: string[];
3082
+ normalValue: readonly boolean[];
3083
+ }>;
3084
+ `,
3085
+ options: { ignorePrefixes: ['mut_', 'mutable_'] },
3086
+ },
3087
+ {
3088
+ name: 'Ignore prefix in nested properties',
3089
+ source: dedent`
3090
+ type T = {
3091
+ data: {
3092
+ mut_items: number[];
3093
+ items: string[];
3094
+ };
3095
+ };
3096
+ `,
3097
+ expected: dedent`
3098
+ type T = Readonly<{
3099
+ data: Readonly<{
3100
+ mut_items: number[];
3101
+ items: readonly string[];
3102
+ }>;
3103
+ }>;
3104
+ `,
3105
+ options: { ignorePrefixes: ['mut_'] },
3106
+ },
3107
+ {
3108
+ name: 'Ignore prefix in interface',
3109
+ source: dedent`
3110
+ interface User {
3111
+ mut_permissions: string[];
3112
+ roles: string[];
3113
+ }
3114
+ `,
3115
+ expected: dedent`
3116
+ interface User {
3117
+ mut_permissions: string[];
3118
+ readonly roles: readonly string[];
3119
+ }
3120
+ `,
3121
+ options: { ignorePrefixes: ['mut_'] },
3122
+ },
3123
+ {
3124
+ name: 'Ignore prefixes for various name node types in interface',
3125
+ source: dedent`
3126
+ interface I {
3127
+ mut_a: string[]; // Identifier
3128
+ #mut_x: string[]; // PrivateIdentifier
3129
+ "mut_b": string[]; // StringLiteral
3130
+ ['mut_c']: string[]; // ComputedPropertyName with StringLiteral
3131
+ ['mu' + 't_d']: string[]; // ComputedPropertyName
3132
+ }
3133
+ `,
3134
+ expected: dedent`
3135
+ interface I {
3136
+ mut_a: string[]; // Identifier
3137
+ #mut_x: string[]; // PrivateIdentifier
3138
+ "mut_b": string[]; // StringLiteral
3139
+ ['mut_c']: string[]; // ComputedPropertyName with StringLiteral
3140
+ readonly ['mu' + 't_d']: readonly string[]; // ComputedPropertyName
3141
+ }
3142
+ `,
3143
+ options: { ignorePrefixes: ['mut_'] },
3144
+ },
3145
+ {
3146
+ name: 'Ignore prefix with index signature',
3147
+ source: dedent`
3148
+ type T = {
3149
+ mut_items: {
3150
+ [key: string]: number[];
3151
+ };
3152
+ };
3153
+ `,
3154
+ expected: dedent`
3155
+ type T = Readonly<{
3156
+ mut_items: {
3157
+ [key: string]: number[];
3158
+ };
3159
+ }>;
3160
+ `,
3161
+ options: { ignorePrefixes: ['mut_'] },
3162
+ },
3163
+ {
3164
+ name: 'Index signature in ignored property',
3165
+ source: dedent`
3166
+ interface Store {
3167
+ mut_data: {
3168
+ [key: string]: string[];
3169
+ };
3170
+ }
3171
+ `,
3172
+ expected: dedent`
3173
+ interface Store {
3174
+ mut_data: {
3175
+ [key: string]: string[];
3176
+ };
3177
+ }
3178
+ `,
3179
+ options: { ignorePrefixes: ['mut_'] },
3180
+ },
3181
+ {
3182
+ name: 'Type alias with ignored prefix',
3183
+ source: dedent`
3184
+ type mut_Config = {
3185
+ items: string[];
3186
+ };
3187
+ `,
3188
+ expected: dedent`
3189
+ type mut_Config = {
3190
+ items: string[];
3191
+ };
3192
+ `,
3193
+ options: { ignorePrefixes: ['mut_'] },
3194
+ },
3195
+ {
3196
+ name: 'Function with ignored prefix',
3197
+ source: dedent`
3198
+ function mut_process(data: string[]): number[] { return []; }
3199
+ `,
3200
+ expected: dedent`
3201
+ function mut_process(data: string[]): number[] { return []; }
3202
+ `,
3203
+ options: { ignorePrefixes: ['mut_'] },
3204
+ },
3205
+ {
3206
+ name: 'Class with ignored prefix',
3207
+ source: dedent`
3208
+ class mut_Store {
3209
+ items: string[] = [];
3210
+ }
3211
+ `,
3212
+ expected: dedent`
3213
+ class mut_Store {
3214
+ items: string[] = [];
3215
+ }
3216
+ `,
3217
+ options: { ignorePrefixes: ['mut_'] },
3218
+ },
3219
+ {
3220
+ name: 'Variable declaration with ignored prefix',
3221
+ source: dedent`
3222
+ const mut_items: string[] = []
3223
+ `,
3224
+ expected: dedent`
3225
+ const mut_items: string[] = []
3226
+ `,
3227
+ options: { ignorePrefixes: ['mut_'] },
3228
+ },
3229
+ {
3230
+ name: 'Generic type with ignored prefix',
3231
+ source: dedent`
3232
+ type mut_List<T> = {
3233
+ items: T[]
3234
+ }
3235
+ `,
3236
+ expected: dedent`
3237
+ type mut_List<T> = {
3238
+ items: T[];
3239
+ };
3240
+ `,
3241
+ options: { ignorePrefixes: ['mut_'] },
3242
+ },
3243
+ {
3244
+ name: 'Union type with ignored prefix members',
3245
+ source: dedent`
3246
+ type Result = mut_Success | Error;
3247
+ type mut_Success = {
3248
+ data: string[]
3249
+ };
3250
+ `,
3251
+ expected: dedent`
3252
+ type Result = mut_Success | Error;
3253
+ type mut_Success = {
3254
+ data: string[];
3255
+ };
3256
+ `,
3257
+ options: { ignorePrefixes: ['mut_'] },
3258
+ },
3259
+ {
3260
+ name: 'Property with ignored prefix in class',
3261
+ source: dedent`
3262
+ class Store {
3263
+ mut_items: string[] = [];
3264
+ items: number[] = [];
3265
+ }
3266
+ `,
3267
+ expected: dedent`
3268
+ class Store {
3269
+ mut_items: string[] = [];
3270
+ readonly items: readonly number[] = [];
3271
+ }
3272
+ `,
3273
+ options: { ignorePrefixes: ['mut_'] },
3274
+ },
3275
+ {
3276
+ name: 'Method parameter with ignored prefix',
3277
+ source: dedent`
3278
+ class Store {
3279
+ process(mut_data: string[], data: number[]) {}
3280
+ }
3281
+ `,
3282
+ expected: dedent`
3283
+ class Store {
3284
+ process(mut_data: string[], data: readonly number[]) {}
3285
+ }
3286
+ `,
3287
+ options: { ignorePrefixes: ['mut_'] },
3288
+ },
3289
+ {
3290
+ name: 'Function expression with ignored prefix',
3291
+ source: dedent`
3292
+ const fn = function mut_process(data: string[]): number[] { return []; }
3293
+ `,
3294
+ expected: dedent`
3295
+ const fn = function mut_process(data: string[]): number[] { return []; }
3296
+ `,
3297
+ options: { ignorePrefixes: ['mut_'] },
3298
+ },
3299
+ {
3300
+ name: 'Array binding pattern in variable declaration (not implemented)',
3301
+ source: dedent`
3302
+ const [mut_first, mut_second]: [string[], number[]] = [[], []];
3303
+ `,
3304
+ expected: dedent`
3305
+ const [mut_first, mut_second]: readonly [readonly string[], readonly number[]] = [[], []];
3306
+ `,
3307
+ options: { ignorePrefixes: ['mut_'] },
3308
+ },
3309
+ {
3310
+ name: 'Object binding pattern in variable declaration (not implemented)',
3311
+ source: dedent`
3312
+ const { mut_arr, mut_obj }: { mut_arr: string[], mut_obj: number[] } = { mut_arr: [], mut_obj: [] };
3313
+ `,
3314
+ expected: dedent`
3315
+ const { mut_arr, mut_obj }: Readonly<{ mut_arr: string[], mut_obj: number[] }> = { mut_arr: [], mut_obj: [] };
3316
+ `,
3317
+ options: { ignorePrefixes: ['mut_'] },
3318
+ },
3319
+ {
3320
+ name: 'Parameter with ignored prefix in class constructor',
3321
+ source: dedent`
3322
+ class Example {
3323
+ constructor(mut_config: string[]) {}
3324
+ }
3325
+ `,
3326
+ expected: dedent`
3327
+ class Example {
3328
+ constructor(mut_config: string[]) {}
3329
+ }
3330
+ `,
3331
+ options: { ignorePrefixes: ['mut_'] },
3332
+ },
3333
+ {
3334
+ name: 'Parameter with ignored prefix in type literal method',
3335
+ source: dedent`
3336
+ type Handler = {
3337
+ process(mut_data: string[]): void;
3338
+ }
3339
+ `,
3340
+ expected: dedent`
3341
+ type Handler = Readonly<{
3342
+ process(mut_data: string[]): void;
3343
+ }>;
3344
+ `,
3345
+ options: { ignorePrefixes: ['mut_'] },
3346
+ },
3347
+ {
3348
+ name: 'Multiple parameters with ignored prefix',
3349
+ source: dedent`
3350
+ interface API {
3351
+ update(mut_id: string, mut_data: object[], data: string[]): void;
3352
+ }
3353
+ `,
3354
+ expected: dedent`
3355
+ interface API {
3356
+ update(mut_id: string, mut_data: object[], data: readonly string[]): void;
3357
+ }
3358
+ `,
3359
+ options: { ignorePrefixes: ['mut_'] },
3360
+ },
3361
+ {
3362
+ name: 'Arrow function with ignored prefix parameters',
3363
+ source: dedent`
3364
+ const handler = (mut_event: Event[], event: CustomEvent[]) => {};
3365
+ `,
3366
+ expected: dedent`
3367
+ const handler = (mut_event: Event[], event: readonly CustomEvent[]) => {};
3368
+ `,
3369
+ options: { ignorePrefixes: ['mut_'] },
3370
+ },
3371
+ ])('$name', testFn);
3372
+ });
3373
+
3374
+ describe('Disable comment handling', () => {
3375
+ test.each([
3376
+ {
3377
+ name: 'Skip Array type with disable comment',
3378
+ source: dedent`
3379
+ // transformer-ignore-next-line
3380
+ type Foo = number[]; // Should not change to readonly number[]
3381
+ `,
3382
+ expected: dedent`
3383
+ // transformer-ignore-next-line
3384
+ type Foo = number[]; // Should not change to readonly number[]
3385
+ `,
3386
+ },
3387
+ {
3388
+ name: 'Skip Generic Array type with disable comment',
3389
+ source: dedent`
3390
+ // transformer-ignore-next-line
3391
+ type Bar = Array<string>; // Should not change to readonly string[]
3392
+ `,
3393
+ expected: dedent`
3394
+ // transformer-ignore-next-line
3395
+ type Bar = Array<string>; // Should not change to readonly string[]
3396
+ `,
3397
+ },
3398
+ {
3399
+ name: 'Skip Tuple type with disable comment',
3400
+ source: dedent`
3401
+ // transformer-ignore-next-line
3402
+ type Baz = [number, string]; // Should not change to readonly [...]
3403
+ `,
3404
+ expected: dedent`
3405
+ // transformer-ignore-next-line
3406
+ type Baz = [number, string]; // Should not change to readonly [...]
3407
+ `,
3408
+ },
3409
+ {
3410
+ name: 'Skip Set type with disable comment',
3411
+ source: dedent`
3412
+ // transformer-ignore-next-line
3413
+ type Qux = Set<number>; // Should not change to ReadonlySet
3414
+ `,
3415
+ expected: dedent`
3416
+ // transformer-ignore-next-line
3417
+ type Qux = Set<number>; // Should not change to ReadonlySet
3418
+ `,
3419
+ },
3420
+ {
3421
+ name: 'Skip Map type with disable comment',
3422
+ source: dedent`
3423
+ // transformer-ignore-next-line
3424
+ type Quux = Map<string, boolean>; // Should not change to ReadonlyMap
3425
+ `,
3426
+ expected: dedent`
3427
+ // transformer-ignore-next-line
3428
+ type Quux = Map<string, boolean>; // Should not change to ReadonlyMap
3429
+ `,
3430
+ },
3431
+ {
3432
+ name: 'Skip Interface property with disable comment',
3433
+ source: dedent`
3434
+ interface MyInterface {
3435
+ // transformer-ignore-next-line
3436
+ prop: string[]; // This specific property should be skipped
3437
+ anotherProp: number[]; // This should still be transformed
3438
+ }
3439
+ `,
3440
+ expected: dedent`
3441
+ interface MyInterface {
3442
+ // transformer-ignore-next-line
3443
+ prop: string[]; // This specific property should be skipped
3444
+ readonly anotherProp: readonly number[]; // This should still be transformed
3445
+ }
3446
+ `,
3447
+ },
3448
+ {
3449
+ name: 'Skip whole Interface with disable comment',
3450
+ source: dedent`
3451
+ // transformer-ignore-next-line
3452
+ interface MyInterface {
3453
+ prop: string[]; // Should not change
3454
+ anotherProp: number[]; // Should not change
3455
+ }
3456
+ `,
3457
+ expected: dedent`
3458
+ // transformer-ignore-next-line
3459
+ interface MyInterface {
3460
+ prop: string[]; // Should not change
3461
+ anotherProp: number[]; // Should not change
3462
+ }
3463
+ `,
3464
+ },
3465
+ {
3466
+ name: 'Skip Class property with disable comment',
3467
+ source: dedent`
3468
+ class MyClass {
3469
+ // transformer-ignore-next-line
3470
+ prop: number[] = []; // This specific property should be skipped
3471
+ another: boolean[] = []; // This should still be transformed
3472
+ }
3473
+ `,
3474
+ expected: dedent`
3475
+ class MyClass {
3476
+ // transformer-ignore-next-line
3477
+ prop: number[] = []; // This specific property should be skipped
3478
+ readonly another: readonly boolean[] = []; // This should still be transformed
3479
+ }
3480
+ `,
3481
+ },
3482
+ {
3483
+ name: 'Skip whole Class with disable comment',
3484
+ source: dedent`
3485
+ // transformer-ignore-next-line
3486
+ class MyClass {
3487
+ prop: number[] = []; // Should not change
3488
+ another: boolean[] = []; // Should not change
3489
+ }
3490
+ `,
3491
+ expected: dedent`
3492
+ // transformer-ignore-next-line
3493
+ class MyClass {
3494
+ prop: number[] = []; // Should not change
3495
+ another: boolean[] = []; // Should not change
3496
+ }
3497
+ `,
3498
+ },
3499
+ {
3500
+ name: 'Skip Function parameter type with disable comment',
3501
+ source: dedent`
3502
+ function myFunc(
3503
+ a: string[], // Should be transformed
3504
+ // transformer-ignore-next-line
3505
+ b: Map<string, number>, // Should be skipped
3506
+ c: Set<boolean> // Should be transformed
3507
+ ) {}
3508
+ `,
3509
+ expected: dedent`
3510
+ function myFunc(
3511
+ a: readonly string[], // Should be transformed
3512
+ // transformer-ignore-next-line
3513
+ b: Map<string, number>, // Should be skipped
3514
+ c: ReadonlySet<boolean> // Should be transformed
3515
+ ) {}
3516
+ `,
3517
+ },
3518
+ {
3519
+ name: 'Skip whole Function declaration with disable comment',
3520
+ source: dedent`
3521
+ // transformer-ignore-next-line
3522
+ function myFunc(a: string[], b: Map<string, number>): Set<boolean>[] {} // Should not change
3523
+ `,
3524
+ expected: dedent`
3525
+ // transformer-ignore-next-line
3526
+ function myFunc(a: string[], b: Map<string, number>): Set<boolean>[] {} // Should not change
3527
+ `,
3528
+ },
3529
+ {
3530
+ name: 'Skip Function return type with disable comment',
3531
+ source: dedent`
3532
+ function myFunc(
3533
+ a: string[] // Param should be transformed
3534
+ ):
3535
+ // transformer-ignore-next-line
3536
+ Set<number>[] {} // Return type should be skipped
3537
+ `,
3538
+ expected: dedent`
3539
+ function myFunc(
3540
+ a: readonly string[] // Param should be transformed
3541
+ ):
3542
+ // transformer-ignore-next-line
3543
+ Set<number>[] {} // Return type should be skipped
3544
+ `,
3545
+ },
3546
+ {
3547
+ name: 'Skip Type literal with disable comment',
3548
+ source: dedent`
3549
+ // transformer-ignore-next-line
3550
+ type Ignored = { x: number[] }; // Should not change to Readonly<{...}>
3551
+ `,
3552
+ expected: dedent`
3553
+ // transformer-ignore-next-line
3554
+ type Ignored = { x: number[] }; // Should not change to Readonly<{...}>
3555
+ `,
3556
+ },
3557
+ {
3558
+ name: 'Disable comment only affects next line (mixed constructs)',
3559
+ source: dedent`
3560
+ type A = number[]; // Should be transformed
3561
+ // transformer-ignore-next-line
3562
+ type B = string[]; // Should be skipped
3563
+ type C = boolean[]; // Should be transformed
3564
+ `,
3565
+ expected: dedent`
3566
+ type A = readonly number[]; // Should be transformed
3567
+ // transformer-ignore-next-line
3568
+ type B = string[]; // Should be skipped
3569
+ type C = readonly boolean[]; // Should be transformed
3570
+ `,
3571
+ },
3572
+ {
3573
+ name: 'File scope transformer-ignore',
3574
+ source: dedent`
3575
+ /* transformer-ignore */
3576
+ type A = number[]; // Should be skipped
3577
+ type B = string[]; // Should be skipped
3578
+ type C = boolean[]; // Should be skipped
3579
+ `,
3580
+ expected: dedent`
3581
+ /* transformer-ignore */
3582
+ type A = number[]; // Should be skipped
3583
+ type B = string[]; // Should be skipped
3584
+ type C = boolean[]; // Should be skipped
3585
+ `,
3586
+ },
3587
+ ])('$name', testFn);
3588
+ });
3589
+
3590
+ describe('Error Cases', () => {
3591
+ test('Invalid DeepReadonlyTypeName', () => {
3592
+ expect(() => {
3593
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
3594
+ vi.spyOn(console, 'debug').mockImplementation(() => {});
3595
+
3596
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
3597
+ vi.spyOn(console, 'log').mockImplementation(() => {});
3598
+
3599
+ const source = dedent`
3600
+ type T = number[];
3601
+ `;
3602
+
3603
+ transformSourceCode(source, false, [
3604
+ convertToReadonlyTypeTransformer({
3605
+ DeepReadonly: {
3606
+ typeName: 'Readonly',
3607
+ },
3608
+ }),
3609
+ ]);
3610
+ }).toThrowError(
3611
+ 'Invalid DeepReadonly typeName "Readonly" passed to convertToReadonlyType',
3612
+ );
3613
+ });
3614
+
3615
+ test('Unexpected number of type arguments for Array', () => {
3616
+ expect(() => {
3617
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
3618
+ vi.spyOn(console, 'debug').mockImplementation(() => {});
3619
+
3620
+ // eslint-disable-next-line vitest/no-restricted-vi-methods
3621
+ vi.spyOn(console, 'log').mockImplementation(() => {});
3622
+
3623
+ const source = dedent`
3624
+ type T = Array<number, string>;
3625
+ `; // Invalid Array usage
3626
+
3627
+ transformSourceCode(source, false, [
3628
+ convertToReadonlyTypeTransformer(),
3629
+ ]);
3630
+ }).toThrowError('Unexpected number of type arguments');
3631
+ });
3632
+ });
3633
+ });
3634
+
3635
+ const toUnionAndIntersectionTestCase = ({
3636
+ title,
3637
+ testCase,
3638
+ }: Readonly<{
3639
+ title: (op: 'Union' | 'Intersection') => string;
3640
+ testCase: (op: '|' | '&') => Readonly<{
3641
+ source: string;
3642
+ expected: string;
3643
+ debug?: boolean;
3644
+ }>;
3645
+ }>): readonly Readonly<{
3646
+ name: string;
3647
+ source: string;
3648
+ expected: string;
3649
+ }>[] =>
3650
+ (['|', '&'] as const).map((op) => ({
3651
+ name: title(op === '&' ? 'Intersection' : 'Union'),
3652
+ ...testCase(op),
3653
+ }));