ts-codemod-lib 3.4.4 → 3.5.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 (49) hide show
  1. package/README.md +26 -7
  2. package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
  3. package/dist/functions/ast-transformers/append-as-const.mjs +6 -3
  4. package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
  5. package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
  6. package/dist/functions/ast-transformers/convert-interface-to-type.mjs +2 -1
  7. package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -1
  8. package/dist/functions/ast-transformers/convert-to-readonly.d.mts +10 -0
  9. package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
  10. package/dist/functions/ast-transformers/convert-to-readonly.mjs +120 -16
  11. package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
  12. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +7 -1
  13. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
  14. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +25 -2
  15. package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
  16. package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
  17. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs +3 -2
  18. package/dist/functions/ast-transformers/replace-any-with-unknown.mjs.map +1 -1
  19. package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
  20. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +6 -5
  21. package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -1
  22. package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -1
  23. package/dist/functions/functions/has-disable-next-line-comment.mjs +56 -4
  24. package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -1
  25. package/dist/functions/utils/index.d.mts +1 -0
  26. package/dist/functions/utils/index.d.mts.map +1 -1
  27. package/dist/functions/utils/index.mjs +1 -0
  28. package/dist/functions/utils/index.mjs.map +1 -1
  29. package/dist/functions/utils/replace-node-with-text.d.mts +21 -0
  30. package/dist/functions/utils/replace-node-with-text.d.mts.map +1 -0
  31. package/dist/functions/utils/replace-node-with-text.mjs +22 -0
  32. package/dist/functions/utils/replace-node-with-text.mjs.map +1 -0
  33. package/dist/functions/utils/replace-with-debug.d.mts.map +1 -1
  34. package/dist/functions/utils/replace-with-debug.mjs +2 -1
  35. package/dist/functions/utils/replace-with-debug.mjs.map +1 -1
  36. package/package.json +25 -25
  37. package/src/functions/ast-transformers/append-as-const.mts +9 -3
  38. package/src/functions/ast-transformers/convert-interface-to-type.mts +2 -1
  39. package/src/functions/ast-transformers/convert-to-readonly.mts +187 -22
  40. package/src/functions/ast-transformers/convert-to-readonly.test.mts +430 -32
  41. package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +36 -5
  42. package/src/functions/ast-transformers/replace-any-with-unknown.mts +3 -2
  43. package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +7 -5
  44. package/src/functions/functions/has-disable-next-line-comment.mts +79 -6
  45. package/src/functions/functions/has-disable-next-line-comment.test.mts +96 -0
  46. package/src/functions/utils/index.mts +1 -0
  47. package/src/functions/utils/replace-node-with-text.mts +37 -0
  48. package/src/functions/utils/replace-node-with-text.test.mts +89 -0
  49. package/src/functions/utils/replace-with-debug.mts +2 -1
@@ -7,9 +7,7 @@ import * as prettierPluginEstree from 'prettier/plugins/estree';
7
7
  import * as prettierPluginTypeScript from 'prettier/plugins/typescript';
8
8
  import * as prettier from 'prettier/standalone';
9
9
  import { expectType } from 'ts-data-forge';
