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,1391 @@
1
+ import {
2
+ Arr,
3
+ expectType,
4
+ ISet,
5
+ isString,
6
+ mapNullable,
7
+ match,
8
+ pipe,
9
+ } from 'ts-data-forge';
10
+ import * as tsm from 'ts-morph';
11
+ import {
12
+ hasDisableNextLineComment,
13
+ isPrimitiveTypeNode,
14
+ isReadonlyArrayTypeNode,
15
+ isReadonlyTupleOrArrayTypeNode,
16
+ isReadonlyTupleTypeNode,
17
+ isReadonlyTypeReferenceNode,
18
+ removeParentheses,
19
+ unwrapReadonlyTypeArgText,
20
+ wrapWithParentheses,
21
+ } from '../functions/index.mjs';
22
+ import { replaceNodeWithDebugPrint } from '../utils/index.mjs';
23
+ import {
24
+ groupUnionIntersectionTypes,
25
+ invalidDeepReadonlyTypeName,
26
+ nextReadonlyContext,
27
+ type ReadonlyContext,
28
+ } from './readonly-transformer-helpers/index.mjs';
29
+ import { type TsMorphTransformer } from './types.mjs';
30
+
31
+ export const convertToReadonlyTypeTransformer =
32
+ (options?: ReadonlyTransformerOptions): TsMorphTransformer =>
33
+ (sourceAst) => {
34
+ if (
35
+ options?.DeepReadonly?.typeName !== undefined &&
36
+ invalidDeepReadonlyTypeName.has(options.DeepReadonly.typeName)
37
+ ) {
38
+ throw new Error(
39
+ `Invalid DeepReadonly typeName "${options.DeepReadonly.typeName}" passed to convertToReadonlyType`,
40
+ );
41
+ }
42
+
43
+ const DeepReadonlyTypeName =
44
+ options?.DeepReadonly?.typeName ?? 'DeepReadonly';
45
+
46
+ const ignorePrefixes = ISet.create(options?.ignorePrefixes ?? ['mut_']);
47
+
48
+ const optionsInternal: ReadonlyTransformerOptionsInternal = {
49
+ DeepReadonly: {
50
+ typeName: DeepReadonlyTypeName,
51
+ applyLevel: 'keep',
52
+ },
53
+ ignoreEmptyObjectTypes: options?.ignoreEmptyObjectTypes ?? true,
54
+ ignoredPrefixes: ignorePrefixes,
55
+ debugPrint: options?.debug === true ? console.debug : () => {},
56
+ replaceNode:
57
+ options?.debug === true
58
+ ? replaceNodeWithDebugPrint
59
+ : (node, newNodeText) => node.replaceWithText(newNodeText),
60
+ };
61
+
62
+ for (const node of sourceAst.getChildren()) {
63
+ transformNode(node, initialReadonlyContext, optionsInternal);
64
+ }
65
+ };
66
+
67
+ export type ReadonlyTransformerOptions = DeepReadonly<{
68
+ /**
69
+ * Options for a type utility `DeepReadonly` that recursively applies readonly.
70
+ */
71
+ DeepReadonly?: {
72
+ /**
73
+ * The name of a type utility that recursively applies `Readonly`.
74
+ *
75
+ * @default "DeepReadonly"
76
+ */
77
+ typeName?: string;
78
+
79
+ /**
80
+ * Whether to apply `DeepReadonly` aggressively or remove `DeepReadonly` as
81
+ * possible.
82
+ *
83
+ * If `applyLevel` is `"applyAggressively"`, it applies `DeepReadonly` to all
84
+ * nested type literals without external type references. For example, the
85
+ * following conversions are applied:
86
+ *
87
+ * - `number[][]` to be `DeepReadonly<number[][]>`
88
+ * - `number[]` to be `readonly number[]`
89
+ * - `<T>(arg: T[][]) => void` to be `<T>(arg: readonly (readonly T[])[]>) =>
90
+ * void`
91
+ *
92
+ * If `applyLevel` is `"removeAsPossible"`, the following conversions are applied:
93
+ *
94
+ * - `DeepReadonly<number[][]>` to be `readonly (readonly number[])[]`
95
+ * - `DeepReadonly<{ a: number[] }>` to be `Readonly<{ a: readonly number[]
96
+ * }>`
97
+ * - `<T>(arg: DeepReadonly<T[][]>) => void` to be `<T>(arg:
98
+ * DeepReadonly<T[][]>) => void`
99
+ *
100
+ * @default 'keep'
101
+ */
102
+ // TODO
103
+ // applyLevel?: 'applyAggressively' | 'keep' | 'removeAsPossible';
104
+ };
105
+
106
+ /**
107
+ * Ignore the readonly conversion for `{}`.
108
+ *
109
+ * @default true
110
+ */
111
+ ignoreEmptyObjectTypes?: boolean;
112
+
113
+ /**
114
+ * A mute keywords to ignore the readonly conversion.
115
+ *
116
+ * (e.g. `"mut_"`)
117
+ */
118
+ ignorePrefixes?: string[];
119
+
120
+ debug?: boolean;
121
+ }>;
122
+
123
+ type ReadonlyTransformerOptionsInternal = Readonly<{
124
+ DeepReadonly: Readonly<{
125
+ typeName: string;
126
+
127
+ // removeAsPossible: 正規化後、 DeepReadonly の子ノードが typeLiteral かまたはその union or intersection であり、 それぞれの TypeLiteral がプリミティブ値のメンバーしか持たない場合。
128
+ applyLevel: 'applyAggressively' | 'keep' | 'removeAsPossible';
129
+ }>;
130
+
131
+ ignoreEmptyObjectTypes: boolean;
132
+
133
+ ignoredPrefixes: ISet<string>;
134
+
135
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types -- ts-morph uses mutable types
136
+ debugPrint: (...args: unknown[]) => void;
137
+ replaceNode: (node: tsm.Node, newNodeText: string) => void;
138
+ }>;
139
+
140
+ const initialReadonlyContext: ReadonlyContext = {
141
+ type: 'none',
142
+ indexedAccessDepth: 0,
143
+ } as const;
144
+
145
+ const transformNode = (
146
+ node: tsm.Node,
147
+ readonlyContext: ReadonlyContext,
148
+ options: ReadonlyTransformerOptionsInternal,
149
+ ): void => {
150
+ if (
151
+ tsm.Node.isTypeNode(node) ||
152
+ node.isKind(tsm.SyntaxKind.InterfaceDeclaration) ||
153
+ node.isKind(tsm.SyntaxKind.ClassDeclaration)
154
+ ) {
155
+ options.debugPrint();
156
+
157
+ options.debugPrint(`transformNode [${node.getKindName()}] `);
158
+
159
+ options.debugPrint(node.getFullText());
160
+
161
+ options.debugPrint({ readonlyContext });
162
+
163
+ options.debugPrint();
164
+ }
165
+
166
+ if (hasDisableNextLineComment(node)) {
167
+ options.debugPrint('skipped by disable-next-line comment');
168
+
169
+ return;
170
+ }
171
+
172
+ // check for ignorePrefix
173
+ if (node.isKind(tsm.SyntaxKind.VariableDeclaration)) {
174
+ const nodeName = node.getName();
175
+
176
+ if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
177
+ // Skip conversion for variable declarations with ignored prefixes
178
+ // Example: const mut_foo: string[] = []; -> remains as is, without appending `as const`
179
+ options.debugPrint('skipped variable declaration by ignorePrefixes');
180
+
181
+ return;
182
+ }
183
+
184
+ // TODO: Support ignoredPrefixes in ArrayBindingPattern
185
+ // if (ts.isArrayBindingPattern(nodeName)) {
186
+ // // for (const [i, el] of nodeName.elements.entries())
187
+ // }
188
+
189
+ // TODO: Support ignoredPrefixes in ObjectBindingPattern
190
+ // if (ts.isObjectBindingPattern(nodeName)) {
191
+ // // for (const [i, el] of nodeName.elements.entries())
192
+ // }
193
+ }
194
+
195
+ // Skip readonly conversion for function declarations with ignored prefixes
196
+ // Example: function mut_foo() {} -> parameters and return types remain as is
197
+ if (
198
+ (node.isKind(tsm.SyntaxKind.FunctionDeclaration) ||
199
+ node.isKind(tsm.SyntaxKind.FunctionExpression)) &&
200
+ options.ignoredPrefixes.some((p) => node.getName()?.startsWith(p) === true)
201
+ ) {
202
+ return;
203
+ }
204
+
205
+ if (node.isKind(tsm.SyntaxKind.Parameter)) {
206
+ const nodeName = node.getName();
207
+
208
+ if (options.ignoredPrefixes.some((p) => nodeName.startsWith(p))) {
209
+ // Skip readonly conversion for variable declarations with ignored prefixes
210
+ // Example: const mut_foo: string[] = []; -> remains as is, without readonly conversion
211
+ return;
212
+ }
213
+
214
+ // TODO: Support ignoredPrefixes in ArrayBindingPattern
215
+ // if (ts.isArrayBindingPattern(nodeName)) {
216
+ // }
217
+
218
+ // TODO: Support ignoredPrefixes in ObjectBindingPattern
219
+ // if (ts.isObjectBindingPattern(nodeName)) {
220
+ // }
221
+ }
222
+
223
+ // Skip readonly conversion for type alias declarations with ignored prefixes
224
+ // Example: type mut_Foo = number[]; -> remains as number[] without readonly
225
+ if (
226
+ node.isKind(tsm.SyntaxKind.TypeAliasDeclaration) &&
227
+ options.ignoredPrefixes.some((p) => node.getName().startsWith(p))
228
+ ) {
229
+ return;
230
+ }
231
+
232
+ if (node.isKind(tsm.SyntaxKind.TypeReference)) {
233
+ if (readonlyContext.type === 'readonly') {
234
+ throw new Error(
235
+ 'Invalid readonlyContext "readonly" passed to TypeReferenceNode',
236
+ );
237
+ }
238
+
239
+ transformTypeReferenceNode(node, readonlyContext, options);
240
+
241
+ return;
242
+ }
243
+
244
+ if (node.isKind(tsm.SyntaxKind.ArrayType)) {
245
+ transformArrayTypeNode(node, readonlyContext, options);
246
+
247
+ return;
248
+ }
249
+
250
+ if (node.isKind(tsm.SyntaxKind.TupleType)) {
251
+ transformTupleTypeNode(node, readonlyContext, options);
252
+
253
+ return;
254
+ }
255
+
256
+ if (node.isKind(tsm.SyntaxKind.RestType)) {
257
+ transformRestTypeNode(node, readonlyContext, options);
258
+
259
+ return;
260
+ }
261
+
262
+ if (node.isKind(tsm.SyntaxKind.TypeOperator)) {
263
+ if (readonlyContext.type === 'readonly') {
264
+ throw new Error(
265
+ 'Invalid readonlyContext "readonly" passed to TypeOperatorNode',
266
+ );
267
+ }
268
+
269
+ transformTypeOperatorNode(node, readonlyContext, options);
270
+
271
+ return;
272
+ }
273
+
274
+ if (node.isKind(tsm.SyntaxKind.TypeLiteral)) {
275
+ if (readonlyContext.type === 'readonly') {
276
+ throw new Error(
277
+ 'Invalid readonlyContext "readonly" passed to TypeLiteralNode',
278
+ );
279
+ }
280
+
281
+ transformTypeLiteralNode(node, readonlyContext, options);
282
+
283
+ return;
284
+ }
285
+
286
+ if (node.isKind(tsm.SyntaxKind.IndexedAccessType)) {
287
+ transformIndexedAccessTypeNode(node, readonlyContext, options);
288
+
289
+ return;
290
+ }
291
+
292
+ if (node.isKind(tsm.SyntaxKind.MappedType)) {
293
+ if (readonlyContext.type === 'readonly') {
294
+ throw new Error(
295
+ 'Invalid readonlyContext "readonly" passed to MappedTypeNode',
296
+ );
297
+ }
298
+
299
+ transformMappedTypeNode(node, readonlyContext, options);
300
+
301
+ return;
302
+ }
303
+
304
+ if (node.isKind(tsm.SyntaxKind.InterfaceDeclaration)) {
305
+ // Skip readonly conversion for interface declarations with ignored prefixes
306
+ // Example: interface mut_Interface {...} -> properties remain without readonly
307
+ if (options.ignoredPrefixes.some((p) => node.getName().startsWith(p))) {
308
+ return;
309
+ }
310
+
311
+ transformInterfaceDeclarationNode(node, options);
312
+
313
+ return;
314
+ }
315
+
316
+ if (node.isKind(tsm.SyntaxKind.ClassDeclaration)) {
317
+ // Skip readonly conversion for class declarations with ignored prefixes
318
+ // Example: class mut_Class {...} -> properties remain without readonly
319
+ if (
320
+ options.ignoredPrefixes.some(
321
+ (p) => node.getName()?.startsWith(p) === true,
322
+ )
323
+ ) {
324
+ return;
325
+ }
326
+
327
+ transformClassDeclarationNode(node, options);
328
+
329
+ return;
330
+ }
331
+
332
+ if (node.isKind(tsm.SyntaxKind.IntersectionType)) {
333
+ if (readonlyContext.type === 'readonly') {
334
+ throw new Error(
335
+ 'Invalid readonlyContext "readonly" passed to IntersectionTypeNode',
336
+ );
337
+ }
338
+
339
+ transformIntersectionTypeNode(node, readonlyContext, options);
340
+
341
+ return;
342
+ }
343
+
344
+ if (node.isKind(tsm.SyntaxKind.UnionType)) {
345
+ if (readonlyContext.type === 'readonly') {
346
+ throw new Error(
347
+ 'Invalid readonlyContext "readonly" passed to UnionTypeNode',
348
+ );
349
+ }
350
+
351
+ transformUnionTypeNode(node, readonlyContext, options);
352
+
353
+ return;
354
+ }
355
+
356
+ if (node.isKind(tsm.SyntaxKind.ParenthesizedType)) {
357
+ // NOTE: readonlyContext を不変のままバケツリレーさせるためにこのケースも必要
358
+ transformParenthesizedTypeNode(node, readonlyContext, options);
359
+
360
+ return;
361
+ }
362
+
363
+ for (const child of node.getChildren()) {
364
+ transformNode(child, initialReadonlyContext, options);
365
+ }
366
+ };
367
+
368
+ //
369
+ // Transformer implementation for each node type
370
+ //
371
+
372
+ /**
373
+ * - `tr(ReadonlyArray<E>) |-> readonly tr(E)[]`
374
+ * - `tr(Readonly<Readonly<E>>) |-> Readonly<tr(E)>`
375
+ * - `tr(DeepReadonly<Readonly<E>>) |-> DeepReadonly<tr(E)>`
376
+ * - `tr(Readonly<number>) |-> number`
377
+ * - `tr(Readonly<E[]>) |-> readonly tr(E)[]`
378
+ * - `tr(Readonly<[E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
379
+ * - `tr(Readonly<readonly E[]>) |-> readonly tr(E)[]`
380
+ * - `tr(Readonly<readonly [E1, E2, E3]>) |-> readonly [tr(E1), tr(E2), tr(E3)]`
381
+ * - `tr(Readonly<A | readonly E[]>) |-> Readonly<tr(A)> | readonly tr(E)[]>`
382
+ */
383
+ const transformTypeReferenceNode = (
384
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
385
+ node: tsm.TypeReferenceNode,
386
+ readonlyContext: Exclude<
387
+ ReadonlyContext,
388
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
389
+ >,
390
+ options: ReadonlyTransformerOptionsInternal,
391
+ ): void => {
392
+ const typeNameStr = node.getTypeName().getText();
393
+
394
+ const typeArguments = node.getTypeArguments();
395
+
396
+ // Array<T> / ReadonlyArray<T> to readonly T[]
397
+ if (typeNameStr === 'Array' || typeNameStr === 'ReadonlyArray') {
398
+ if (!Arr.isArrayOfLength(typeArguments, 1)) {
399
+ throw new Error(
400
+ `Unexpected number of type arguments "${typeArguments.length}" for ${typeNameStr}.`,
401
+ );
402
+ }
403
+
404
+ // Recursive processing
405
+ transformNode(
406
+ typeArguments[0],
407
+ {
408
+ type: 'none',
409
+ indexedAccessDepth: readonlyContext.indexedAccessDepth,
410
+ },
411
+ options,
412
+ );
413
+
414
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415
+ const T = node.getTypeArguments()[0]!;
416
+
417
+ options.replaceNode(
418
+ node,
419
+ readonlyContext.type === 'DeepReadonly'
420
+ ? wrapWithParentheses(`${wrapWithParentheses(T.getFullText())}[]`)
421
+ : wrapWithParentheses(
422
+ `readonly ${wrapWithParentheses(T.getFullText())}[]`,
423
+ ),
424
+ );
425
+
426
+ return;
427
+ }
428
+
429
+ // Set<T> to ReadonlySet<T>
430
+ if (typeNameStr === 'Set') {
431
+ if (!Arr.isArrayOfLength(typeArguments, 1)) {
432
+ throw new Error(
433
+ `Unexpected number of type arguments "${typeArguments.length}" for Set.`,
434
+ );
435
+ }
436
+
437
+ // Recursive processing
438
+ transformNode(
439
+ typeArguments[0],
440
+ {
441
+ type: 'none',
442
+ indexedAccessDepth: readonlyContext.indexedAccessDepth,
443
+ },
444
+ options,
445
+ );
446
+
447
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
448
+ const T = node.getTypeArguments()[0]!;
449
+
450
+ options.replaceNode(node, `ReadonlySet<${T.getFullText()}>`);
451
+
452
+ return;
453
+ }
454
+
455
+ // Map<T> to ReadonlyMap<T>
456
+ if (typeNameStr === 'Map') {
457
+ if (!Arr.isArrayOfLength(typeArguments, 2)) {
458
+ throw new Error(
459
+ `Unexpected number of type arguments "${typeArguments.length}" for Map.`,
460
+ );
461
+ }
462
+
463
+ const nextReadonlyContextValue = nextReadonlyContext({
464
+ currentReadonlyContext: readonlyContext,
465
+ nextReadonlyContextType: 'none',
466
+ indexedAccessDepthChange: 'keep',
467
+ });
468
+
469
+ // Recursive processing
470
+ for (const t of typeArguments) {
471
+ transformNode(t, nextReadonlyContextValue, options);
472
+ }
473
+
474
+ const [K, V] = node.getTypeArguments();
475
+
476
+ options.replaceNode(
477
+ node,
478
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
479
+ `ReadonlyMap<${K!.getFullText()}, ${V!.getFullText()}>`,
480
+ );
481
+
482
+ return;
483
+ }
484
+
485
+ // remove unnecessary `Readonly` wrapper or convert to readonly operator
486
+ if (typeNameStr === 'Readonly') {
487
+ if (!Arr.isArrayOfLength(typeArguments, 1)) {
488
+ throw new Error(
489
+ `Unexpected number of type arguments "${typeArguments.length}" for Readonly.`,
490
+ );
491
+ }
492
+
493
+ // Recursive processing
494
+ transformNode(
495
+ typeArguments[0],
496
+ nextReadonlyContext({
497
+ currentReadonlyContext: readonlyContext,
498
+ nextReadonlyContextType: 'Readonly',
499
+ indexedAccessDepthChange: 'keep',
500
+ }),
501
+ options,
502
+ );
503
+
504
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
505
+ const T = removeParentheses(node.getTypeArguments()[0]!);
506
+
507
+ // DeepReadonly<Readonly<T>> -> DeepReadonly<T>
508
+ if (readonlyContext.type === 'DeepReadonly') {
509
+ options.replaceNode(node, T.getFullText());
510
+
511
+ return;
512
+ }
513
+
514
+ // Readonly<{ ... }>[I] -> { ... }[I]
515
+ if (readonlyContext.indexedAccessDepth > 0) {
516
+ options.replaceNode(node, T.getFullText());
517
+
518
+ return;
519
+ }
520
+
521
+ // Readonly<Readonly<T>> -> Readonly<T>
522
+ if (isReadonlyTypeReferenceNode(T)) {
523
+ options.replaceNode(node, T.getFullText());
524
+
525
+ return;
526
+ }
527
+
528
+ // Readonly<number> -> number
529
+ if (isPrimitiveTypeNode(T)) {
530
+ options.replaceNode(node, T.getFullText());
531
+
532
+ return;
533
+ }
534
+
535
+ // T = E[]
536
+ // Readonly<E[]> -> readonly E[]
537
+ //
538
+ // T = [E1, E2, E3]
539
+ // Readonly<[E1, E2, E3]> -> readonly [E1, E2, E3]
540
+ if (
541
+ T.isKind(tsm.SyntaxKind.ArrayType) ||
542
+ T.isKind(tsm.SyntaxKind.TupleType)
543
+ ) {
544
+ options.replaceNode(
545
+ node,
546
+ wrapWithParentheses(`readonly ${T.getFullText()}`),
547
+ );
548
+
549
+ return;
550
+ }
551
+
552
+ // T = readonly E[] or readonly [E1, E2, E3]
553
+ // Readonly<readonly E[]> -> readonly E[]
554
+ // Readonly<readonly [E1, E2, E3]> -> readonly [E1, E2, E3]
555
+ if (isReadonlyTupleOrArrayTypeNode(T)) {
556
+ options.replaceNode(node, T.getFullText());
557
+
558
+ return;
559
+ }
560
+
561
+ // T = A | B | C
562
+ // T = A & B & C
563
+ if (
564
+ T.isKind(tsm.SyntaxKind.UnionType) ||
565
+ T.isKind(tsm.SyntaxKind.IntersectionType)
566
+ ) {
567
+ options.replaceNode(node, T.getFullText());
568
+
569
+ return;
570
+ }
571
+
572
+ return;
573
+ }
574
+
575
+ // DeepReadonly
576
+ if (typeNameStr === options.DeepReadonly.typeName) {
577
+ if (!Arr.isArrayOfLength(typeArguments, 1)) {
578
+ throw new Error(
579
+ `Unexpected number of type arguments "${typeArguments.length}" for Readonly.`,
580
+ );
581
+ }
582
+
583
+ // Recursive processing
584
+ transformNode(
585
+ typeArguments[0],
586
+ nextReadonlyContext({
587
+ currentReadonlyContext: readonlyContext,
588
+ nextReadonlyContextType: 'DeepReadonly',
589
+ }),
590
+ options,
591
+ );
592
+
593
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
594
+ const T = node.getTypeArguments()[0]!;
595
+
596
+ // DeepReadonly<DeepReadonly<T>> -> DeepReadonly<T>
597
+ if (readonlyContext.type === 'DeepReadonly') {
598
+ options.replaceNode(node, T.getFullText());
599
+
600
+ return;
601
+ }
602
+
603
+ // Readonly<number> -> number
604
+ if (isPrimitiveTypeNode(T)) {
605
+ options.replaceNode(node, T.getFullText());
606
+
607
+ return;
608
+ }
609
+
610
+ // T = P[]
611
+ // DeepReadonly<P[]> -> readonly P[] (for primitive type arrays, convert to readonly P[] form)
612
+ // DeepReadonly<O[]> -> DeepReadonly<O[]> (for object type arrays, keep as is to recursively apply Readonly)
613
+ // DeepReadonly<P[][]> -> DeepReadonly<P[][]>
614
+ if (
615
+ T.isKind(tsm.SyntaxKind.ArrayType) &&
616
+ isPrimitiveTypeNode(T.getElementTypeNode())
617
+ ) {
618
+ options.replaceNode(
619
+ node,
620
+ wrapWithParentheses(`readonly ${T.getFullText()}`),
621
+ );
622
+
623
+ return;
624
+ }
625
+
626
+ // T = [P1, P2, P3]
627
+ // DeepReadonly<[P1, P2, P3]> -> readonly [P1, P2, P3]
628
+ if (
629
+ T.isKind(tsm.SyntaxKind.TupleType) &&
630
+ T.getElements().every(isPrimitiveTypeNode)
631
+ ) {
632
+ options.replaceNode(
633
+ node,
634
+ wrapWithParentheses(`readonly ${T.getFullText()}`),
635
+ );
636
+
637
+ return;
638
+ }
639
+
640
+ return;
641
+ }
642
+
643
+ {
644
+ const nextReadonlyContextValue = nextReadonlyContext({
645
+ currentReadonlyContext: readonlyContext,
646
+ nextReadonlyContextType: 'none',
647
+ indexedAccessDepthChange: 'keep',
648
+ });
649
+
650
+ // Recursive processing
651
+ for (const t of node.getTypeArguments()) {
652
+ transformNode(t, nextReadonlyContextValue, options);
653
+ }
654
+ }
655
+ };
656
+
657
+ /** `tr(E[]) |-> tr(E)[]` */
658
+ const transformArrayTypeNode = (
659
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
660
+ node: tsm.ArrayTypeNode,
661
+ readonlyContext: ReadonlyContext,
662
+ options: ReadonlyTransformerOptionsInternal,
663
+ ): void => {
664
+ // Recursive processing
665
+ transformNode(
666
+ node.getElementTypeNode(),
667
+ nextReadonlyContext({
668
+ currentReadonlyContext: readonlyContext,
669
+ nextReadonlyContextType: 'none',
670
+ indexedAccessDepthChange: 'decr',
671
+ }),
672
+ options,
673
+ );
674
+
675
+ switch (readonlyContext.type) {
676
+ case 'DeepReadonly':
677
+ case 'readonly':
678
+ return; // remain tr(E)[] as is
679
+
680
+ // Unnecessary `Readonly` wrapper will be remove in transformTypeReferenceNode
681
+ case 'Readonly':
682
+ case 'none':
683
+ if (readonlyContext.indexedAccessDepth === 0) {
684
+ options.replaceNode(
685
+ node,
686
+ wrapWithParentheses(`readonly ${node.getFullText()}`),
687
+ );
688
+ }
689
+ }
690
+ };
691
+
692
+ /** `tr([E1, E2, E3])` |-> `[tr(E1), tr(E2), tr(E3)]` */
693
+ const transformTupleTypeNode = (
694
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
695
+ node: tsm.TupleTypeNode,
696
+ readonlyContext: ReadonlyContext,
697
+ options: ReadonlyTransformerOptionsInternal,
698
+ ): void => {
699
+ // Recursive processing
700
+ {
701
+ const nextReadonlyContextValue = nextReadonlyContext({
702
+ currentReadonlyContext: readonlyContext,
703
+ nextReadonlyContextType: 'none',
704
+ indexedAccessDepthChange: 'decr',
705
+ });
706
+
707
+ for (const el of node.getElements()) {
708
+ if (el.isKind(tsm.SyntaxKind.NamedTupleMember)) {
709
+ if (options.ignoredPrefixes.every((p) => !el.getName().startsWith(p))) {
710
+ transformNode(el.getTypeNode(), nextReadonlyContextValue, options);
711
+ }
712
+ } else {
713
+ transformNode(el, nextReadonlyContextValue, options);
714
+ }
715
+ }
716
+ }
717
+
718
+ switch (readonlyContext.type) {
719
+ case 'DeepReadonly':
720
+ case 'readonly':
721
+ return; // remain tr(E)[] as is
722
+
723
+ // Unnecessary `Readonly` wrapper will be remove in transformTypeReferenceNode
724
+ case 'Readonly':
725
+ case 'none':
726
+ if (readonlyContext.indexedAccessDepth === 0) {
727
+ options.replaceNode(
728
+ node,
729
+ wrapWithParentheses(`readonly ${node.getFullText()}`),
730
+ );
731
+ }
732
+ }
733
+ };
734
+
735
+ /** `tr("...T")` |-> `...tr(T)` */
736
+ const transformRestTypeNode = (
737
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
738
+ node: tsm.RestTypeNode,
739
+ readonlyContext: ReadonlyContext,
740
+ options: ReadonlyTransformerOptionsInternal,
741
+ ): void => {
742
+ // Recursive processing
743
+ transformNode(
744
+ node.getTypeNode() /* = T */,
745
+ nextReadonlyContext({
746
+ currentReadonlyContext: readonlyContext,
747
+ nextReadonlyContextType: 'none',
748
+ indexedAccessDepthChange: 'decr',
749
+ }),
750
+ options,
751
+ );
752
+
753
+ const R = removeParentheses(node.getTypeNode());
754
+
755
+ // `tr("...readonly E[]") |-> ...tr(E)[]`
756
+ // `tr("...readonly [E1, E2]") |-> ...[tr(E1), tr(E2)]`
757
+ if (isReadonlyArrayTypeNode(R) || isReadonlyTupleTypeNode(R)) {
758
+ options.replaceNode(
759
+ node,
760
+ `...${R.getTypeNode().getFullText()}` /* = tr(E)[] or [tr(E1), tr(E2)] */,
761
+ );
762
+ }
763
+ };
764
+
765
+ /**
766
+ * - `readonly T[][] |-> readonly (readonly T[])[]`
767
+ * - `keyof { a: number[] } |-> keyof Readonly<{ a: readonly number[] }>`
768
+ */
769
+ const transformTypeOperatorNode = (
770
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
771
+ node: tsm.TypeOperatorTypeNode,
772
+ readonlyContext: Exclude<
773
+ ReadonlyContext,
774
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
775
+ >,
776
+ options: ReadonlyTransformerOptionsInternal,
777
+ ): void => {
778
+ // Recursive processing
779
+ transformNode(
780
+ node.getTypeNode(),
781
+ nextReadonlyContext({
782
+ currentReadonlyContext: readonlyContext,
783
+ nextReadonlyContextType:
784
+ node.getOperator() === tsm.SyntaxKind.ReadonlyKeyword
785
+ ? 'readonly'
786
+ : 'none',
787
+ indexedAccessDepthChange: 'decr',
788
+ }),
789
+ options,
790
+ );
791
+
792
+ switch (readonlyContext.type) {
793
+ // DeepReadonly<readonly E[]> -> DeepReadonly<E[]>
794
+ case 'DeepReadonly':
795
+ options.replaceNode(node, node.getTypeNode().getFullText());
796
+
797
+ return;
798
+
799
+ case 'Readonly':
800
+ case 'none':
801
+ if (readonlyContext.indexedAccessDepth > 0) {
802
+ options.replaceNode(node, node.getTypeNode().getFullText());
803
+ }
804
+ }
805
+ };
806
+
807
+ // `{ readonly member: V } |-> Readonly<{ member: V }>`
808
+ const transformTypeLiteralNode = (
809
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
810
+ node: tsm.TypeLiteralNode,
811
+ readonlyContext: Exclude<
812
+ ReadonlyContext,
813
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
814
+ >,
815
+ options: ReadonlyTransformerOptionsInternal,
816
+ ): void => {
817
+ if (options.ignoreEmptyObjectTypes && node.getMembers().length === 0) {
818
+ return;
819
+ }
820
+
821
+ // Recursive processing
822
+ transformMembers(
823
+ node.getMembers(),
824
+ 'remove',
825
+ nextReadonlyContext({
826
+ currentReadonlyContext: readonlyContext,
827
+ nextReadonlyContextType: 'none',
828
+ indexedAccessDepthChange: 'decr',
829
+ }),
830
+ options,
831
+ );
832
+
833
+ switch (readonlyContext.type) {
834
+ case 'DeepReadonly':
835
+ case 'Readonly':
836
+ // Don't wrap with Readonly if already readonly
837
+ return;
838
+
839
+ case 'none': {
840
+ if (readonlyContext.indexedAccessDepth === 0) {
841
+ // `{ readonly x: X, readonly y: Y } |-> Readonly<{ x: X, y: Y }>`
842
+ options.replaceNode(node, `Readonly<${node.getFullText()}>`);
843
+ }
844
+ }
845
+ }
846
+ };
847
+
848
+ /** `tr([A, B, C][I])` |-> `[tr(A), tr(B), tr(C)][I]` */
849
+ const transformIndexedAccessTypeNode = (
850
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
851
+ node: tsm.IndexedAccessTypeNode,
852
+ readonlyContext: ReadonlyContext,
853
+ options: ReadonlyTransformerOptionsInternal,
854
+ ): void => {
855
+ // Recursive processing
856
+ transformNode(
857
+ node.getObjectTypeNode(),
858
+ nextReadonlyContext({
859
+ currentReadonlyContext: readonlyContext,
860
+ nextReadonlyContextType: 'none',
861
+ indexedAccessDepthChange: 'incr',
862
+ }),
863
+ options,
864
+ );
865
+
866
+ transformNode(
867
+ node.getIndexTypeNode() /* = I */,
868
+ nextReadonlyContext({
869
+ currentReadonlyContext: readonlyContext,
870
+ nextReadonlyContextType: 'Readonly',
871
+ indexedAccessDepthChange: 'decr',
872
+ }),
873
+ options,
874
+ );
875
+ };
876
+
877
+ /**
878
+ * - `{ [key in Obj]: V }` -> `Readonly<{ [key in Obj]: V }>`
879
+ * - `{ -readonly [key in Obj]: V }` -> `Readonly<{ [key in Obj]: V }>`
880
+ * - `{ readonly [key in Obj]: V }` -> `Readonly<{ [key in Obj]: V }>`
881
+ * - `{ +readonly [key in Obj]: V }` -> `Readonly<{ [key in Obj]: V }>`
882
+ */
883
+ const transformMappedTypeNode = (
884
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
885
+ node: tsm.MappedTypeNode,
886
+ readonlyContext: Exclude<
887
+ ReadonlyContext,
888
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
889
+ >,
890
+ options: ReadonlyTransformerOptionsInternal,
891
+ ): void => {
892
+ const typeNode = node.getTypeNode();
893
+
894
+ if (typeNode !== undefined) {
895
+ transformNode(
896
+ typeNode,
897
+ nextReadonlyContext({
898
+ currentReadonlyContext: readonlyContext,
899
+ nextReadonlyContextType: 'none',
900
+ indexedAccessDepthChange: 'decr',
901
+ }),
902
+ options,
903
+ );
904
+ }
905
+
906
+ const questionTokenText =
907
+ pipe(node.getQuestionToken()?.getText()).mapNullable((t) =>
908
+ t === '?' ? '?' : t === '-' ? '-?' : t === '+' ? '+?' : undefined,
909
+ ).value ?? '';
910
+
911
+ const result: string = pipe(
912
+ // remove readonlyToken
913
+ `{ [${node.getTypeParameter().getFullText()}]${questionTokenText}: ${node.getTypeNode()?.getFullText()} }`,
914
+ ).map((mpt) =>
915
+ match(readonlyContext.type, {
916
+ // Don't wrap with Readonly if already readonly or unnecessary
917
+ DeepReadonly: mpt,
918
+ Readonly: mpt,
919
+ none: readonlyContext.indexedAccessDepth > 0 ? mpt : `Readonly<${mpt}>`,
920
+ }),
921
+ ).value;
922
+
923
+ options.replaceNode(node, result);
924
+ };
925
+
926
+ // Making interface members readonly
927
+ const transformInterfaceDeclarationNode = (
928
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
929
+ node: tsm.InterfaceDeclaration,
930
+ options: ReadonlyTransformerOptionsInternal,
931
+ ): void => {
932
+ for (const param of node.getTypeParameters()) {
933
+ transformNode(param, initialReadonlyContext, options);
934
+ }
935
+
936
+ for (const n of node.getHeritageClauses()) {
937
+ transformNode(n, initialReadonlyContext, options);
938
+ }
939
+
940
+ transformMembers(node.getMembers(), 'add', initialReadonlyContext, options);
941
+ };
942
+
943
+ const transformClassDeclarationNode = (
944
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
945
+ node: tsm.ClassDeclaration,
946
+ options: ReadonlyTransformerOptionsInternal,
947
+ ): void => {
948
+ for (const n of node.getTypeParameters()) {
949
+ transformNode(n, initialReadonlyContext, options);
950
+ }
951
+
952
+ for (const n of node.getHeritageClauses()) {
953
+ transformNode(n, initialReadonlyContext, options);
954
+ }
955
+
956
+ for (const mb of node.getMembers()) {
957
+ if (hasDisableNextLineComment(mb)) {
958
+ options.debugPrint('skipped member by disable-next-line comment');
959
+
960
+ continue;
961
+ }
962
+
963
+ if (mb.isKind(tsm.SyntaxKind.PropertyDeclaration)) {
964
+ if (!checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options)) {
965
+ mb.setIsReadonly(true);
966
+
967
+ const type = mb.getTypeNode();
968
+
969
+ if (type !== undefined) {
970
+ transformNode(type, initialReadonlyContext, options);
971
+ }
972
+
973
+ const initializer = mb.getInitializer();
974
+
975
+ if (initializer !== undefined) {
976
+ transformNode(initializer, initialReadonlyContext, options);
977
+ }
978
+ }
979
+
980
+ continue;
981
+ }
982
+
983
+ transformNode(mb, initialReadonlyContext, options);
984
+ }
985
+
986
+ for (const mb of node.getChildrenOfKind(tsm.SyntaxKind.IndexSignature)) {
987
+ transformIndexSignatureDeclaration(
988
+ mb,
989
+ 'add',
990
+ initialReadonlyContext,
991
+ options,
992
+ );
993
+ }
994
+
995
+ for (const ctor of node.getConstructors()) {
996
+ for (const param of ctor.getParameters()) {
997
+ if (hasDisableNextLineComment(param)) {
998
+ options.debugPrint(
999
+ 'skipped class constructor parameter by disable-next-line comment',
1000
+ );
1001
+
1002
+ continue;
1003
+ }
1004
+
1005
+ {
1006
+ const name = (param satisfies tsm.ParameterDeclaration).getName();
1007
+
1008
+ if (options.ignoredPrefixes.some((p) => name.startsWith(p))) {
1009
+ continue;
1010
+ }
1011
+ }
1012
+
1013
+ // Check if parameter is a property declaration (public/protected/private)
1014
+ const scope = param.getScope();
1015
+
1016
+ // public -> public readonly
1017
+ // protected -> protected readonly
1018
+ // private -> private readonly
1019
+ if (
1020
+ scope === tsm.Scope.Public ||
1021
+ scope === tsm.Scope.Protected ||
1022
+ scope === tsm.Scope.Private
1023
+ ) {
1024
+ param.setIsReadonly(true);
1025
+ }
1026
+
1027
+ const type = param.getTypeNode();
1028
+
1029
+ if (type !== undefined) {
1030
+ transformNode(type, initialReadonlyContext, options);
1031
+ }
1032
+ }
1033
+
1034
+ const body = ctor.getBody();
1035
+
1036
+ if (body !== undefined) {
1037
+ transformNode(body, initialReadonlyContext, options);
1038
+ }
1039
+
1040
+ continue;
1041
+ }
1042
+ };
1043
+
1044
+ /**
1045
+ * - `tr(A & B) -> tr(A) & tr(B)`
1046
+ * - `tr(Readonly<A> & Readonly<B>) -> Readonly<tr(A) & tr(B)>`
1047
+ */
1048
+ const transformIntersectionTypeNode = (
1049
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1050
+ node: tsm.IntersectionTypeNode,
1051
+ readonlyContext: Exclude<
1052
+ ReadonlyContext,
1053
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
1054
+ >,
1055
+ options: ReadonlyTransformerOptionsInternal,
1056
+ ): void => {
1057
+ transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '&');
1058
+ };
1059
+
1060
+ /**
1061
+ * - `tr(A | B) |-> tr(A) | tr(B)`
1062
+ * - `tr(Readonly<A> | Readonly<B>) |-> Readonly<tr(A) | tr(B)>`
1063
+ */
1064
+ const transformUnionTypeNode = (
1065
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1066
+ node: tsm.UnionTypeNode,
1067
+ readonlyContext: Exclude<
1068
+ ReadonlyContext,
1069
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
1070
+ >,
1071
+ options: ReadonlyTransformerOptionsInternal,
1072
+ ): void => {
1073
+ transformUnionOrIntersectionTypeNodeImpl(node, readonlyContext, options, '|');
1074
+ };
1075
+
1076
+ const transformUnionOrIntersectionTypeNodeImpl = (
1077
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1078
+ node: tsm.IntersectionTypeNode | tsm.UnionTypeNode,
1079
+ readonlyContext: Exclude<
1080
+ ReadonlyContext,
1081
+ Readonly<{ type: 'readonly'; indexedAccessDepth: SafeUintWithSmallInt }>
1082
+ >,
1083
+ options: ReadonlyTransformerOptionsInternal,
1084
+ operator: '&' | '|',
1085
+ ): void => {
1086
+ const nextReadonlyContextValue = nextReadonlyContext({
1087
+ currentReadonlyContext: readonlyContext,
1088
+ nextReadonlyContextType: 'none',
1089
+ indexedAccessDepthChange: 'keep',
1090
+ });
1091
+
1092
+ // Recursive processing
1093
+ for (const n of node.getTypeNodes() /* = [A, B] */) {
1094
+ transformNode(n, nextReadonlyContextValue, options);
1095
+ }
1096
+
1097
+ const newTypes = node.getTypeNodes();
1098
+
1099
+ const { primitives, arraysAndTuples, typeLiterals, others } =
1100
+ groupUnionIntersectionTypes(newTypes);
1101
+
1102
+ options.debugPrint({ primitives, arraysAndTuples, typeLiterals, others });
1103
+
1104
+ const typeLiteralsWrappedWithReadonly: readonly [] | readonly [string] =
1105
+ typeLiterals === undefined
1106
+ ? []
1107
+ : [
1108
+ unionToString({
1109
+ types: typeLiterals.nodes.map((n) =>
1110
+ isReadonlyTypeReferenceNode(n)
1111
+ ? // NOTE: Readonly<A & B> -> (A & B)
1112
+ unwrapReadonlyTypeArgText(n)
1113
+ : (n satisfies tsm.TypeLiteralNode).getFullText(),
1114
+ ),
1115
+ op: operator,
1116
+ wrapWithReadonly:
1117
+ nextReadonlyContextValue.type !== 'DeepReadonly' &&
1118
+ nextReadonlyContextValue.indexedAccessDepth === 0,
1119
+ }),
1120
+ ];
1121
+
1122
+ // Sort by first occurrence (preserving the original union order as much as possible)
1123
+ const sorted: readonly string[] = [
1124
+ mapNullable(
1125
+ primitives,
1126
+ (a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
1127
+ ),
1128
+ mapNullable(
1129
+ arraysAndTuples,
1130
+ (a) => [a.nodes.map((n) => n.getFullText()), a.firstPosition] as const,
1131
+ ),
1132
+ mapNullable(
1133
+ typeLiterals,
1134
+ (a) => [typeLiteralsWrappedWithReadonly, a.firstPosition] as const,
1135
+ ),
1136
+ mapNullable(
1137
+ others,
1138
+ (a) =>
1139
+ [
1140
+ a.nodes.map((n) => wrapWithParentheses(n.getFullText())),
1141
+ a.firstPosition,
1142
+ ] as const,
1143
+ ),
1144
+ ]
1145
+ .filter((x) => x !== undefined)
1146
+ .toSorted((a, b) => a[1] - b[1])
1147
+ .flatMap((a) => a[0]);
1148
+
1149
+ // Readonly<number & { x: X } & { y: Y } & readonly E[]>
1150
+ // -> number & readonly E[] & Readonly<{ x: X } & { y: Y }>
1151
+
1152
+ // Readonly<number | { x: X } | { y: Y } | readonly E[]>
1153
+ // -> number | readonly E[] | Readonly<{ x: X } | { y: Y }>
1154
+ options.replaceNode(
1155
+ node,
1156
+ unionToString({ types: sorted, op: operator, wrapWithReadonly: false }),
1157
+ );
1158
+ };
1159
+
1160
+ const unionToString = ({
1161
+ types,
1162
+ op,
1163
+ wrapWithReadonly,
1164
+ }: Readonly<{
1165
+ types: readonly string[];
1166
+ op: '&' | '|';
1167
+ wrapWithReadonly: boolean | string;
1168
+ }>): string =>
1169
+ types.length === 0
1170
+ ? 'never'
1171
+ : Arr.isArrayOfLength(types, 1)
1172
+ ? wrapWithReadonly === false
1173
+ ? wrapWithParentheses(types[0])
1174
+ : `${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types[0]}>`
1175
+ : wrapWithReadonly === false
1176
+ ? wrapWithParentheses(types.join(` ${op} `))
1177
+ : `${isString(wrapWithReadonly) ? wrapWithReadonly : 'Readonly'}<${types.join(` ${op} `)}>`;
1178
+
1179
+ /** Convert ((T)) -> (T) recursively */
1180
+ const transformParenthesizedTypeNode = (
1181
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1182
+ node: tsm.ParenthesizedTypeNode,
1183
+ readonlyContext: ReadonlyContext,
1184
+ options: ReadonlyTransformerOptionsInternal,
1185
+ ): void => {
1186
+ const typeNode = node.getTypeNode();
1187
+
1188
+ if (typeNode.isKind(tsm.SyntaxKind.ParenthesizedType)) {
1189
+ // Recursive processing
1190
+ transformParenthesizedTypeNode(typeNode, readonlyContext, options);
1191
+ }
1192
+
1193
+ transformNode(node.getTypeNode(), readonlyContext, options);
1194
+
1195
+ const T = node.getTypeNode();
1196
+
1197
+ if (
1198
+ // remove () if T is TypeReferenceNode
1199
+ // e.g. `(Readonly<A>) |-> Readonly<A>`
1200
+ T.isKind(tsm.SyntaxKind.TypeReference) ||
1201
+ // remove () if T is ArrayTypeNode
1202
+ // e.g. `(A[]) |-> A[]`
1203
+ T.isKind(tsm.SyntaxKind.ArrayType) ||
1204
+ // remove () if T is TupleTypeNode
1205
+ // e.g. `([A]) |-> [A]`
1206
+ T.isKind(tsm.SyntaxKind.TupleType) ||
1207
+ // remove () if T is PrimitiveTypeNode
1208
+ // e.g. `(number) |-> number`
1209
+ isPrimitiveTypeNode(T) ||
1210
+ // remove () if T is TypeLiteralNode
1211
+ // e.g. `({ member: V }) |-> { member: V }`
1212
+ T.isKind(tsm.SyntaxKind.TypeLiteral)
1213
+ ) {
1214
+ options.replaceNode(node, T.getFullText());
1215
+ }
1216
+ };
1217
+
1218
+ /**
1219
+ * `tr(["member1: V1", "member2: V2", "member3: V3"])`
1220
+ *
1221
+ * -> `["member1: tr(V1)", "member2: tr(V2)", "member3: tr(V3)"]`
1222
+ */
1223
+ const transformMembers = (
1224
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1225
+ members: readonly tsm.TypeElementTypes[],
1226
+ readonlyModifier: 'add' | 'remove',
1227
+ readonlyContext: Extract<
1228
+ ReadonlyContext,
1229
+ Readonly<{
1230
+ type: 'DeepReadonly' | 'none';
1231
+ indexedAccessDepth: SafeUintWithSmallInt;
1232
+ }>
1233
+ >,
1234
+ options: ReadonlyTransformerOptionsInternal,
1235
+ ): void => {
1236
+ for (const mb of members) {
1237
+ if (hasDisableNextLineComment(mb)) {
1238
+ options.debugPrint('skipped by member disable-next-line comment');
1239
+
1240
+ continue;
1241
+ }
1242
+
1243
+ if (mb.isKind(tsm.SyntaxKind.PropertySignature)) {
1244
+ if (!checkIfPropertyNameShouldBeIgnored(mb.getNameNode(), options)) {
1245
+ transformPropertySignature(
1246
+ mb,
1247
+ readonlyModifier,
1248
+ readonlyContext,
1249
+ options,
1250
+ );
1251
+ }
1252
+
1253
+ continue;
1254
+ }
1255
+
1256
+ if (mb.isKind(tsm.SyntaxKind.IndexSignature)) {
1257
+ transformIndexSignatureDeclaration(
1258
+ mb,
1259
+ readonlyModifier,
1260
+ readonlyContext,
1261
+ options,
1262
+ );
1263
+
1264
+ continue;
1265
+ }
1266
+
1267
+ transformNode(mb, readonlyContext, options);
1268
+
1269
+ continue;
1270
+ }
1271
+ };
1272
+
1273
+ const transformPropertySignature = (
1274
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1275
+ node: tsm.PropertySignature,
1276
+ readonlyModifier: 'add' | 'remove',
1277
+ readonlyContext: Extract<
1278
+ ReadonlyContext,
1279
+ Readonly<{
1280
+ type: 'DeepReadonly' | 'none';
1281
+ indexedAccessDepth: SafeUintWithSmallInt;
1282
+ }>
1283
+ >,
1284
+ options: ReadonlyTransformerOptionsInternal,
1285
+ ): void => {
1286
+ if (
1287
+ readonlyContext.type === 'DeepReadonly' ||
1288
+ readonlyModifier === 'remove'
1289
+ ) {
1290
+ node.setIsReadonly(false);
1291
+ } else {
1292
+ node.setIsReadonly(true);
1293
+ }
1294
+
1295
+ {
1296
+ const type = node.getTypeNode();
1297
+
1298
+ if (type !== undefined) {
1299
+ transformNode(type, readonlyContext, options);
1300
+ }
1301
+ }
1302
+ };
1303
+
1304
+ const transformIndexSignatureDeclaration = (
1305
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1306
+ node: tsm.IndexSignatureDeclaration,
1307
+ readonlyModifier: 'add' | 'remove',
1308
+ readonlyContext: Extract<
1309
+ ReadonlyContext,
1310
+ Readonly<{
1311
+ type: 'DeepReadonly' | 'none';
1312
+ indexedAccessDepth: SafeUintWithSmallInt;
1313
+ }>
1314
+ >,
1315
+ options: ReadonlyTransformerOptionsInternal,
1316
+ ): void => {
1317
+ if (hasDisableNextLineComment(node)) {
1318
+ options.debugPrint('skipped index signature by disable-next-line comment');
1319
+
1320
+ return;
1321
+ }
1322
+
1323
+ if (
1324
+ readonlyContext.type === 'DeepReadonly' ||
1325
+ readonlyModifier === 'remove'
1326
+ ) {
1327
+ // node.setIsReadonly(false);
1328
+ node.toggleModifier('readonly', false);
1329
+ } else {
1330
+ node.toggleModifier('readonly', true);
1331
+ // node.setIsReadonly(true);
1332
+ }
1333
+
1334
+ {
1335
+ const key = node.getKeyTypeNode();
1336
+
1337
+ transformNode(key, readonlyContext, options);
1338
+ }
1339
+
1340
+ {
1341
+ const ret = node.getReturnTypeNode();
1342
+
1343
+ if (ret !== undefined) {
1344
+ transformNode(ret, readonlyContext, options);
1345
+ }
1346
+ }
1347
+ };
1348
+
1349
+ const checkIfPropertyNameShouldBeIgnored = (
1350
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
1351
+ nameNode: tsm.PropertyName,
1352
+ options: ReadonlyTransformerOptionsInternal,
1353
+ ): boolean => {
1354
+ expectType<typeof nameNode, tsm.PropertyName>('=');
1355
+
1356
+ expectType<
1357
+ tsm.PropertyName,
1358
+ | tsm.NumericLiteral // skip
1359
+ | tsm.BigIntLiteral // skip
1360
+ | tsm.NoSubstitutionTemplateLiteral // invalid syntax
1361
+ | tsm.Identifier // mut_x: number[]
1362
+ | tsm.StringLiteral // "mut_x": number[]
1363
+ | tsm.PrivateIdentifier // #memberName: number[] (class only)
1364
+ | tsm.ComputedPropertyName // [`mut_x`]: number[]
1365
+ >('=');
1366
+
1367
+ return (
1368
+ (nameNode.isKind(tsm.SyntaxKind.Identifier) &&
1369
+ pipe(nameNode.getText()).map((nm) =>
1370
+ options.ignoredPrefixes.some((p) => nm.startsWith(p)),
1371
+ ).value) ||
1372
+ (nameNode.isKind(tsm.SyntaxKind.StringLiteral) &&
1373
+ pipe(nameNode.getLiteralValue()).map((nm) =>
1374
+ options.ignoredPrefixes.some((p) => nm.startsWith(p)),
1375
+ ).value) ||
1376
+ (nameNode.isKind(tsm.SyntaxKind.PrivateIdentifier) &&
1377
+ pipe(nameNode.getText()).map((nm) =>
1378
+ options.ignoredPrefixes.some((p) => nm.startsWith(`#${p}`)),
1379
+ ).value) ||
1380
+ (nameNode.isKind(tsm.SyntaxKind.ComputedPropertyName) &&
1381
+ pipe(nameNode.getExpression()).map((exp) => {
1382
+ if (exp.isKind(tsm.SyntaxKind.StringLiteral)) {
1383
+ const nm = exp.getLiteralValue();
1384
+
1385
+ return options.ignoredPrefixes.some((p) => nm.startsWith(p));
1386
+ }
1387
+
1388
+ return false;
1389
+ }).value)
1390
+ );
1391
+ };