ts-codemod-lib 3.4.1 → 3.4.3

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.
@@ -540,6 +540,55 @@ describe(convertToReadonlyTransformer, () => {
540
540
  type T = readonly Readonly<{ x: 1 } | { y: 2 }>[]
541
541
  `,
542
542
  },
543
+ {
544
+ name: 'DeepReadonly wrapped with `Readonly`',
545
+ source: dedent`
546
+ type T = Readonly<DeepReadonly<{ a: number[] }>>
547
+ `,
548
+ expected: dedent`
549
+ type T = DeepReadonly<{ a: number[] }>
550
+ `, // Readonly<DeepReadonly<T>> -> DeepReadonly<T>
551
+ },
552
+ {
553
+ name: 'Parenthesized DeepReadonly wrapped with `Readonly`',
554
+ source: dedent`
555
+ type T = Readonly<(DeepReadonly<{ a: number[] }>)>
556
+ `,
557
+ expected: dedent`
558
+ type T = DeepReadonly<{ a: number[] }>
559
+ `,
560
+ },
561
+ {
562
+ name: 'Generic DeepReadonly wrapped with `Readonly`',
563
+ source: dedent`
564
+ type T<A> = Readonly<DeepReadonly<A>>
565
+ `,
566
+ expected: dedent`
567
+ type T<A> = DeepReadonly<A>
568
+ `,
569
+ },
570
+ {
571
+ name: 'DeepReadonly wrapped with `Readonly` under a custom typeName',
572
+ source: dedent`
573
+ type T = Readonly<DeepRO<{ a: number[] }>>
574
+ `,
575
+ expected: dedent`
576
+ type T = DeepRO<{ a: number[] }>
577
+ `,
578
+ options: { DeepReadonly: { typeName: 'DeepRO' } },
579
+ },
580
+ {
581
+ // `DeepReadonly` is not the configured name here, so it is an
582
+ // ordinary generic and the `Readonly` around it stays.
583
+ name: 'A type named DeepReadonly is not unwrapped under a custom typeName',
584
+ source: dedent`
585
+ type T<A> = Readonly<DeepReadonly<A>>
586
+ `,
587
+ expected: dedent`
588
+ type T<A> = Readonly<DeepReadonly<A>>
589
+ `,
590
+ options: { DeepReadonly: { typeName: 'DeepRO' } },
591
+ },
543
592
  ])('$name', testFn);
544
593
  });
545
594
 
@@ -2774,7 +2823,7 @@ describe(convertToReadonlyTransformer, () => {
2774
2823
  type NestedIntersection = ({ a: string; } & { b: number; }) & { c: boolean[]; };
2775
2824
  `,
2776
2825
  expected: dedent`
2777
- type NestedIntersection = Readonly<({ a: string; } & { b: number; }) & { c: readonly boolean[]; }>;
2826
+ type NestedIntersection = Readonly<{ a: string; } & { b: number; } & { c: readonly boolean[]; }>;
2778
2827
  `,
2779
2828
  },
2780
2829
  {
@@ -2832,6 +2881,52 @@ describe(convertToReadonlyTransformer, () => {
2832
2881
  type ReadonlyUnion = Readonly<{ a: readonly string[]; } | { b: readonly number[]; }>;
2833
2882
  `,
2834
2883
  },
2884
+ {
2885
+ name: 'Readonly members whose argument is a union are merged without parentheses',
2886
+ source: dedent`
2887
+ type T = Readonly<{ a: number } | { b: number }> | Readonly<{ c: number }>;
2888
+ `,
2889
+ expected: dedent`
2890
+ type T = Readonly<{ a: number } | { b: number } | { c: number }>;
2891
+ `,
2892
+ },
2893
+ {
2894
+ name: 'Readonly members whose argument is a parenthesized union are merged without parentheses',
2895
+ source: dedent`
2896
+ type T = Readonly<{ a: number }> | Readonly<({ b: number } | { c: number })>;
2897
+ `,
2898
+ expected: dedent`
2899
+ type T = Readonly<{ a: number } | { b: number } | { c: number }>;
2900
+ `,
2901
+ },
2902
+ {
2903
+ name: 'Readonly members whose argument is an intersection are merged without parentheses',
2904
+ source: dedent`
2905
+ type T = Readonly<{ a: number } & { b: number }> & Readonly<{ c: number }>;
2906
+ `,
2907
+ expected: dedent`
2908
+ type T = Readonly<{ a: number } & { b: number } & { c: number }>;
2909
+ `,
2910
+ },
2911
+ {
2912
+ // An intersection argument spliced into a union keeps its parentheses.
2913
+ name: 'An intersection argument merged into a union of Readonly members keeps its parentheses',
2914
+ source: dedent`
2915
+ type T = Readonly<{ a: number } & { b: number }> | Readonly<{ c: number }>;
2916
+ `,
2917
+ expected: dedent`
2918
+ type T = Readonly<({ a: number } & { b: number }) | { c: number }>;
2919
+ `,
2920
+ },
2921
+ {
2922
+ name: 'A union argument merged into an intersection of Readonly members keeps its parentheses',
2923
+ source: dedent`
2924
+ type T = Readonly<{ a: number } | { b: number }> & Readonly<{ c: number }>;
2925
+ `,
2926
+ expected: dedent`
2927
+ type T = Readonly<({ a: number } | { b: number }) & { c: number }>;
2928
+ `,
2929
+ },
2835
2930
  ])('$name', testFn);
2836
2931
  });
2837
2932
 
@@ -3034,6 +3129,398 @@ describe(convertToReadonlyTransformer, () => {
3034
3129
  });
3035
3130
  });
3036
3131
 
3132
+ describe('Readonly over a union or intersection', () => {
3133
+ test.each(
3134
+ [
3135
+ // A member whose readonly-ness the codemod cannot see stays under the
3136
+ // `Readonly` it was written under.
3137
+ {
3138
+ name: 'An alias in an intersection under Readonly stays under it',
3139
+ source: dedent`
3140
+ type X<A> = Readonly<A & { b: string[] }>;
3141
+ `,
3142
+ expected: dedent`
3143
+ type X<A> = Readonly<A & { b: readonly string[] }>;
3144
+ `,
3145
+ },
3146
+ {
3147
+ name: 'An alias in a union under Readonly stays under it',
3148
+ source: dedent`
3149
+ type X<A> = Readonly<A | { b: string[] }>;
3150
+ `,
3151
+ expected: dedent`
3152
+ type X<A> = Readonly<A | { b: readonly string[] }>;
3153
+ `,
3154
+ },
3155
+ {
3156
+ name: 'An intersection of aliases under Readonly is left as written',
3157
+ source: dedent`
3158
+ type X<A, B> = Readonly<A & B>;
3159
+ `,
3160
+ expected: dedent`
3161
+ type X<A, B> = Readonly<A & B>;
3162
+ `,
3163
+ },
3164
+ {
3165
+ name: 'A nested union under Readonly stays under it',
3166
+ source: dedent`
3167
+ type X<A, B> = Readonly<(A | B) & { c: string[] }>;
3168
+ `,
3169
+ expected: dedent`
3170
+ type X<A, B> = Readonly<(A | B) & { c: readonly string[] }>;
3171
+ `,
3172
+ },
3173
+ {
3174
+ name: 'Members readonly on their own are still taken out',
3175
+ source: dedent`
3176
+ type X<A> = Readonly<number | string[] | DeepReadonly<A> | Map<string, A> | { b: string }>;
3177
+ `,
3178
+ expected: dedent`
3179
+ type X<A> = number | readonly string[] | DeepReadonly<A> | ReadonlyMap<string, A> | Readonly<{ b: string }>;
3180
+ `,
3181
+ },
3182
+ {
3183
+ // `Readonly` would drop the call signature.
3184
+ name: 'A function type is still taken out',
3185
+ source: dedent`
3186
+ type X = Readonly<(() => void) & { b: string }>;
3187
+ `,
3188
+ expected: dedent`
3189
+ type X = (() => void) & Readonly<{ b: string }>;
3190
+ `,
3191
+ },
3192
+
3193
+ // #2231: what replaces `Readonly<…>` keeps the parentheses its
3194
+ // position needs.
3195
+ {
3196
+ name: 'Readonly of an intersection with an alias as an array element under readonly',
3197
+ source: dedent`
3198
+ type A = Readonly<{ a: string }>;
3199
+ export type X = readonly Readonly<A & { b: string }>[];
3200
+ `,
3201
+ expected: dedent`
3202
+ type A = Readonly<{ a: string }>;
3203
+ export type X = readonly Readonly<A & { b: string }>[];
3204
+ `,
3205
+ },
3206
+ {
3207
+ name: 'Readonly of an intersection with an alias as an array element',
3208
+ source: dedent`
3209
+ type A = Readonly<{ a: string }>;
3210
+ export type X = Readonly<A & { b: string }>[];
3211
+ `,
3212
+ expected: dedent`
3213
+ type A = Readonly<{ a: string }>;
3214
+ export type X = readonly Readonly<A & { b: string }>[];
3215
+ `,
3216
+ },
3217
+ {
3218
+ name: 'Readonly of a union with an alias as an array element',
3219
+ source: dedent`
3220
+ type X<A> = Readonly<A | { b: string }>[];
3221
+ `,
3222
+ expected: dedent`
3223
+ type X<A> = readonly Readonly<A | { b: string }>[];
3224
+ `,
3225
+ },
3226
+ {
3227
+ name: 'Readonly of an intersection with an alias in ReadonlyArray',
3228
+ source: dedent`
3229
+ type X<A> = ReadonlyArray<Readonly<A & { b: string }>>;
3230
+ `,
3231
+ expected: dedent`
3232
+ type X<A> = readonly Readonly<A & { b: string }>[];
3233
+ `,
3234
+ },
3235
+ {
3236
+ name: 'A Readonly that is taken off as an array element',
3237
+ source: dedent`
3238
+ type X = Readonly<number | readonly string[]>[];
3239
+ `,
3240
+ expected: dedent`
3241
+ type X = readonly (number | readonly string[])[];
3242
+ `,
3243
+ },
3244
+ {
3245
+ name: 'A Readonly that is taken off under readonly',
3246
+ source: dedent`
3247
+ type X = readonly Readonly<number & { b: string }>[];
3248
+ `,
3249
+ expected: dedent`
3250
+ type X = readonly (number & Readonly<{ b: string }>)[];
3251
+ `,
3252
+ },
3253
+ {
3254
+ name: 'A Readonly that is taken off as an intersection member',
3255
+ source: dedent`
3256
+ type X = { a: string } & Readonly<number | { b: string }>;
3257
+ `,
3258
+ expected: dedent`
3259
+ type X = Readonly<{ a: string }> & (number | Readonly<{ b: string }>);
3260
+ `,
3261
+ },
3262
+
3263
+ // #2169: `Readonly<A> & Readonly<B>` is merged only when both are
3264
+ // object types, because `Readonly<string[] & X>` leaves the array
3265
+ // mutable.
3266
+ {
3267
+ name: 'An intersection of Readonly aliases is not merged',
3268
+ source: dedent`
3269
+ type X<A, B> = Readonly<A> & Readonly<B>;
3270
+ `,
3271
+ expected: dedent`
3272
+ type X<A, B> = Readonly<A> & Readonly<B>;
3273
+ `,
3274
+ },
3275
+ {
3276
+ name: 'A Readonly alias is not merged with a type literal',
3277
+ source: dedent`
3278
+ type X<A> = Readonly<A> & { b: string[] };
3279
+ `,
3280
+ expected: dedent`
3281
+ type X<A> = Readonly<A> & Readonly<{ b: readonly string[] }>;
3282
+ `,
3283
+ },
3284
+ {
3285
+ name: 'Readonly type literals in an intersection are merged',
3286
+ source: dedent`
3287
+ type X = Readonly<{ a: string } | { c: string }> & Readonly<{ b: string }>;
3288
+ `,
3289
+ expected: dedent`
3290
+ type X = Readonly<({ a: string } | { c: string }) & { b: string }>;
3291
+ `,
3292
+ },
3293
+ {
3294
+ // A mapped type over anything but `keyof T` is an object type.
3295
+ name: 'Mapped types in an intersection under Readonly are merged',
3296
+ source: dedent`
3297
+ type X<S, K extends keyof S> = Readonly<{ [k in K]?: S[k] } & { [k in Exclude<keyof S, K>]: S[k] }>;
3298
+ `,
3299
+ expected: dedent`
3300
+ type X<S, K extends keyof S> = Readonly<{ [k in K]?: S[k] } & { [k in Exclude<keyof S, K>]: S[k] }>;
3301
+ `,
3302
+ },
3303
+ {
3304
+ // `{ [k in keyof T]: T[k] }` maps an array `T` to an array.
3305
+ name: 'A homomorphic mapped type in an intersection is not merged',
3306
+ source: dedent`
3307
+ type X<T> = Readonly<{ [k in keyof T]: T[k] }> & Readonly<{ b: string }>;
3308
+ `,
3309
+ expected: dedent`
3310
+ type X<T> = Readonly<{ [k in keyof T]: T[k] }> & Readonly<{ b: string }>;
3311
+ `,
3312
+ },
3313
+ {
3314
+ name: 'A union of Readonly aliases is still merged',
3315
+ source: dedent`
3316
+ type X<A, B> = Readonly<A> | Readonly<B>;
3317
+ `,
3318
+ expected: dedent`
3319
+ type X<A, B> = Readonly<A | B>;
3320
+ `,
3321
+ },
3322
+ ].flatMap((c) => [
3323
+ c,
3324
+ // The output is a fixed point: running the codemod again keeps it,
3325
+ // readonly-ness included.
3326
+ { ...c, name: `${c.name} (fixed point)`, source: c.expected },
3327
+ ]),
3328
+ )('$name', testFn);
3329
+
3330
+ // What merging `Readonly` members relies on.
3331
+ test('premises of merging Readonly members', () => {
3332
+ type X = { x: number };
3333
+
3334
+ // `Readonly` distributes over `|`, so merging a union is safe.
3335
+ expectType<Readonly<string[] | X>, readonly string[] | Readonly<X>>('=');
3336
+
3337
+ // It does not over `&` with an array member: the merged form leaves the
3338
+ // array mutable.
3339
+ expectType<Readonly<string[] & X>, readonly string[] & Readonly<X>>('<=');
3340
+
3341
+ expectType<Readonly<string[] & X>, readonly string[] & Readonly<X>>('!=');
3342
+ });
3343
+ });
3344
+
3345
+ describe('Comments in front of a member', () => {
3346
+ // `replaceWithText` overwrites a node from `getStart()`, so a comment in
3347
+ // front of it stays where it is. Text built for the replacement must carry
3348
+ // the comments after that point — those in front of each member and those
3349
+ // in front of the `|` / `&` separating them — and none before it.
3350
+ test.each([
3351
+ {
3352
+ name: 'JSDoc on each member of a union with a leading `|`',
3353
+ source: dedent`
3354
+ export type ProbeStore =
3355
+ /** Kept until the browser is closed. */
3356
+ | 'session'
3357
+ /** Kept until it is forgotten. */
3358
+ | 'device';
3359
+ `,
3360
+ // Prettier itself moves the second comment to the end of the line
3361
+ // before it; here it stays in front of its member.
3362
+ expected: dedent`
3363
+ export type ProbeStore =
3364
+ | /** Kept until the browser is closed. */ 'session'
3365
+ | /** Kept until it is forgotten. */ 'device';
3366
+ `,
3367
+ },
3368
+ {
3369
+ name: 'Line comments on each member of a union with a leading `|`',
3370
+ source: dedent`
3371
+ type T =
3372
+ // first
3373
+ | 'a'
3374
+ // second
3375
+ | 'b'
3376
+ // third
3377
+ | 'c';
3378
+ `,
3379
+ expected: dedent`
3380
+ type T =
3381
+ // first
3382
+ | 'a'
3383
+ // second
3384
+ | 'b'
3385
+ // third
3386
+ | 'c';
3387
+ `,
3388
+ },
3389
+ {
3390
+ name: 'Comments on each member of a union without a leading `|`',
3391
+ source: dedent`
3392
+ type T = /** a */ 'a' | /** b */ 'b';
3393
+ `,
3394
+ expected: dedent`
3395
+ type T = /** a */ 'a' | /** b */ 'b';
3396
+ `,
3397
+ },
3398
+ {
3399
+ name: 'A comment after `|` stays with the member after it',
3400
+ source: dedent`
3401
+ type T = 'a' | // about b
3402
+ 'b';
3403
+ `,
3404
+ expected: dedent`
3405
+ type T = 'a' | // about b
3406
+ 'b';
3407
+ `,
3408
+ },
3409
+ ...toUnionAndIntersectionTestCase({
3410
+ title: (op) => `Comments on members of an ${op} that are converted`,
3411
+ testCase: (op) => ({
3412
+ source: dedent`
3413
+ type T =
3414
+ /** list */
3415
+ ${op} number[]
3416
+ /** object */
3417
+ ${op} { a: number[] };
3418
+ `,
3419
+ // The comment on the object moves inside the `Readonly` around it.
3420
+ expected: dedent`
3421
+ type T =
3422
+ /** list */
3423
+ ${op} readonly number[]
3424
+ ${op} Readonly</** object */ { a: readonly number[] }>;
3425
+ `,
3426
+ }),
3427
+ }),
3428
+ ...toUnionAndIntersectionTestCase({
3429
+ title: (op) =>
3430
+ `Comments on Readonly members of an ${op} that are merged`,
3431
+ testCase: (op) => ({
3432
+ source: dedent`
3433
+ type T = /** a */ Readonly<{ a: number }> ${op} /** b */ Readonly<{ b: number }>;
3434
+ `,
3435
+ expected: dedent`
3436
+ type T = /** a */ Readonly<{ a: number } ${op} /** b */ { b: number }>;
3437
+ `,
3438
+ }),
3439
+ }),
3440
+ {
3441
+ name: 'Comments on DeepReadonly members that are merged',
3442
+ source: dedent`
3443
+ type T = /** a */ DeepReadonly<{ a: number[] }> | /** b */ DeepReadonly<{ b: string[] }>;
3444
+ `,
3445
+ expected: dedent`
3446
+ type T = /** a */ DeepReadonly<{ a: number[] } | /** b */ { b: string[] }>;
3447
+ `,
3448
+ },
3449
+ {
3450
+ name: 'A comment in front of a union inside parentheses',
3451
+ source: dedent`
3452
+ type T = (/** a */ 'a' | 'b')[];
3453
+ `,
3454
+ expected: dedent`
3455
+ type T = /** a */ readonly ('a' | 'b')[];
3456
+ `,
3457
+ },
3458
+ {
3459
+ name: 'Comments around parentheses that Readonly<T> unwraps',
3460
+ source: dedent`
3461
+ type T = Readonly</** a */ (/** b */ number[])>;
3462
+ `,
3463
+ expected: dedent`
3464
+ type T = /** a */ /** b */ readonly number[];
3465
+ `,
3466
+ },
3467
+ {
3468
+ name: 'Comments around a DeepReadonly argument that Readonly<T> unwraps',
3469
+ source: dedent`
3470
+ type T = Readonly</** a */ (/** b */ DeepReadonly<{ a: number[] }>)>;
3471
+ `,
3472
+ expected: dedent`
3473
+ type T = /** a */ /** b */ DeepReadonly<{ a: number[] }>;
3474
+ `,
3475
+ },
3476
+ {
3477
+ name: 'Comments around a union argument spliced into a merged union',
3478
+ source: dedent`
3479
+ type T = Readonly</** a */ (/** b */ { a: number } | { b: number })> | Readonly<{ c: number }>;
3480
+ `,
3481
+ expected: dedent`
3482
+ type T = /** a */ /** b */ Readonly<{ a: number } | { b: number } | { c: number }>;
3483
+ `,
3484
+ },
3485
+ {
3486
+ name: 'Comments around parentheses in a rest element',
3487
+ source: dedent`
3488
+ type T = [string, ...(/** a */ readonly number[])];
3489
+ `,
3490
+ expected: dedent`
3491
+ type T = readonly [string, .../** a */ number[]];
3492
+ `,
3493
+ },
3494
+ {
3495
+ name: 'A comment in front of an array type',
3496
+ source: dedent`
3497
+ type T = /** c */ number[];
3498
+ `,
3499
+ expected: dedent`
3500
+ type T = /** c */ readonly number[];
3501
+ `,
3502
+ },
3503
+ {
3504
+ name: 'A comment in front of a tuple type',
3505
+ source: dedent`
3506
+ type T = /** c */ [number, string];
3507
+ `,
3508
+ expected: dedent`
3509
+ type T = /** c */ readonly [number, string];
3510
+ `,
3511
+ },
3512
+ {
3513
+ name: 'A comment in front of a type literal',
3514
+ source: dedent`
3515
+ type T = /** c */ { a: number };
3516
+ `,
3517
+ expected: dedent`
3518
+ type T = /** c */ Readonly<{ a: number }>;
3519
+ `,
3520
+ },
3521
+ ])('$name', testFn);
3522
+ });
3523
+
3037
3524
  describe('Parenthesized types', () => {
3038
3525
  test.each([
3039
3526
  {
@@ -3877,7 +4364,7 @@ describe(convertToReadonlyTransformer, () => {
3877
4364
  type /* Block Comment */ WithComments = Readonly<{
3878
4365
  prop1: readonly string[]; // Trailing comment
3879
4366
  /* Another block */
3880
- prop2: /* member type comment */ readonly /* member type inner comment */ number[];
4367
+ prop2: /* member type comment */ /* member type inner comment */ readonly number[];
3881
4368
  /* End block */
3882
4369
  }>;
3883
4370
  `,
@@ -4678,11 +5165,10 @@ const toUnionAndIntersectionTestCase = ({
4678
5165
 
4679
5166
  describe('idempotency', () => {
4680
5167
  test('a union whose members carry leading whitespace reaches a fixed point', () => {
4681
- // The members are read with `getFullText()`, which includes the whitespace
4682
- // in front of each one, and joined with a separator that adds its own. Left
4683
- // alone the two compound, and a file with `// prettier-ignore` on a long
4684
- // union grows by one space per member on every run — nothing downstream
4685
- // normalizes it back.
5168
+ // Whitespace in front of a member, joined with a separator that adds its
5169
+ // own, compounds when left alone, and a file with `// prettier-ignore` on a
5170
+ // long union grows by one space per member on every run — nothing
5171
+ // downstream normalizes it back.
4686
5172
  const code = [
4687
5173
  '// prettier-ignore',
4688
5174
  'export type P = ',
@@ -4700,4 +5186,45 @@ describe('idempotency', () => {
4700
5186
 
4701
5187
  expect(twice).toBe(once);
4702
5188
  });
5189
+
5190
+ test('comments on union members survive rounds of the codemod and Prettier', async () => {
5191
+ // `fix:codemod` runs the codemod and then Prettier, which takes off the
5192
+ // parentheses the codemod puts around a union, so every run starts again
5193
+ // from the unparenthesized form. A comment lost in one round or copied in
5194
+ // the next shows up only across several of them.
5195
+ const code = dedent`
5196
+ export type ProbeStore =
5197
+ /** Kept until the browser is closed. */
5198
+ | 'session'
5199
+ /** Kept until it is forgotten. */
5200
+ | 'device';
5201
+ `;
5202
+
5203
+ const round = async (src: string): Promise<string> =>
5204
+ prettier.format(
5205
+ transformSourceCode(src, false, [convertToReadonlyTransformer()]),
5206
+ {
5207
+ parser: 'typescript',
5208
+ plugins: [prettierPluginTypeScript, prettierPluginEstree],
5209
+ },
5210
+ );
5211
+
5212
+ const round1 = await round(code);
5213
+
5214
+ const round2 = await round(round1);
5215
+
5216
+ const round3 = await round(round2);
5217
+
5218
+ expect(round1).toBe(
5219
+ `${dedent`
5220
+ export type ProbeStore =
5221
+ /** Kept until the browser is closed. */
5222
+ "session" | /** Kept until it is forgotten. */ "device";
5223
+ `}\n`,
5224
+ );
5225
+
5226
+ expect(round2).toBe(round1);
5227
+
5228
+ expect(round3).toBe(round1);
5229
+ });
4703
5230
  });