ts-codemod-lib 3.4.0 → 3.4.2
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.
- package/README.md +4 -4
- package/dist/functions/ast-transformers/append-as-const.d.mts +2 -2
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +13 -3
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +1 -1
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts +2 -2
- package/dist/functions/ast-transformers/convert-to-readonly.d.mts.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +157 -33
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts +2 -2
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.d.mts.map +1 -1
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs +24 -6
- package/dist/functions/ast-transformers/enable-no-unchecked-indexed-access.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +32 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +88 -2
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +1 -1
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts +1 -1
- package/dist/functions/ast-transformers/replace-any-with-unknown.d.mts.map +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +1 -1
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts +1 -1
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -1
- package/dist/functions/functions/is-atomic-type-node.d.mts +1 -1
- package/dist/functions/functions/is-atomic-type-node.d.mts.map +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts +1 -1
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -1
- package/dist/functions/functions/unwrap-readonly.d.mts +1 -1
- package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -1
- package/package.json +14 -14
- package/src/functions/ast-transformers/append-as-const.mts +21 -5
- package/src/functions/ast-transformers/append-as-const.test.mts +24 -0
- package/src/functions/ast-transformers/convert-interface-to-type.mts +1 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +249 -52
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +421 -7
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.mts +30 -8
- package/src/functions/ast-transformers/enable-no-unchecked-indexed-access.test.mts +111 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +1 -1
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +124 -3
- package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +1 -1
- package/src/functions/ast-transformers/replace-any-with-unknown.mts +1 -1
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +1 -1
- package/src/functions/ast-transformers/transform-source-code.mts +1 -1
- package/src/functions/functions/is-atomic-type-node.mts +1 -1
- package/src/functions/functions/is-readonly-node.mts +1 -1
- package/src/functions/functions/unwrap-readonly.mts +1 -1
|
@@ -9,7 +9,7 @@ import * as prettier from 'prettier/standalone';
|
|
|
9
9
|
import { expectType } from 'ts-data-forge';
|
|
10
10
|
// Aliased: an import named `DeepReadonly` makes `vitest/prefer-describe-function-title`
|
|
11
11
|
// rewrite `describe('DeepReadonly', ...)` into a reference to this type.
|
|
12
|
-
import {
|
|
12
|
+
import type { DeepReadonly as TsTypeForgeDeepReadonly } from 'ts-type-forge';
|
|
13
13
|
import {
|
|
14
14
|
convertToReadonlyTransformer,
|
|
15
15
|
type ReadonlyTransformerOptions,
|
|
@@ -3034,6 +3034,380 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
3034
3034
|
});
|
|
3035
3035
|
});
|
|
3036
3036
|
|
|
3037
|
+
describe('Readonly over a union or intersection', () => {
|
|
3038
|
+
test.each(
|
|
3039
|
+
[
|
|
3040
|
+
// A member whose readonly-ness the codemod cannot see stays under the
|
|
3041
|
+
// `Readonly` it was written under.
|
|
3042
|
+
{
|
|
3043
|
+
name: 'An alias in an intersection under Readonly stays under it',
|
|
3044
|
+
source: dedent`
|
|
3045
|
+
type X<A> = Readonly<A & { b: string[] }>;
|
|
3046
|
+
`,
|
|
3047
|
+
expected: dedent`
|
|
3048
|
+
type X<A> = Readonly<A & { b: readonly string[] }>;
|
|
3049
|
+
`,
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
name: 'An alias in a union under Readonly stays under it',
|
|
3053
|
+
source: dedent`
|
|
3054
|
+
type X<A> = Readonly<A | { b: string[] }>;
|
|
3055
|
+
`,
|
|
3056
|
+
expected: dedent`
|
|
3057
|
+
type X<A> = Readonly<A | { b: readonly string[] }>;
|
|
3058
|
+
`,
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
name: 'An intersection of aliases under Readonly is left as written',
|
|
3062
|
+
source: dedent`
|
|
3063
|
+
type X<A, B> = Readonly<A & B>;
|
|
3064
|
+
`,
|
|
3065
|
+
expected: dedent`
|
|
3066
|
+
type X<A, B> = Readonly<A & B>;
|
|
3067
|
+
`,
|
|
3068
|
+
},
|
|
3069
|
+
{
|
|
3070
|
+
name: 'A nested union under Readonly stays under it',
|
|
3071
|
+
source: dedent`
|
|
3072
|
+
type X<A, B> = Readonly<(A | B) & { c: string[] }>;
|
|
3073
|
+
`,
|
|
3074
|
+
expected: dedent`
|
|
3075
|
+
type X<A, B> = Readonly<(A | B) & { c: readonly string[] }>;
|
|
3076
|
+
`,
|
|
3077
|
+
},
|
|
3078
|
+
{
|
|
3079
|
+
name: 'Members readonly on their own are still taken out',
|
|
3080
|
+
source: dedent`
|
|
3081
|
+
type X<A> = Readonly<number | string[] | DeepReadonly<A> | Map<string, A> | { b: string }>;
|
|
3082
|
+
`,
|
|
3083
|
+
expected: dedent`
|
|
3084
|
+
type X<A> = number | readonly string[] | DeepReadonly<A> | ReadonlyMap<string, A> | Readonly<{ b: string }>;
|
|
3085
|
+
`,
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
// `Readonly` would drop the call signature.
|
|
3089
|
+
name: 'A function type is still taken out',
|
|
3090
|
+
source: dedent`
|
|
3091
|
+
type X = Readonly<(() => void) & { b: string }>;
|
|
3092
|
+
`,
|
|
3093
|
+
expected: dedent`
|
|
3094
|
+
type X = (() => void) & Readonly<{ b: string }>;
|
|
3095
|
+
`,
|
|
3096
|
+
},
|
|
3097
|
+
|
|
3098
|
+
// #2231: what replaces `Readonly<…>` keeps the parentheses its
|
|
3099
|
+
// position needs.
|
|
3100
|
+
{
|
|
3101
|
+
name: 'Readonly of an intersection with an alias as an array element under readonly',
|
|
3102
|
+
source: dedent`
|
|
3103
|
+
type A = Readonly<{ a: string }>;
|
|
3104
|
+
export type X = readonly Readonly<A & { b: string }>[];
|
|
3105
|
+
`,
|
|
3106
|
+
expected: dedent`
|
|
3107
|
+
type A = Readonly<{ a: string }>;
|
|
3108
|
+
export type X = readonly Readonly<A & { b: string }>[];
|
|
3109
|
+
`,
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
name: 'Readonly of an intersection with an alias as an array element',
|
|
3113
|
+
source: dedent`
|
|
3114
|
+
type A = Readonly<{ a: string }>;
|
|
3115
|
+
export type X = Readonly<A & { b: string }>[];
|
|
3116
|
+
`,
|
|
3117
|
+
expected: dedent`
|
|
3118
|
+
type A = Readonly<{ a: string }>;
|
|
3119
|
+
export type X = readonly Readonly<A & { b: string }>[];
|
|
3120
|
+
`,
|
|
3121
|
+
},
|
|
3122
|
+
{
|
|
3123
|
+
name: 'Readonly of a union with an alias as an array element',
|
|
3124
|
+
source: dedent`
|
|
3125
|
+
type X<A> = Readonly<A | { b: string }>[];
|
|
3126
|
+
`,
|
|
3127
|
+
expected: dedent`
|
|
3128
|
+
type X<A> = readonly Readonly<A | { b: string }>[];
|
|
3129
|
+
`,
|
|
3130
|
+
},
|
|
3131
|
+
{
|
|
3132
|
+
name: 'Readonly of an intersection with an alias in ReadonlyArray',
|
|
3133
|
+
source: dedent`
|
|
3134
|
+
type X<A> = ReadonlyArray<Readonly<A & { b: string }>>;
|
|
3135
|
+
`,
|
|
3136
|
+
expected: dedent`
|
|
3137
|
+
type X<A> = readonly Readonly<A & { b: string }>[];
|
|
3138
|
+
`,
|
|
3139
|
+
},
|
|
3140
|
+
{
|
|
3141
|
+
name: 'A Readonly that is taken off as an array element',
|
|
3142
|
+
source: dedent`
|
|
3143
|
+
type X = Readonly<number | readonly string[]>[];
|
|
3144
|
+
`,
|
|
3145
|
+
expected: dedent`
|
|
3146
|
+
type X = readonly (number | readonly string[])[];
|
|
3147
|
+
`,
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
name: 'A Readonly that is taken off under readonly',
|
|
3151
|
+
source: dedent`
|
|
3152
|
+
type X = readonly Readonly<number & { b: string }>[];
|
|
3153
|
+
`,
|
|
3154
|
+
expected: dedent`
|
|
3155
|
+
type X = readonly (number & Readonly<{ b: string }>)[];
|
|
3156
|
+
`,
|
|
3157
|
+
},
|
|
3158
|
+
{
|
|
3159
|
+
name: 'A Readonly that is taken off as an intersection member',
|
|
3160
|
+
source: dedent`
|
|
3161
|
+
type X = { a: string } & Readonly<number | { b: string }>;
|
|
3162
|
+
`,
|
|
3163
|
+
expected: dedent`
|
|
3164
|
+
type X = Readonly<{ a: string }> & (number | Readonly<{ b: string }>);
|
|
3165
|
+
`,
|
|
3166
|
+
},
|
|
3167
|
+
|
|
3168
|
+
// #2169: `Readonly<A> & Readonly<B>` is merged only when both are
|
|
3169
|
+
// object types, because `Readonly<string[] & X>` leaves the array
|
|
3170
|
+
// mutable.
|
|
3171
|
+
{
|
|
3172
|
+
name: 'An intersection of Readonly aliases is not merged',
|
|
3173
|
+
source: dedent`
|
|
3174
|
+
type X<A, B> = Readonly<A> & Readonly<B>;
|
|
3175
|
+
`,
|
|
3176
|
+
expected: dedent`
|
|
3177
|
+
type X<A, B> = Readonly<A> & Readonly<B>;
|
|
3178
|
+
`,
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
name: 'A Readonly alias is not merged with a type literal',
|
|
3182
|
+
source: dedent`
|
|
3183
|
+
type X<A> = Readonly<A> & { b: string[] };
|
|
3184
|
+
`,
|
|
3185
|
+
expected: dedent`
|
|
3186
|
+
type X<A> = Readonly<A> & Readonly<{ b: readonly string[] }>;
|
|
3187
|
+
`,
|
|
3188
|
+
},
|
|
3189
|
+
{
|
|
3190
|
+
name: 'Readonly type literals in an intersection are merged',
|
|
3191
|
+
source: dedent`
|
|
3192
|
+
type X = Readonly<{ a: string } | { c: string }> & Readonly<{ b: string }>;
|
|
3193
|
+
`,
|
|
3194
|
+
expected: dedent`
|
|
3195
|
+
type X = Readonly<({ a: string } | { c: string }) & { b: string }>;
|
|
3196
|
+
`,
|
|
3197
|
+
},
|
|
3198
|
+
{
|
|
3199
|
+
// A mapped type over anything but `keyof T` is an object type.
|
|
3200
|
+
name: 'Mapped types in an intersection under Readonly are merged',
|
|
3201
|
+
source: dedent`
|
|
3202
|
+
type X<S, K extends keyof S> = Readonly<{ [k in K]?: S[k] } & { [k in Exclude<keyof S, K>]: S[k] }>;
|
|
3203
|
+
`,
|
|
3204
|
+
expected: dedent`
|
|
3205
|
+
type X<S, K extends keyof S> = Readonly<{ [k in K]?: S[k] } & { [k in Exclude<keyof S, K>]: S[k] }>;
|
|
3206
|
+
`,
|
|
3207
|
+
},
|
|
3208
|
+
{
|
|
3209
|
+
// `{ [k in keyof T]: T[k] }` maps an array `T` to an array.
|
|
3210
|
+
name: 'A homomorphic mapped type in an intersection is not merged',
|
|
3211
|
+
source: dedent`
|
|
3212
|
+
type X<T> = Readonly<{ [k in keyof T]: T[k] }> & Readonly<{ b: string }>;
|
|
3213
|
+
`,
|
|
3214
|
+
expected: dedent`
|
|
3215
|
+
type X<T> = Readonly<{ [k in keyof T]: T[k] }> & Readonly<{ b: string }>;
|
|
3216
|
+
`,
|
|
3217
|
+
},
|
|
3218
|
+
{
|
|
3219
|
+
name: 'A union of Readonly aliases is still merged',
|
|
3220
|
+
source: dedent`
|
|
3221
|
+
type X<A, B> = Readonly<A> | Readonly<B>;
|
|
3222
|
+
`,
|
|
3223
|
+
expected: dedent`
|
|
3224
|
+
type X<A, B> = Readonly<A | B>;
|
|
3225
|
+
`,
|
|
3226
|
+
},
|
|
3227
|
+
].flatMap((c) => [
|
|
3228
|
+
c,
|
|
3229
|
+
// The output is a fixed point: running the codemod again keeps it,
|
|
3230
|
+
// readonly-ness included.
|
|
3231
|
+
{ ...c, name: `${c.name} (fixed point)`, source: c.expected },
|
|
3232
|
+
]),
|
|
3233
|
+
)('$name', testFn);
|
|
3234
|
+
|
|
3235
|
+
// What merging `Readonly` members relies on.
|
|
3236
|
+
test('premises of merging Readonly members', () => {
|
|
3237
|
+
type X = { x: number };
|
|
3238
|
+
|
|
3239
|
+
// `Readonly` distributes over `|`, so merging a union is safe.
|
|
3240
|
+
expectType<Readonly<string[] | X>, readonly string[] | Readonly<X>>('=');
|
|
3241
|
+
|
|
3242
|
+
// It does not over `&` with an array member: the merged form leaves the
|
|
3243
|
+
// array mutable.
|
|
3244
|
+
expectType<Readonly<string[] & X>, readonly string[] & Readonly<X>>('<=');
|
|
3245
|
+
|
|
3246
|
+
expectType<Readonly<string[] & X>, readonly string[] & Readonly<X>>('!=');
|
|
3247
|
+
});
|
|
3248
|
+
});
|
|
3249
|
+
|
|
3250
|
+
describe('Comments in front of a member', () => {
|
|
3251
|
+
// `replaceWithText` overwrites a node from `getStart()`, so a comment in
|
|
3252
|
+
// front of it stays where it is. Text built for the replacement must carry
|
|
3253
|
+
// the comments after that point — those in front of each member and those
|
|
3254
|
+
// in front of the `|` / `&` separating them — and none before it.
|
|
3255
|
+
test.each([
|
|
3256
|
+
{
|
|
3257
|
+
name: 'JSDoc on each member of a union with a leading `|`',
|
|
3258
|
+
source: dedent`
|
|
3259
|
+
export type ProbeStore =
|
|
3260
|
+
/** Kept until the browser is closed. */
|
|
3261
|
+
| 'session'
|
|
3262
|
+
/** Kept until it is forgotten. */
|
|
3263
|
+
| 'device';
|
|
3264
|
+
`,
|
|
3265
|
+
// Prettier itself moves the second comment to the end of the line
|
|
3266
|
+
// before it; here it stays in front of its member.
|
|
3267
|
+
expected: dedent`
|
|
3268
|
+
export type ProbeStore =
|
|
3269
|
+
| /** Kept until the browser is closed. */ 'session'
|
|
3270
|
+
| /** Kept until it is forgotten. */ 'device';
|
|
3271
|
+
`,
|
|
3272
|
+
},
|
|
3273
|
+
{
|
|
3274
|
+
name: 'Line comments on each member of a union with a leading `|`',
|
|
3275
|
+
source: dedent`
|
|
3276
|
+
type T =
|
|
3277
|
+
// first
|
|
3278
|
+
| 'a'
|
|
3279
|
+
// second
|
|
3280
|
+
| 'b'
|
|
3281
|
+
// third
|
|
3282
|
+
| 'c';
|
|
3283
|
+
`,
|
|
3284
|
+
expected: dedent`
|
|
3285
|
+
type T =
|
|
3286
|
+
// first
|
|
3287
|
+
| 'a'
|
|
3288
|
+
// second
|
|
3289
|
+
| 'b'
|
|
3290
|
+
// third
|
|
3291
|
+
| 'c';
|
|
3292
|
+
`,
|
|
3293
|
+
},
|
|
3294
|
+
{
|
|
3295
|
+
name: 'Comments on each member of a union without a leading `|`',
|
|
3296
|
+
source: dedent`
|
|
3297
|
+
type T = /** a */ 'a' | /** b */ 'b';
|
|
3298
|
+
`,
|
|
3299
|
+
expected: dedent`
|
|
3300
|
+
type T = /** a */ 'a' | /** b */ 'b';
|
|
3301
|
+
`,
|
|
3302
|
+
},
|
|
3303
|
+
{
|
|
3304
|
+
name: 'A comment after `|` stays with the member after it',
|
|
3305
|
+
source: dedent`
|
|
3306
|
+
type T = 'a' | // about b
|
|
3307
|
+
'b';
|
|
3308
|
+
`,
|
|
3309
|
+
expected: dedent`
|
|
3310
|
+
type T = 'a' | // about b
|
|
3311
|
+
'b';
|
|
3312
|
+
`,
|
|
3313
|
+
},
|
|
3314
|
+
...toUnionAndIntersectionTestCase({
|
|
3315
|
+
title: (op) => `Comments on members of an ${op} that are converted`,
|
|
3316
|
+
testCase: (op) => ({
|
|
3317
|
+
source: dedent`
|
|
3318
|
+
type T =
|
|
3319
|
+
/** list */
|
|
3320
|
+
${op} number[]
|
|
3321
|
+
/** object */
|
|
3322
|
+
${op} { a: number[] };
|
|
3323
|
+
`,
|
|
3324
|
+
// The comment on the object moves inside the `Readonly` around it.
|
|
3325
|
+
expected: dedent`
|
|
3326
|
+
type T =
|
|
3327
|
+
/** list */
|
|
3328
|
+
${op} readonly number[]
|
|
3329
|
+
${op} Readonly</** object */ { a: readonly number[] }>;
|
|
3330
|
+
`,
|
|
3331
|
+
}),
|
|
3332
|
+
}),
|
|
3333
|
+
...toUnionAndIntersectionTestCase({
|
|
3334
|
+
title: (op) =>
|
|
3335
|
+
`Comments on Readonly members of an ${op} that are merged`,
|
|
3336
|
+
testCase: (op) => ({
|
|
3337
|
+
source: dedent`
|
|
3338
|
+
type T = /** a */ Readonly<{ a: number }> ${op} /** b */ Readonly<{ b: number }>;
|
|
3339
|
+
`,
|
|
3340
|
+
expected: dedent`
|
|
3341
|
+
type T = /** a */ Readonly<{ a: number } ${op} /** b */ { b: number }>;
|
|
3342
|
+
`,
|
|
3343
|
+
}),
|
|
3344
|
+
}),
|
|
3345
|
+
{
|
|
3346
|
+
name: 'Comments on DeepReadonly members that are merged',
|
|
3347
|
+
source: dedent`
|
|
3348
|
+
type T = /** a */ DeepReadonly<{ a: number[] }> | /** b */ DeepReadonly<{ b: string[] }>;
|
|
3349
|
+
`,
|
|
3350
|
+
expected: dedent`
|
|
3351
|
+
type T = /** a */ DeepReadonly<{ a: number[] } | /** b */ { b: string[] }>;
|
|
3352
|
+
`,
|
|
3353
|
+
},
|
|
3354
|
+
{
|
|
3355
|
+
name: 'A comment in front of a union inside parentheses',
|
|
3356
|
+
source: dedent`
|
|
3357
|
+
type T = (/** a */ 'a' | 'b')[];
|
|
3358
|
+
`,
|
|
3359
|
+
expected: dedent`
|
|
3360
|
+
type T = /** a */ readonly ('a' | 'b')[];
|
|
3361
|
+
`,
|
|
3362
|
+
},
|
|
3363
|
+
{
|
|
3364
|
+
name: 'Comments around parentheses that Readonly<T> unwraps',
|
|
3365
|
+
source: dedent`
|
|
3366
|
+
type T = Readonly</** a */ (/** b */ number[])>;
|
|
3367
|
+
`,
|
|
3368
|
+
expected: dedent`
|
|
3369
|
+
type T = /** a */ /** b */ readonly number[];
|
|
3370
|
+
`,
|
|
3371
|
+
},
|
|
3372
|
+
{
|
|
3373
|
+
name: 'Comments around parentheses in a rest element',
|
|
3374
|
+
source: dedent`
|
|
3375
|
+
type T = [string, ...(/** a */ readonly number[])];
|
|
3376
|
+
`,
|
|
3377
|
+
expected: dedent`
|
|
3378
|
+
type T = readonly [string, .../** a */ number[]];
|
|
3379
|
+
`,
|
|
3380
|
+
},
|
|
3381
|
+
{
|
|
3382
|
+
name: 'A comment in front of an array type',
|
|
3383
|
+
source: dedent`
|
|
3384
|
+
type T = /** c */ number[];
|
|
3385
|
+
`,
|
|
3386
|
+
expected: dedent`
|
|
3387
|
+
type T = /** c */ readonly number[];
|
|
3388
|
+
`,
|
|
3389
|
+
},
|
|
3390
|
+
{
|
|
3391
|
+
name: 'A comment in front of a tuple type',
|
|
3392
|
+
source: dedent`
|
|
3393
|
+
type T = /** c */ [number, string];
|
|
3394
|
+
`,
|
|
3395
|
+
expected: dedent`
|
|
3396
|
+
type T = /** c */ readonly [number, string];
|
|
3397
|
+
`,
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
name: 'A comment in front of a type literal',
|
|
3401
|
+
source: dedent`
|
|
3402
|
+
type T = /** c */ { a: number };
|
|
3403
|
+
`,
|
|
3404
|
+
expected: dedent`
|
|
3405
|
+
type T = /** c */ Readonly<{ a: number }>;
|
|
3406
|
+
`,
|
|
3407
|
+
},
|
|
3408
|
+
])('$name', testFn);
|
|
3409
|
+
});
|
|
3410
|
+
|
|
3037
3411
|
describe('Parenthesized types', () => {
|
|
3038
3412
|
test.each([
|
|
3039
3413
|
{
|
|
@@ -3877,7 +4251,7 @@ describe(convertToReadonlyTransformer, () => {
|
|
|
3877
4251
|
type /* Block Comment */ WithComments = Readonly<{
|
|
3878
4252
|
prop1: readonly string[]; // Trailing comment
|
|
3879
4253
|
/* Another block */
|
|
3880
|
-
prop2: /* member type comment */
|
|
4254
|
+
prop2: /* member type comment */ /* member type inner comment */ readonly number[];
|
|
3881
4255
|
/* End block */
|
|
3882
4256
|
}>;
|
|
3883
4257
|
`,
|
|
@@ -4678,11 +5052,10 @@ const toUnionAndIntersectionTestCase = ({
|
|
|
4678
5052
|
|
|
4679
5053
|
describe('idempotency', () => {
|
|
4680
5054
|
test('a union whose members carry leading whitespace reaches a fixed point', () => {
|
|
4681
|
-
//
|
|
4682
|
-
//
|
|
4683
|
-
//
|
|
4684
|
-
//
|
|
4685
|
-
// normalizes it back.
|
|
5055
|
+
// Whitespace in front of a member, joined with a separator that adds its
|
|
5056
|
+
// own, compounds when left alone, and a file with `// prettier-ignore` on a
|
|
5057
|
+
// long union grows by one space per member on every run — nothing
|
|
5058
|
+
// downstream normalizes it back.
|
|
4686
5059
|
const code = [
|
|
4687
5060
|
'// prettier-ignore',
|
|
4688
5061
|
'export type P = ',
|
|
@@ -4700,4 +5073,45 @@ describe('idempotency', () => {
|
|
|
4700
5073
|
|
|
4701
5074
|
expect(twice).toBe(once);
|
|
4702
5075
|
});
|
|
5076
|
+
|
|
5077
|
+
test('comments on union members survive rounds of the codemod and Prettier', async () => {
|
|
5078
|
+
// `fix:codemod` runs the codemod and then Prettier, which takes off the
|
|
5079
|
+
// parentheses the codemod puts around a union, so every run starts again
|
|
5080
|
+
// from the unparenthesized form. A comment lost in one round or copied in
|
|
5081
|
+
// the next shows up only across several of them.
|
|
5082
|
+
const code = dedent`
|
|
5083
|
+
export type ProbeStore =
|
|
5084
|
+
/** Kept until the browser is closed. */
|
|
5085
|
+
| 'session'
|
|
5086
|
+
/** Kept until it is forgotten. */
|
|
5087
|
+
| 'device';
|
|
5088
|
+
`;
|
|
5089
|
+
|
|
5090
|
+
const round = async (src: string): Promise<string> =>
|
|
5091
|
+
prettier.format(
|
|
5092
|
+
transformSourceCode(src, false, [convertToReadonlyTransformer()]),
|
|
5093
|
+
{
|
|
5094
|
+
parser: 'typescript',
|
|
5095
|
+
plugins: [prettierPluginTypeScript, prettierPluginEstree],
|
|
5096
|
+
},
|
|
5097
|
+
);
|
|
5098
|
+
|
|
5099
|
+
const round1 = await round(code);
|
|
5100
|
+
|
|
5101
|
+
const round2 = await round(round1);
|
|
5102
|
+
|
|
5103
|
+
const round3 = await round(round2);
|
|
5104
|
+
|
|
5105
|
+
expect(round1).toBe(
|
|
5106
|
+
`${dedent`
|
|
5107
|
+
export type ProbeStore =
|
|
5108
|
+
/** Kept until the browser is closed. */
|
|
5109
|
+
"session" | /** Kept until it is forgotten. */ "device";
|
|
5110
|
+
`}\n`,
|
|
5111
|
+
);
|
|
5112
|
+
|
|
5113
|
+
expect(round2).toBe(round1);
|
|
5114
|
+
|
|
5115
|
+
expect(round3).toBe(round1);
|
|
5116
|
+
});
|
|
4703
5117
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Arr } from 'ts-data-forge';
|
|
2
2
|
import * as tsm from 'ts-morph';
|
|
3
|
-
import {
|
|
3
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
4
4
|
import { hasDisableNextLineComment } from '../functions/index.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import type { TsMorphTransformer } from './types.mjs';
|
|
6
6
|
|
|
7
7
|
const TRANSFORMER_NAME = 'enable-no-unchecked-indexed-access';
|
|
8
8
|
|
|
@@ -280,27 +280,49 @@ const isIgnoredByComment = (
|
|
|
280
280
|
* the target of an assignment the element type without `undefined`, so the
|
|
281
281
|
* type comparison in {@link collectAssertionPositions} already passes them
|
|
282
282
|
* over. The exceptions handled here are the ones it does widen.
|
|
283
|
+
*
|
|
284
|
+
* The position is that of the outermost parenthesis around the access:
|
|
285
|
+
* `(xs[0]) === undefined` tests the value exactly as `xs[0] === undefined`
|
|
286
|
+
* does, while the `!` still goes right after the access, inside them.
|
|
283
287
|
*/
|
|
284
288
|
const isAssertableReadPosition = (
|
|
285
289
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
286
290
|
node: IndexReadExpression,
|
|
287
291
|
): boolean => {
|
|
288
|
-
const
|
|
292
|
+
const outer = outermostParenthesized(node);
|
|
293
|
+
|
|
294
|
+
const parent = outer.getParent();
|
|
289
295
|
|
|
290
296
|
if (parent === undefined) {
|
|
291
297
|
return false;
|
|
292
298
|
}
|
|
293
299
|
|
|
294
300
|
return (
|
|
295
|
-
!isAlreadyAssertedOrGuarded(node, parent) &&
|
|
296
|
-
!isWriteTarget(
|
|
297
|
-
!isTestedForAbsence(
|
|
301
|
+
!isAlreadyAssertedOrGuarded(node, outer, parent) &&
|
|
302
|
+
!isWriteTarget(outer, parent) &&
|
|
303
|
+
!isTestedForAbsence(outer, parent)
|
|
298
304
|
);
|
|
299
305
|
};
|
|
300
306
|
|
|
301
|
-
/** `
|
|
307
|
+
/** `node`, or the outermost of the parentheses written around it. */
|
|
308
|
+
const outermostParenthesized = (node: tsm.Node): tsm.Node => {
|
|
309
|
+
const parent = node.getParent();
|
|
310
|
+
|
|
311
|
+
return parent?.isKind(tsm.SyntaxKind.ParenthesizedExpression) === true
|
|
312
|
+
? outermostParenthesized(parent)
|
|
313
|
+
: node;
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* `xs[0]!`, `xs[0] as T`, `<T>xs[0]`, `xs[0]?.foo`, `xs[0]?.()`, `xs[0]?.[1]`.
|
|
318
|
+
*
|
|
319
|
+
* `outer` is `node` with its parentheses (see
|
|
320
|
+
* {@link outermostParenthesized}); a chain continues from the access itself
|
|
321
|
+
* but is guarded from the outside of the parentheses (`(xs[0])?.foo`).
|
|
322
|
+
*/
|
|
302
323
|
const isAlreadyAssertedOrGuarded = (
|
|
303
324
|
node: tsm.Node,
|
|
325
|
+
outer: tsm.Node,
|
|
304
326
|
parent: tsm.Node,
|
|
305
327
|
): boolean => {
|
|
306
328
|
if (
|
|
@@ -322,7 +344,7 @@ const isAlreadyAssertedOrGuarded = (
|
|
|
322
344
|
parent.isKind(tsm.SyntaxKind.ElementAccessExpression) ||
|
|
323
345
|
parent.isKind(tsm.SyntaxKind.CallExpression)) &&
|
|
324
346
|
parent.hasQuestionDotToken() &&
|
|
325
|
-
parent.getExpression() ===
|
|
347
|
+
parent.getExpression() === outer
|
|
326
348
|
);
|
|
327
349
|
};
|
|
328
350
|
|
|
@@ -391,6 +391,96 @@ describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
|
391
391
|
])('$name', testFn);
|
|
392
392
|
});
|
|
393
393
|
|
|
394
|
+
describe('positions judged through parentheses and type wrappers', () => {
|
|
395
|
+
test.each([
|
|
396
|
+
{
|
|
397
|
+
name: 'a parenthesized read is still asserted',
|
|
398
|
+
source: 'console.log((xs[0]).toFixed());',
|
|
399
|
+
expected: 'console.log((xs[0]!).toFixed());',
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
name: 'parenthesized equality against undefined',
|
|
403
|
+
source: 'console.log((xs[0]) === undefined);',
|
|
404
|
+
expected: 'console.log((xs[0]) === undefined);',
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: 'parenthesized nullish coalescing',
|
|
408
|
+
source: 'console.log((xs[0]) ?? 0);',
|
|
409
|
+
expected: 'console.log((xs[0]) ?? 0);',
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'parenthesized optional property access',
|
|
413
|
+
source: 'console.log((xs[0])?.toFixed());',
|
|
414
|
+
expected: 'console.log((xs[0])?.toFixed());',
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
name: 'parenthesized if condition',
|
|
418
|
+
source: 'if ((xs[0])) { console.log(1); }',
|
|
419
|
+
expected: 'if ((xs[0])) { console.log(1); }',
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
name: 'parenthesized typeof operand',
|
|
423
|
+
source: 'console.log(typeof (xs[0]));',
|
|
424
|
+
expected: 'console.log(typeof (xs[0]));',
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'parenthesized negation operand',
|
|
428
|
+
source: 'console.log(!((xs[0])));',
|
|
429
|
+
expected: 'console.log(!((xs[0])));',
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: 'parenthesized existing non-null assertion',
|
|
433
|
+
source: 'console.log((xs[0])!.toFixed());',
|
|
434
|
+
expected: 'console.log((xs[0])!.toFixed());',
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'parenthesized type assertion',
|
|
438
|
+
source: 'console.log(((xs[0]) as number).toFixed());',
|
|
439
|
+
expected: 'console.log(((xs[0]) as number).toFixed());',
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: 'satisfies a type without undefined, which needs the assertion',
|
|
443
|
+
source: 'console.log(xs[0] satisfies number);',
|
|
444
|
+
expected: 'console.log(xs[0]! satisfies number);',
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: 'parenthesized satisfies a type without undefined',
|
|
448
|
+
source: 'console.log((xs[0]) satisfies number);',
|
|
449
|
+
expected: 'console.log((xs[0]!) satisfies number);',
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: 'satisfies a type with undefined, where the assertion is harmless',
|
|
453
|
+
source: 'console.log(xs[0] satisfies number | undefined);',
|
|
454
|
+
expected: 'console.log(xs[0]! satisfies number | undefined);',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
name: 'parenthesized satisfies a type with undefined',
|
|
458
|
+
source: 'console.log((xs[0]) satisfies number | undefined);',
|
|
459
|
+
expected: 'console.log((xs[0]!) satisfies number | undefined);',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: 'parenthesized compound assignment target',
|
|
463
|
+
source: '(mut_ys[0]) += 1;',
|
|
464
|
+
expected: '(mut_ys[0]) += 1;',
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: 'parenthesized assignment target',
|
|
468
|
+
source: '(mut_ys[0]) = 1;',
|
|
469
|
+
expected: '(mut_ys[0]) = 1;',
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'parenthesized delete target',
|
|
473
|
+
source: "delete (rec['a']);",
|
|
474
|
+
expected: "delete (rec['a']);",
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'parenthesized for-of loop target',
|
|
478
|
+
source: 'for ((mut_ys[0]) of xs) { break; }',
|
|
479
|
+
expected: 'for ((mut_ys[0]) of xs) { break; }',
|
|
480
|
+
},
|
|
481
|
+
])('$name', testFn);
|
|
482
|
+
});
|
|
483
|
+
|
|
394
484
|
describe('applyLevel: "avoidWhereUndefinedIsAllowed"', () => {
|
|
395
485
|
test.each([
|
|
396
486
|
{
|
|
@@ -541,6 +631,27 @@ describe(enableNoUncheckedIndexedAccessTransformer, () => {
|
|
|
541
631
|
);
|
|
542
632
|
});
|
|
543
633
|
|
|
634
|
+
test('an access under `satisfies` or in parentheses compiles once transformed', () => {
|
|
635
|
+
const source = dedent`
|
|
636
|
+
declare const xs: readonly number[];
|
|
637
|
+
|
|
638
|
+
export const a = xs[0] satisfies number;
|
|
639
|
+
|
|
640
|
+
export const b = (xs[0]) satisfies number;
|
|
641
|
+
|
|
642
|
+
export const c = (xs[0]).toFixed();
|
|
643
|
+
|
|
644
|
+
export const d = (xs[0]) === undefined ? 0 : (xs[0]) ?? 1;
|
|
645
|
+
`;
|
|
646
|
+
|
|
647
|
+
expect(diagnosticsUnderTheOption(source).length).toBeGreaterThan(0);
|
|
648
|
+
|
|
649
|
+
assert.deepStrictEqual(
|
|
650
|
+
diagnosticsUnderTheOption(transformInProject(source)),
|
|
651
|
+
[],
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
|
|
544
655
|
test('array destructuring is left alone, since `!` has nowhere to go there', () => {
|
|
545
656
|
const source = dedent`
|
|
546
657
|
declare const xs: readonly number[];
|