10
- // Aliased: an import named `DeepReadonly` makes `vitest/prefer-describe-function-title`
11
- // rewrite `describe('DeepReadonly', ...)` into a reference to this type.
12
- import type { DeepReadonly as TsTypeForgeDeepReadonly } from 'ts-type-forge';
10
+ import type { DeepReadonly } from 'ts-type-forge';
13
11
  import {
14
12
  convertToReadonlyTransformer,
15
13
  type ReadonlyTransformerOptions,
@@ -3090,34 +3088,25 @@ describe(convertToReadonlyTransformer, () => {
3090
3088
  type B = Readonly<{ b: string[] }>;
3091
3089
 
3092
3090
  // `|` commutes with DeepReadonly, so merging a union is safe.
3093
- expectType<
3094
- TsTypeForgeDeepReadonly<A | B>,
3095
- TsTypeForgeDeepReadonly<A> | TsTypeForgeDeepReadonly<B>
3096
- >('=');
3091
+ expectType<DeepReadonly<A | B>, DeepReadonly<A> | DeepReadonly<B>>('=');
3097
3092
 
3098
- expectType<
3099
- TsTypeForgeDeepReadonly<F | X>,
3100
- TsTypeForgeDeepReadonly<F> | TsTypeForgeDeepReadonly<X>
3101
- >('=');
3093
+ expectType<DeepReadonly<F | X>, DeepReadonly<F> | DeepReadonly<X>>('=');
3102
3094
 
3103
3095
  expectType<
3104
- TsTypeForgeDeepReadonly<readonly string[] | X>,
3105
- TsTypeForgeDeepReadonly<readonly string[]> | TsTypeForgeDeepReadonly<X>
3096
+ DeepReadonly<readonly string[] | X>,
3097
+ DeepReadonly<readonly string[]> | DeepReadonly<X>
3106
3098
  >('=');
3107
3099
 
3108
3100
  // `&` does not: an intersection is tested as a whole and matches every
3109
3101
  // branch any one member matches. `F & X` hits the function branch and
3110
3102
  // comes back unchanged, leaving `x` mutable.
3111
- expectType<TsTypeForgeDeepReadonly<F & X>, F & X>('=');
3103
+ expectType<DeepReadonly<F & X>, F & X>('=');
3112
3104
 
3113
- expectType<
3114
- TsTypeForgeDeepReadonly<F & X>,
3115
- TsTypeForgeDeepReadonly<F> & TsTypeForgeDeepReadonly<X>
3116
- >('!=');
3105
+ expectType<DeepReadonly<F & X>, DeepReadonly<F> & DeepReadonly<X>>('!=');
3117
3106
 
3118
3107
  expectType<
3119
- TsTypeForgeDeepReadonly<readonly string[] & X>,
3120
- TsTypeForgeDeepReadonly<readonly string[]> & TsTypeForgeDeepReadonly<X>
3108
+ DeepReadonly<readonly string[] & X>,
3109
+ DeepReadonly<readonly string[]> & DeepReadonly<X>
3121
3110
  >('!=');
3122
3111
 
3123
3112
  // `Readonly` over type literals does commute with `&`, which is what
@@ -3937,6 +3926,413 @@ describe(convertToReadonlyTransformer, () => {
3937
3926
  ])('$name', testFn);
3938
3927
  });
3939
3928
 
3929
+ describe('Type literals with a property of an ignored prefix', () => {
3930
+ // `Readonly<T>` would make the ignored property readonly too, so such a
3931
+ // literal is left unwrapped and its other members are marked readonly in
3932
+ // place, as an interface's are.
3933
+ test.each([
3934
+ {
3935
+ name: 'Members other than the ignored one are marked readonly in place',
3936
+ source: dedent`
3937
+ type T = { mut_a: number[]; b: string[]; readonly c: number };
3938
+ `,
3939
+ expected: dedent`
3940
+ type T = { mut_a: number[]; readonly b: readonly string[]; readonly c: number };
3941
+ `,
3942
+ },
3943
+ {
3944
+ name: 'An ignored property already marked readonly is left as written',
3945
+ source: dedent`
3946
+ type T = { readonly mut_a: number[]; b: string[] };
3947
+ `,
3948
+ expected: dedent`
3949
+ type T = { readonly mut_a: number[]; readonly b: readonly string[] };
3950
+ `,
3951
+ },
3952
+ {
3953
+ name: 'A string literal name or a computed name of a literal is recognized',
3954
+ source: dedent`
3955
+ type T = { "mut_a": number[]; ['mut_b']: number[]; [\`mut_c\`]: number[]; d: string[] };
3956
+ `,
3957
+ expected: dedent`
3958
+ type T = { "mut_a": number[]; ['mut_b']: number[]; [\`mut_c\`]: number[]; readonly d: readonly string[] };
3959
+ `,
3960
+ },
3961
+ {
3962
+ name: 'An index signature alongside is marked readonly in place',
3963
+ source: dedent`
3964
+ type T = { mut_a: number[]; [key: string]: unknown };
3965
+ `,
3966
+ expected: dedent`
3967
+ type T = { mut_a: number[]; readonly [key: string]: unknown };
3968
+ `,
3969
+ },
3970
+ {
3971
+ name: 'Every configured prefix is recognized',
3972
+ source: dedent`
3973
+ type T = { draftItems: number[]; b: string[] };
3974
+ type U = { mutable_items: number[]; b: string[] };
3975
+ `,
3976
+ expected: dedent`
3977
+ type T = { draftItems: number[]; readonly b: readonly string[] };
3978
+ type U = { mutable_items: number[]; readonly b: readonly string[] };
3979
+ `,
3980
+ options: { ignorePrefixes: ['draft', 'mutable_'] },
3981
+ },
3982
+ {
3983
+ name: 'The "_mut_" prefix is recognized by default',
3984
+ source: dedent`
3985
+ type T = { _mut_items: number[]; b: string[] };
3986
+ `,
3987
+ expected: dedent`
3988
+ type T = { _mut_items: number[]; readonly b: readonly string[] };
3989
+ `,
3990
+ },
3991
+ {
3992
+ name: 'Ancestors are wrapped with the shallow Readonly only',
3993
+ source: dedent`
3994
+ type T = {
3995
+ a: {
3996
+ b: {
3997
+ mut_c: number[];
3998
+ d: string[];
3999
+ };
4000
+ };
4001
+ };
4002
+ `,
4003
+ expected: dedent`
4004
+ type T = Readonly<{
4005
+ a: Readonly<{
4006
+ b: {
4007
+ mut_c: number[];
4008
+ readonly d: readonly string[];
4009
+ };
4010
+ }>;
4011
+ }>;
4012
+ `,
4013
+ },
4014
+ {
4015
+ name: 'Nested in an interface',
4016
+ source: dedent`
4017
+ interface I {
4018
+ a: { mut_b: number[]; c: string[] };
4019
+ }
4020
+ `,
4021
+ expected: dedent`
4022
+ interface I {
4023
+ readonly a: { mut_b: number[]; readonly c: readonly string[] };
4024
+ }
4025
+ `,
4026
+ },
4027
+ {
4028
+ name: 'As an array element',
4029
+ source: dedent`
4030
+ type T = { mut_a: number[] }[];
4031
+ `,
4032
+ expected: dedent`
4033
+ type T = readonly { mut_a: number[] }[];
4034
+ `,
4035
+ },
4036
+ {
4037
+ name: 'In a union, it is not merged into the Readonly of the others',
4038
+ source: dedent`
4039
+ type T = { mut_a: number[]; b: string } | { c: number[] } | { d: string };
4040
+ `,
4041
+ expected: dedent`
4042
+ type T = { mut_a: number[]; readonly b: string } | Readonly<{ c: readonly number[] } | { d: string }>;
4043
+ `,
4044
+ },
4045
+ {
4046
+ name: 'In an intersection, it is not merged into the Readonly of the others',
4047
+ source: dedent`
4048
+ type T = { mut_a: number[]; b: string } & { c: number[] };
4049
+ `,
4050
+ expected: dedent`
4051
+ type T = { mut_a: number[]; readonly b: string } & Readonly<{ c: readonly number[] }>;
4052
+ `,
4053
+ },
4054
+ {
4055
+ name: 'In an intersection under Readonly, it is taken out',
4056
+ source: dedent`
4057
+ type T = Readonly<{ mut_a: number[]; b: string } & { c: string }>;
4058
+ `,
4059
+ expected: dedent`
4060
+ type T = { mut_a: number[]; readonly b: string } & Readonly<{ c: string }>;
4061
+ `,
4062
+ },
4063
+ {
4064
+ // Such a `Readonly` is also what earlier versions wrote.
4065
+ name: 'A Readonly written around it is taken off',
4066
+ source: dedent`
4067
+ type T = Readonly<{ mut_a: number[]; b: string[] }>;
4068
+ `,
4069
+ expected: dedent`
4070
+ type T = { mut_a: number[]; readonly b: readonly string[] };
4071
+ `,
4072
+ },
4073
+ {
4074
+ name: 'Nested Readonly written around it are taken off',
4075
+ source: dedent`
4076
+ type T = Readonly<Readonly<{ mut_a: number[]; b: string }>>;
4077
+ `,
4078
+ expected: dedent`
4079
+ type T = { mut_a: number[]; readonly b: string };
4080
+ `,
4081
+ },
4082
+ {
4083
+ name: 'A Readonly written around it in a union is taken off',
4084
+ source: dedent`
4085
+ type T = Readonly<{ mut_a: number[]; b: string }> | { c: string };
4086
+ `,
4087
+ expected: dedent`
4088
+ type T = { mut_a: number[]; readonly b: string } | Readonly<{ c: string }>;
4089
+ `,
4090
+ },
4091
+ {
4092
+ name: 'A Readonly written around it under a Readonly union is taken off',
4093
+ source: dedent`
4094
+ type T = Readonly<Readonly<{ mut_a: number[] }> | { c: string }>;
4095
+ `,
4096
+ expected: dedent`
4097
+ type T = { mut_a: number[] } | Readonly<{ c: string }>;
4098
+ `,
4099
+ },
4100
+ {
4101
+ // That member gets no `readonly` of its own, so the wrapper stays.
4102
+ name: 'A member skipped by the ignore comment keeps the Readonly',
4103
+ source: dedent`
4104
+ type T = Readonly<{
4105
+ // transformer-ignore-next-line
4106
+ b: string[];
4107
+ mut_a: number[];
4108
+ }>;
4109
+ `,
4110
+ expected: dedent`
4111
+ type T = Readonly<{
4112
+ // transformer-ignore-next-line
4113
+ b: string[];
4114
+ mut_a: number[];
4115
+ }>;
4116
+ `,
4117
+ },
4118
+ {
4119
+ name: 'A literal skipped by the ignore comment keeps the Readonly',
4120
+ source: dedent`
4121
+ type T = Readonly<
4122
+ // transformer-ignore-next-line
4123
+ { mut_a: number[]; b: string[] }
4124
+ >;
4125
+ `,
4126
+ expected: dedent`
4127
+ type T = Readonly<
4128
+ // transformer-ignore-next-line
4129
+ { mut_a: number[]; b: string[] }
4130
+ >;
4131
+ `,
4132
+ },
4133
+ {
4134
+ name: 'A literal skipped by the ignore comment in an intersection under Readonly stays under it',
4135
+ source: dedent`
4136
+ type T = Readonly<
4137
+ { c: string } &
4138
+ // transformer-ignore-next-line
4139
+ { mut_a: number[]; b: string[] }
4140
+ >;
4141
+ `,
4142
+ expected: dedent`
4143
+ type T = Readonly<
4144
+ { c: string } &
4145
+ // transformer-ignore-next-line
4146
+ { mut_a: number[]; b: string[] }
4147
+ >;
4148
+ `,
4149
+ },
4150
+ {
4151
+ name: 'An ignored property skipped by the ignore comment does not keep the Readonly',
4152
+ source: dedent`
4153
+ type T = {
4154
+ // transformer-ignore-next-line
4155
+ mut_a: number[];
4156
+ b: string[];
4157
+ };
4158
+ `,
4159
+ expected: dedent`
4160
+ type T = {
4161
+ // transformer-ignore-next-line
4162
+ mut_a: number[];
4163
+ readonly b: readonly string[];
4164
+ };
4165
+ `,
4166
+ },
4167
+ {
4168
+ // `Readonly` is what makes a method or an accessor readonly, and
4169
+ // neither takes a `readonly` modifier.
4170
+ name: 'A literal with a method signature or an accessor is wrapped as before',
4171
+ source: dedent`
4172
+ type T = { mut_a: number[]; f(): void; b: string[] };
4173
+ type U = Readonly<{ mut_a: number[]; get g(): string; set g(v: string) }>;
4174
+ `,
4175
+ expected: dedent`
4176
+ type T = Readonly<{ mut_a: number[]; f(): void; b: readonly string[] }>;
4177
+ type U = Readonly<{ mut_a: number[]; get g(): string; set g(v: string) }>;
4178
+ `,
4179
+ },
4180
+ {
4181
+ // `Readonly<A> & { ... }` would make an array `A` readonly.
4182
+ name: 'In an intersection under Readonly beside a type that may be an array, it stays under it',
4183
+ source: dedent`
4184
+ type T<A> = Readonly<A & { mut_a: number[]; b: string }>;
4185
+ `,
4186
+ expected: dedent`
4187
+ type T<A> = Readonly<A & { mut_a: number[]; readonly b: string }>;
4188
+ `,
4189
+ },
4190
+ {
4191
+ name: 'In an intersection under Readonly beside a primitive, it is taken out',
4192
+ source: dedent`
4193
+ type T = Readonly<string & { mut_a: number[]; b: string }>;
4194
+ `,
4195
+ expected: dedent`
4196
+ type T = string & { mut_a: number[]; readonly b: string };
4197
+ `,
4198
+ },
4199
+ {
4200
+ name: 'An accessor or a method of an ignored prefix is left as written',
4201
+ source: dedent`
4202
+ type T = { get mut_x(): number[]; set mut_x(v: number[]); mut_f(a: string[]): void; b: string[] };
4203
+ `,
4204
+ expected: dedent`
4205
+ type T = { get mut_x(): number[]; set mut_x(v: number[]); mut_f(a: string[]): void; readonly b: readonly string[] };
4206
+ `,
4207
+ },
4208
+ {
4209
+ name: 'A comment after the literal is kept when the Readonly is taken off',
4210
+ source: dedent`
4211
+ type T = Readonly<{ mut_a: number[]; b: string } /* note */>;
4212
+ type U = Readonly<{ mut_a: number[] } // note
4213
+ >[];
4214
+ `,
4215
+ expected: dedent`
4216
+ type T = /* note */ { mut_a: number[]; readonly b: string };
4217
+ type U =
4218
+ // note
4219
+ readonly { mut_a: number[] }[];
4220
+ `,
4221
+ },
4222
+ {
4223
+ name: 'A Readonly written around a single-member union of a literal with a call signature is kept',
4224
+ source: dedent`
4225
+ type T = Readonly<| { (): void; mut_a: number[] }>;
4226
+ `,
4227
+ expected: dedent`
4228
+ type T = Readonly<{ (): void; mut_a: number[] }>;
4229
+ `,
4230
+ },
4231
+ {
4232
+ name: 'A Readonly written around a literal with a call signature is kept',
4233
+ source: dedent`
4234
+ type T = Readonly<{ (): void; mut_a: number[]; b: string }>;
4235
+ `,
4236
+ expected: dedent`
4237
+ type T = Readonly<{ (): void; mut_a: number[]; b: string }>;
4238
+ `,
4239
+ },
4240
+ {
4241
+ name: 'A member that needs no wrapper does not keep the Readonly',
4242
+ source: dedent`
4243
+ type T = {
4244
+ mut_a: number[];
4245
+ mut_f(): void;
4246
+ // transformer-ignore-next-line
4247
+ readonly b: string;
4248
+ };
4249
+ `,
4250
+ expected: dedent`
4251
+ type T = {
4252
+ mut_a: number[];
4253
+ mut_f(): void;
4254
+ // transformer-ignore-next-line
4255
+ readonly b: string;
4256
+ };
4257
+ `,
4258
+ },
4259
+ {
4260
+ name: 'A Readonly written around it under an indexed access is taken off',
4261
+ source: dedent`
4262
+ type T = Readonly<{ mut_a: number[]; b: string[] }>['b'];
4263
+ `,
4264
+ expected: dedent`
4265
+ type T = { mut_a: number[]; b: readonly string[] }['b'];
4266
+ `,
4267
+ },
4268
+ {
4269
+ name: 'A DeepReadonly written explicitly is kept',
4270
+ source: dedent`
4271
+ type T = DeepReadonly<{ a: { mut_b: number[]; c: string[] } }>;
4272
+ `,
4273
+ expected: dedent`
4274
+ type T = DeepReadonly<{ a: { mut_b: number[]; c: string[] } }>;
4275
+ `,
4276
+ },
4277
+ {
4278
+ name: 'Under an indexed access',
4279
+ source: dedent`
4280
+ type T = { mut_a: number[]; b: string[] }['b'];
4281
+ `,
4282
+ expected: dedent`
4283
+ type T = { mut_a: number[]; b: readonly string[] }['b'];
4284
+ `,
4285
+ },
4286
+ {
4287
+ name: 'Without the prefix configured, the literal is wrapped',
4288
+ source: dedent`
4289
+ type T = { mut_a: number[]; b: string[] };
4290
+ `,
4291
+ expected: dedent`
4292
+ type T = Readonly<{ mut_a: readonly number[]; b: readonly string[] }>;
4293
+ `,
4294
+ options: { ignorePrefixes: [] },
4295
+ },
4296
+ ])('$name', testFn);
4297
+
4298
+ test.each([
4299
+ dedent`
4300
+ type T = {
4301
+ a: { mut_b: number[]; c: string[] } | { d: number[] };
4302
+ mut_e: { f: string[] };
4303
+ };
4304
+ `,
4305
+ 'type T = Readonly<{ mut_a: number[]; b: string }> | { c: string };',
4306
+ 'type T = Readonly<{ mut_a: number[]; b: string }> & { c: string };',
4307
+ 'type T = Readonly<{ mut_a: number[] }> | Readonly<{ c: string }>;',
4308
+ 'type T = Readonly<Readonly<{ mut_a: number[] }> | { c: string }>;',
4309
+ 'type T = Readonly<{ mut_a: number[]; b: string } & { c: string }>;',
4310
+ 'type T = { a: string } | { mut_b: number[] } | { c: string };',
4311
+ 'type T<A> = Readonly<A & { mut_a: number[]; b: string }>;',
4312
+ 'type T = Readonly<string & { mut_a: number[]; b: string }>;',
4313
+ 'type T = Readonly<string[] & { mut_a: number[]; b: string }>;',
4314
+ 'type T = Readonly<(() => void) & { mut_a: number[]; b: string }>;',
4315
+ 'type T = Readonly<{ mut_a: number[] } & { c: string } & string>;',
4316
+ 'type T = Readonly<{ mut_a: number[]; b: string } /* note */>;',
4317
+ 'type T = Readonly<{ mut_a: number[] } // note\n>[];',
4318
+ 'type T = Readonly<{ mut_a: number[] } /* c */> | { d: string };',
4319
+ 'type T = Readonly<\n // transformer-ignore-next-line\n { mut_a: number[]; b: string[] }\n>;',
4320
+ ])(
4321
+ 'a type literal with an ignored property reaches a fixed point: %s',
4322
+ (code) => {
4323
+ const once = transformSourceCode(code, false, [
4324
+ convertToReadonlyTransformer(),
4325
+ ]);
4326
+
4327
+ const twice = transformSourceCode(once, false, [
4328
+ convertToReadonlyTransformer(),
4329
+ ]);
4330
+
4331
+ expect(twice).toBe(once);
4332
+ },
4333
+ );
4334
+ });
4335
+
3940
4336
  describe('Call and construct signatures', () => {
3941
4337
  // `Readonly<T>` is a mapped type over `keyof T`, and a call or construct
3942
4338
  // signature is not a property, so wrapping a type literal that carries one
@@ -4488,10 +4884,10 @@ describe(convertToReadonlyTransformer, () => {
4488
4884
  type T = { mut_value: number[]; normalValue: string[] }
4489
4885
  `,
4490
4886
  expected: dedent`
4491
- type T = Readonly<{
4887
+ type T = {
4492
4888
  mut_value: number[];
4493
- normalValue: readonly string[];
4494
- }>;
4889
+ readonly normalValue: readonly string[];
4890
+ };
4495
4891
  `,
4496
4892
  options: { ignorePrefixes: ['mut_'] },
4497
4893
  },
@@ -4501,11 +4897,11 @@ describe(convertToReadonlyTransformer, () => {
4501
4897
  type T = { mut_value: number[]; mutable_data: string[]; normalValue: boolean[] };
4502
4898
  `,
4503
4899
  expected: dedent`
4504
- type T = Readonly<{
4900
+ type T = {
4505
4901
  mut_value: number[];
4506
4902
  mutable_data: string[];
4507
- normalValue: readonly boolean[];
4508
- }>;
4903
+ readonly normalValue: readonly boolean[];
4904
+ };
4509
4905
  `,
4510
4906
  options: { ignorePrefixes: ['mut_', 'mutable_'] },
4511
4907
  },
@@ -4521,10 +4917,10 @@ describe(convertToReadonlyTransformer, () => {
4521
4917
  `,
4522
4918
  expected: dedent`
4523
4919
  type T = Readonly<{
4524
- data: Readonly<{
4920
+ data: {
4525
4921
  mut_items: number[];
4526
- items: readonly string[];
4527
- }>;
4922
+ readonly items: readonly string[];
4923
+ };
4528
4924
  }>;
4529
4925
  `,
4530
4926
  options: { ignorePrefixes: ['mut_'] },
@@ -4577,11 +4973,11 @@ describe(convertToReadonlyTransformer, () => {
4577
4973
  };
4578
4974
  `,
4579
4975
  expected: dedent`
4580
- type T = Readonly<{
4976
+ type T = {
4581
4977
  mut_items: {
4582
4978
  [key: string]: number[];
4583
4979
  };
4584
- }>;
4980
+ };
4585
4981
  `,
4586
4982
  options: { ignorePrefixes: ['mut_'] },
4587
4983
  },
@@ -4732,12 +5128,14 @@ describe(convertToReadonlyTransformer, () => {
4732
5128
  options: { ignorePrefixes: ['mut_'] },
4733
5129
  },
4734
5130
  {
5131
+ // The binding pattern itself is not looked at (see the TODO in
5132
+ // `transformNode`); the type literal is left as is for its properties.
4735
5133
  name: 'Object binding pattern in variable declaration (not implemented)',
4736
5134
  source: dedent`
4737
5135
  const { mut_arr, mut_obj }: { mut_arr: string[], mut_obj: number[] } = { mut_arr: [], mut_obj: [] };
4738
5136
  `,
4739
5137
  expected: dedent`
4740
- const { mut_arr, mut_obj }: Readonly<{ mut_arr: string[], mut_obj: number[] }> = { mut_arr: [], mut_obj: [] };
5138
+ const { mut_arr, mut_obj }: { mut_arr: string[], mut_obj: number[] } = { mut_arr: [], mut_obj: [] };
4741
5139
  `,
4742
5140
  options: { ignorePrefixes: ['mut_'] },
4743
5141
  },
@@ -22,6 +22,11 @@ import {
22
22
  * member that `Readonly` still means something to (see `staysOutsideReadonly`):
23
23
  * taking such a member out of the `Readonly` it was written under would drop
24
24
  * its readonly-ness.
25
+ *
26
+ * `keptUnwrapped` names the further type literals the caller leaves unwrapped
27
+ * (one with a property of an ignored prefix, such as `mut_`), which stay out of
28
+ * that group as one with a call or construct signature does — except under a
29
+ * `Readonly` intersection with a member that may be an array.
25
30
  */
26
31
  export const groupUnionIntersectionTypes = (
27
32
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -30,10 +35,13 @@ export const groupUnionIntersectionTypes = (
30
35
  operator,
31
36
  underReadonly,
32
37
  deepReadonlyTypeName,
38
+ keptUnwrapped = () => false,
33
39
  }: Readonly<{
34
40
  operator: '&' | '|';
35
41
  underReadonly: boolean;
36
42
  deepReadonlyTypeName: string;
43
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
44
+ keptUnwrapped?: (node: tsm.TypeLiteralNode) => boolean;
37
45
  }>,
38
46
  ): Readonly<{
39
47
  primitives:
@@ -109,6 +117,28 @@ export const groupUnionIntersectionTypes = (
109
117
  others: undefined,
110
118
  };
111
119
 
120
+ // Taking a literal out of `Readonly<A & { mut_x: X }>` leaves
121
+ // `Readonly<A> & { mut_x: X }`, which is another type when `A` may be an
122
+ // array (see `isMergeableReadonlyTypeReferenceNode`). So `keptUnwrapped` is
123
+ // honoured under `Readonly<... & ...>` only when every member that stays
124
+ // under it is an object type.
125
+ const honoursKeptUnwrapped =
126
+ !underReadonly ||
127
+ operator === '|' ||
128
+ types.every((t_) => {
129
+ const t = removeParentheses(t_);
130
+
131
+ return (
132
+ isAtomicTypeNode(t) ||
133
+ t.isKind(tsm.SyntaxKind.ArrayType) ||
134
+ t.isKind(tsm.SyntaxKind.TupleType) ||
135
+ isReadonlyTupleOrArrayTypeNode(t) ||
136
+ t.isKind(tsm.SyntaxKind.TypeLiteral) ||
137
+ isMergeableReadonlyTypeReferenceNode(t, operator) ||
138
+ staysOutsideReadonly(t, deepReadonlyTypeName)
139
+ );
140
+ });
141
+
112
142
  for (const [i, t_] of types.entries()) {
113
143
  const t = removeParentheses(t_);
114
144
 
@@ -143,14 +173,15 @@ export const groupUnionIntersectionTypes = (
143
173
  continue;
144
174
  }
145
175
 
146
- // A type literal that declares a call or construct signature is deliberately
147
- // not grouped here: this group is joined and wrapped in a single
148
- // `Readonly<...>`, which such a literal does not survive. It falls through
149
- // to `others`, which leaves it as written. See
176
+ // A type literal that declares a call or construct signature, or one the
177
+ // caller keeps unwrapped, is deliberately not grouped here: this group is
178
+ // joined and wrapped in a single `Readonly<...>`, which such a literal does
179
+ // not survive. It falls through to `others`, which leaves it as written. See
150
180
  // `hasCallOrConstructSignature`.
151
181
  if (
152
182
  (t.isKind(tsm.SyntaxKind.TypeLiteral) &&
153
- !hasCallOrConstructSignature(t)) ||
183
+ !hasCallOrConstructSignature(t) &&
184
+ !(honoursKeptUnwrapped && keptUnwrapped(t))) ||
154
185
  isMergeableReadonlyTypeReferenceNode(t, operator) ||
155
186
  (underReadonly && !staysOutsideReadonly(t, deepReadonlyTypeName))
156
187
  ) {
@@ -4,6 +4,7 @@ import {
4
4
  isSpreadNamedTupleMemberNode,
5
5
  isSpreadParameterNode,
6
6
  } from '../functions/index.mjs';
7
+ import { replaceNodeWithText } from '../utils/index.mjs';
7
8
  import type { TsMorphTransformer } from './types.mjs';
8
9
 
9
10
  const TRANSFORMER_NAME = 'replace-any-with-unknown';
@@ -37,12 +38,12 @@ const transformNode = (node: tsm.Node): void => {
37
38
  // `[name: E0, ...args: any)]` -> `[name: E0, ...args: unknown[]]`
38
39
  isSpreadNamedTupleMemberNode(parent))
39
40
  ) {
40
- anyKeywordNode.replaceWithText('readonly unknown[]');
41
+ replaceNodeWithText(anyKeywordNode, 'readonly unknown[]');
41
42
 
42
43
  return;
43
44
  }
44
45
 
45
- anyKeywordNode.replaceWithText('unknown');
46
+ replaceNodeWithText(anyKeywordNode, 'unknown');
46
47
 
47
48
  return;
48
49
  }
@@ -2,6 +2,7 @@
2
2
  import { Arr } from 'ts-data-forge';
3
3
  import * as tsm from 'ts-morph';
4
4
  import { hasDisableNextLineComment } from '../functions/index.mjs';
5
+ import { replaceNodeWithText } from '../utils/index.mjs';
5
6
  import type { TsMorphTransformer } from './types.mjs';
6
7
 
7
8
  const TRANSFORMER_NAME = 'replace-record-with-unknown-record';
@@ -88,7 +89,8 @@ const processInterfaceDeclaration = (
88
89
 
89
90
  const interfaceName = interfaceDecl.getName();
90
91
 
91
- interfaceDecl.replaceWithText(
92
+ replaceNodeWithText(
93
+ interfaceDecl,
92
94
  `${exportPrefix}type ${interfaceName} = UnknownRecord;`,
93
95
  );
94
96
 
@@ -128,7 +130,7 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
128
130
  Arr.isFixedLengthArray(1, indexSigs) &&
129
131
  isStringUnknownIndexSignature(indexSigs[0])
130
132
  ) {
131
- node.replaceWithText('UnknownRecord');
133
+ replaceNodeWithText(node, 'UnknownRecord');
132
134
 
133
135
  return true;
134
136
  }
@@ -156,7 +158,7 @@ const visitTypeNode = (node: tsm.TypeNode): boolean => {
156
158
  Arr.isFixedLengthArray(1, indexSigs) &&
157
159
  isStringUnknownIndexSignature(indexSigs[0])
158
160
  ) {
159
- node.replaceWithText('UnknownRecord');
161
+ replaceNodeWithText(node, 'UnknownRecord');
160
162
 
161
163
  return true;
162
164
  }
@@ -229,7 +231,7 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
229
231
  typeArgs[0].getText() === 'string' &&
230
232
  typeArgs[1].getText() === 'unknown'
231
233
  ) {
232
- node.replaceWithText('UnknownRecord');
234
+ replaceNodeWithText(node, 'UnknownRecord');
233
235
 
234
236
  return true;
235
237
  }
@@ -255,7 +257,7 @@ const replaceIfRecordUnknown = (node: tsm.TypeReferenceNode): boolean => {
255
257
  innerTypeArgs[0].getText() === 'string' &&
256
258
  innerTypeArgs[1].getText() === 'unknown'
257
259
  ) {
258
- node.replaceWithText('UnknownRecord');
260
+ replaceNodeWithText(node, 'UnknownRecord');
259
261
 
260
262
  return true;
261
263
  }