ts-codemod-lib 3.5.0 → 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.
@@ -3926,6 +3926,413 @@ describe(convertToReadonlyTransformer, () => {
3926
3926
  ])('$name', testFn);
3927
3927
  });
3928
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
+
3929
4336
  describe('Call and construct signatures', () => {
3930
4337
  // `Readonly<T>` is a mapped type over `keyof T`, and a call or construct
3931
4338
  // signature is not a property, so wrapping a type literal that carries one
@@ -4477,10 +4884,10 @@ describe(convertToReadonlyTransformer, () => {
4477
4884
  type T = { mut_value: number[]; normalValue: string[] }
4478
4885
  `,
4479
4886
  expected: dedent`
4480
- type T = Readonly<{
4887
+ type T = {
4481
4888
  mut_value: number[];
4482
- normalValue: readonly string[];
4483
- }>;
4889
+ readonly normalValue: readonly string[];
4890
+ };
4484
4891
  `,
4485
4892
  options: { ignorePrefixes: ['mut_'] },
4486
4893
  },
@@ -4490,11 +4897,11 @@ describe(convertToReadonlyTransformer, () => {
4490
4897
  type T = { mut_value: number[]; mutable_data: string[]; normalValue: boolean[] };
4491
4898
  `,
4492
4899
  expected: dedent`
4493
- type T = Readonly<{
4900
+ type T = {
4494
4901
  mut_value: number[];
4495
4902
  mutable_data: string[];
4496
- normalValue: readonly boolean[];
4497
- }>;
4903
+ readonly normalValue: readonly boolean[];
4904
+ };
4498
4905
  `,
4499
4906
  options: { ignorePrefixes: ['mut_', 'mutable_'] },
4500
4907
  },
@@ -4510,10 +4917,10 @@ describe(convertToReadonlyTransformer, () => {
4510
4917
  `,
4511
4918
  expected: dedent`
4512
4919
  type T = Readonly<{
4513
- data: Readonly<{
4920
+ data: {
4514
4921
  mut_items: number[];
4515
- items: readonly string[];
4516
- }>;
4922
+ readonly items: readonly string[];
4923
+ };
4517
4924
  }>;
4518
4925
  `,
4519
4926
  options: { ignorePrefixes: ['mut_'] },
@@ -4566,11 +4973,11 @@ describe(convertToReadonlyTransformer, () => {
4566
4973
  };
4567
4974
  `,
4568
4975
  expected: dedent`
4569
- type T = Readonly<{
4976
+ type T = {
4570
4977
  mut_items: {
4571
4978
  [key: string]: number[];
4572
4979
  };
4573
- }>;
4980
+ };
4574
4981
  `,
4575
4982
  options: { ignorePrefixes: ['mut_'] },
4576
4983
  },
@@ -4721,12 +5128,14 @@ describe(convertToReadonlyTransformer, () => {
4721
5128
  options: { ignorePrefixes: ['mut_'] },
4722
5129
  },
4723
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.
4724
5133
  name: 'Object binding pattern in variable declaration (not implemented)',
4725
5134
  source: dedent`
4726
5135
  const { mut_arr, mut_obj }: { mut_arr: string[], mut_obj: number[] } = { mut_arr: [], mut_obj: [] };
4727
5136
  `,
4728
5137
  expected: dedent`
4729
- 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: [] };
4730
5139
  `,
4731
5140
  options: { ignorePrefixes: ['mut_'] },
4732
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
  ) {