ts-codemod-lib 1.4.0 → 1.4.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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-template-curly-in-string */
1
2
  /* eslint-disable import-x/namespace */
2
3
  /* eslint-disable tree-shakable/import-star */
3
4
  /* eslint-disable vitest/expect-expect */
@@ -26,7 +27,7 @@ const testFn = async ({
26
27
  }
27
28
 
28
29
  const transformed = await formatter(
29
- transformSourceCode(source, false, [appendAsConstTransformer()]),
30
+ transformSourceCode(source, false, [appendAsConstTransformer({ debug })]),
30
31
  );
31
32
 
32
33
  const expectedFormatted = await formatter(expected);
@@ -41,474 +42,654 @@ const formatter = (code: string): Promise<string> =>
41
42
  });
42
43
 
43
44
  describe(appendAsConstTransformer, () => {
44
- test.each([
45
- {
46
- name: 'ArrayLiteralExpression - simple',
47
- source: 'const foo = [1, 2, 3];',
48
- expected: 'const foo = [1, 2, 3] as const;',
49
- },
50
- {
51
- name: 'ArrayLiteralExpression - empty',
52
- source: 'const emptyArray = [];',
53
- expected: 'const emptyArray = [] as const;',
54
- },
55
- {
56
- name: 'ArrayLiteralExpression - nested',
57
- source: 'const nested = [1, [2, 3], 4];',
58
- expected: 'const nested = [1, [2, 3], 4] as const;',
59
- },
60
- {
61
- name: 'ArrayLiteralExpression - with strings',
62
- source: "const strArray = ['a', 'b', 'c'];",
63
- expected: "const strArray = ['a', 'b', 'c'] as const;",
64
- },
65
- {
66
- name: 'ArrayLiteralExpression - mixed types',
67
- source: "const mixed = [1, 'a', true, null];",
68
- expected: "const mixed = [1, 'a', true, null] as const;",
69
- },
70
- {
71
- name: 'ObjectLiteralExpression - simple',
72
- source: 'const obj = { a: 1, b: 2 };',
73
- expected: 'const obj = { a: 1, b: 2 } as const;',
74
- },
75
- {
76
- name: 'ObjectLiteralExpression - empty',
77
- source: 'const emptyObj = {};',
78
- expected: 'const emptyObj = {} as const;',
79
- },
80
- {
81
- name: 'ObjectLiteralExpression - nested',
82
- source: 'const nestedObj = { a: 1, b: { c: 2 } };',
83
- expected: 'const nestedObj = { a: 1, b: { c: 2 } } as const;',
84
- },
85
- {
86
- name: 'ObjectLiteralExpression - with array',
87
- source: 'const objWithArray = { a: 1, b: [1, 2, 3] };',
88
- expected: 'const objWithArray = { a: 1, b: [1, 2, 3] } as const;',
89
- },
90
- {
91
- name: 'Mixed - array with object',
92
- source: 'const arrayWithObj = [{ a: 1 }, { b: 2 }];',
93
- expected: 'const arrayWithObj = [{ a: 1 }, { b: 2 }] as const;',
94
- },
95
- {
96
- name: 'Variable declaration with multiple variables',
97
- source: 'const a = [1, 2], b = { c: 3 };',
98
- expected: 'const a = [1, 2] as const, b = { c: 3 } as const;',
99
- },
100
- {
101
- name: 'Variable declaration with mut_ keyword',
102
- source: 'const mut_foo = [1, 2, 3];',
103
- expected: 'const mut_foo = [1, 2, 3];',
104
- },
105
- {
106
- name: 'Variable declaration with multiple variables',
107
- source: 'const mut_a = [1, 2], mut_b = { c: 3 };',
108
- expected: 'const mut_a = [1, 2], mut_b = { c: 3 };',
109
- },
110
- {
111
- name: 'Function return',
112
- source: 'function foo() { return [1, 2, 3]; }',
113
- expected: 'function foo() { return [1, 2, 3] as const; }',
114
- },
115
- {
116
- name: 'Arrow function return',
117
- source: 'const foo = () => ({ a: 1, b: 2 });',
118
- expected: 'const foo = () => ({ a: 1, b: 2 } as const);',
119
- },
120
- {
121
- name: 'Array in function parameter',
122
- source: 'function foo(a = [1, 2]) { return a; }',
123
- expected: 'function foo(a = [1, 2] as const) { return a; }',
124
- },
125
- {
126
- name: 'Object in function parameter',
127
- source: 'function foo(a = { b: 1 }) { return a; }',
128
- expected: 'function foo(a = { b: 1 } as const) { return a; }',
129
- },
130
- {
131
- name: 'Object in function call',
132
- source: 'const a = foo({ b: 1 });',
133
- expected: 'const a = foo({ b: 1 });',
134
- },
135
- {
136
- name: 'Skip ArrayLiteralExpression with disable comment',
137
- source: dedent`
138
- // transformer-ignore-next-line
139
- const skippedArray = [1, 2, 3];
140
- `,
141
- expected: dedent`
142
- // transformer-ignore-next-line
143
- const skippedArray = [1, 2, 3];
144
- `,
145
- },
146
- {
147
- name: 'Skip ObjectLiteralExpression with disable comment',
148
- source: dedent`
149
- // transformer-ignore-next-line
150
- const skippedObject = { a: 1, b: "hello" };
151
- `,
152
- expected: dedent`
153
- // transformer-ignore-next-line
154
- const skippedObject = { a: 1, b: "hello" };
155
- `,
156
- },
157
- {
158
- name: 'Disable comment only affects the immediate next line',
159
- source: dedent`
160
- const transformedArray = [10, 20]; // This should be transformed
161
- // transformer-ignore-next-line
162
- const skippedObject = { x: true }; // This should be skipped
163
- const transformedObject = { y: false }; // This should be transformed
164
- `,
165
- expected: dedent`
166
- const transformedArray = [10, 20] as const; // This should be transformed
167
- // transformer-ignore-next-line
168
- const skippedObject = { x: true }; // This should be skipped
169
- const transformedObject = { y: false } as const; // This should be transformed
170
- `,
171
- },
172
- {
173
- name: 'File scope transformer-ignore',
174
- source: dedent`
175
- /* transformer-ignore */
176
- const transformedArray = [10, 20]; // This should be skipped
177
- const skippedObject = { x: true }; // This should be skipped
178
- const transformedObject = { y: false }; // This should be skipped
179
- `,
180
- expected: dedent`
181
- /* transformer-ignore */
182
- const transformedArray = [10, 20]; // This should be skipped
183
- const skippedObject = { x: true }; // This should be skipped
184
- const transformedObject = { y: false }; // This should be skipped
185
- `,
186
- },
187
- {
188
- name: 'Transformer-specific ignore comment (next line)',
189
- source: dedent`
190
- const a = [1, 2, 3];
191
- // transformer-ignore-next-line append-as-const
192
- const b = [4, 5, 6];
193
- const c = [7, 8, 9];
194
- `,
195
- expected: dedent`
196
- const a = [1, 2, 3] as const;
197
- // transformer-ignore-next-line append-as-const
198
- const b = [4, 5, 6];
199
- const c = [7, 8, 9] as const;
200
- `,
201
- },
202
- {
203
- name: 'Transformer-specific ignore comment (file scope)',
204
- source: dedent`
205
- /* transformer-ignore append-as-const */
206
- const a = [1, 2, 3];
207
- const b = { x: 1 };
208
- `,
209
- expected: dedent`
210
- /* transformer-ignore append-as-const */
211
- const a = [1, 2, 3];
212
- const b = { x: 1 };
213
- `,
214
- },
215
- {
216
- name: 'Multiple transformers in ignore comment',
217
- source: dedent`
218
- const a = [1, 2, 3];
219
- // transformer-ignore-next-line append-as-const, replace-any-with-unknown
220
- const b = [4, 5, 6];
221
- const c = [7, 8, 9];
222
- `,
223
- expected: dedent`
224
- const a = [1, 2, 3] as const;
225
- // transformer-ignore-next-line append-as-const, replace-any-with-unknown
226
- const b = [4, 5, 6];
227
- const c = [7, 8, 9] as const;
228
- `,
229
- },
230
- {
231
- name: 'Wrong transformer name should not affect',
232
- source: dedent`
233
- const a = [1, 2, 3];
234
- // transformer-ignore-next-line some-other-transformer
235
- const b = [4, 5, 6];
236
- const c = [7, 8, 9];
237
- `,
238
- expected: dedent`
239
- const a = [1, 2, 3] as const;
240
- // transformer-ignore-next-line some-other-transformer
241
- const b = [4, 5, 6] as const;
242
- const c = [7, 8, 9] as const;
243
- `,
244
- },
245
- // Cases where the transformer doesn't modify the code
246
- {
247
- name: 'Primitive literal - number',
248
- source: 'const num = 42;',
249
- expected: 'const num = 42;',
250
- },
251
- {
252
- name: 'Primitive literal - string',
253
- source: "const str = 'hello';",
254
- expected: "const str = 'hello';",
255
- },
256
- {
257
- name: 'Primitive literal - boolean',
258
- source: 'const bool = true;',
259
- expected: 'const bool = true;',
260
- },
261
- {
262
- name: 'Primitive literal - null',
263
- source: 'const n = null;',
264
- expected: 'const n = null;',
265
- },
266
- {
267
- name: 'Primitive literal - undefined',
268
- source: 'const u = undefined;',
269
- expected: 'const u = undefined;',
270
- },
271
- {
272
- name: 'Variable reference',
273
- source: 'const a = 1; const b = a;',
274
- expected: 'const a = 1; const b = a;',
275
- },
276
- {
277
- name: 'Function call',
278
- source: 'const result = foo();',
279
- expected: 'const result = foo();',
280
- },
281
- {
282
- name: 'Method call',
283
- source: 'const result = obj.method();',
284
- expected: 'const result = obj.method();',
285
- },
286
- {
287
- name: 'Binary expression',
288
- source: 'const sum = a + b;',
289
- expected: 'const sum = a + b;',
290
- },
291
- {
292
- name: 'Template literal',
293
- // eslint-disable-next-line no-template-curly-in-string
294
- source: 'const greeting = `Hello ${name}`;',
295
- // eslint-disable-next-line no-template-curly-in-string
296
- expected: 'const greeting = `Hello ${name}`;',
297
- },
298
- {
299
- name: 'New expression',
300
- source: 'const date = new Date();',
301
- expected: 'const date = new Date();',
302
- },
303
- {
304
- name: 'Already has as const - array',
305
- source: 'const arr = [1, 2, 3] as const;',
306
- expected: 'const arr = [1, 2, 3] as const;',
307
- },
308
- {
309
- name: 'Already has as const - object',
310
- source: 'const obj = { a: 1 } as const;',
311
- expected: 'const obj = { a: 1 } as const;',
312
- },
313
- {
314
- name: 'Primitive literal - number',
315
- source: 'const num = 42;',
316
- expected: 'const num = 42;',
317
- },
318
- {
319
- name: 'Class declaration',
320
- source: 'class MyClass { prop = 1; }',
321
- expected: 'class MyClass { prop = 1; }',
322
- },
45
+ describe('Primitive types (should not add as const)', () => {
46
+ test.each([
47
+ {
48
+ name: 'number literal',
49
+ source: 'const num = 42;',
50
+ expected: 'const num = 42;',
51
+ },
52
+ {
53
+ name: 'BigInt literal',
54
+ source: 'const bigint = 100n as const;',
55
+ expected: 'const bigint = 100n;',
56
+ },
57
+ {
58
+ name: 'string literal',
59
+ source: "const str = 'hello';",
60
+ expected: "const str = 'hello';",
61
+ },
62
+ {
63
+ name: 'boolean literal',
64
+ source: 'const bool = true;',
65
+ expected: 'const bool = true;',
66
+ },
67
+ {
68
+ name: 'null',
69
+ source: 'const n = null;',
70
+ expected: 'const n = null;',
71
+ },
72
+ {
73
+ name: 'undefined',
74
+ source: 'const u = undefined;',
75
+ expected: 'const u = undefined;',
76
+ },
77
+ {
78
+ name: 'constant declaration with primitive number initializer in parentheses',
79
+ source: 'const foo = (1) as const;',
80
+ expected: 'const foo = 1;',
81
+ },
82
+ ])('$name', testFn);
83
+ });
84
+
85
+ describe('Template literals', () => {
86
+ test.each([
87
+ {
88
+ name: 'template expression without as const (should add)',
89
+ source: 'const greeting = `Hello ${name}`;',
90
+ expected: 'const greeting = `Hello ${name}` as const;',
91
+ },
92
+ {
93
+ name: 'template expression with as const (should keep)',
94
+ source: 'const path = `/${variable}` as const;',
95
+ expected: 'const path = `/${variable}` as const;',
96
+ },
97
+ {
98
+ name: 'no substitution template literal without as const (should keep)',
99
+ source: 'const greeting = `hello`;',
100
+ expected: 'const greeting = `hello`;',
101
+ },
102
+ {
103
+ name: 'no substitution template literal with as const (should remove)',
104
+ source: 'const greeting = `hello` as const;',
105
+ expected: 'const greeting = `hello`;',
106
+ },
107
+ {
108
+ source: dedent`
109
+ const x = "aaa";
110
+ const foo = \`\${x}bbb\`;
111
+ `,
112
+ expected: dedent`
113
+ const x = "aaa";
114
+ const foo = \`\${x}bbb\` as const;
115
+ `,
116
+ },
117
+ ])('$name', testFn);
118
+ });
119
+
120
+ describe('Basic array and object transformations', () => {
121
+ test.each([
122
+ {
123
+ name: 'simple array',
124
+ source: 'const foo = [1, 2, 3];',
125
+ expected: 'const foo = [1, 2, 3] as const;',
126
+ },
127
+ {
128
+ name: 'empty array',
129
+ source: 'const emptyArray = [];',
130
+ expected: 'const emptyArray = [] as const;',
131
+ },
132
+ {
133
+ name: 'nested array',
134
+ source: 'const nested = [1, [2, 3], 4];',
135
+ expected: 'const nested = [1, [2, 3], 4] as const;',
136
+ },
137
+ {
138
+ name: 'nested array',
139
+ source: 'const foo = [[1] as const] as const;',
140
+ expected: 'const foo = [[1]] as const;',
141
+ },
142
+ {
143
+ name: 'nested array',
144
+ source: 'const nested = [1, [2, 3] as const, 4];',
145
+ expected: 'const nested = [1, [2, 3], 4] as const;',
146
+ },
147
+ {
148
+ name: 'should remove nested as const from array literal',
149
+ source: 'const arr = [[1, 2] as const, [3, 4]] as const;',
150
+ expected: 'const arr = [[1, 2], [3, 4]] as const;',
151
+ },
152
+ {
153
+ name: 'array with strings',
154
+ source: "const strArray = ['a', 'b', 'c'];",
155
+ expected: "const strArray = ['a', 'b', 'c'] as const;",
156
+ },
157
+ {
158
+ name: 'array with mixed types',
159
+ source: "const mixed = [1, 'a', true, null];",
160
+ expected: "const mixed = [1, 'a', true, null] as const;",
161
+ },
162
+ {
163
+ name: 'should remove as const from boolean literal inside array and add as const to top level',
164
+ source: 'const arr = [true as const, false];',
165
+ expected: 'const arr = [true, false] as const;',
166
+ },
167
+ {
168
+ name: 'simple object',
169
+ source: 'const obj = { a: 1, b: 2 };',
170
+ expected: 'const obj = { a: 1, b: 2 } as const;',
171
+ },
172
+ {
173
+ name: 'empty object',
174
+ source: 'const emptyObj = {};',
175
+ expected: 'const emptyObj = {} as const;',
176
+ },
177
+ {
178
+ name: 'nested object',
179
+ source: 'const nestedObj = { a: 1, b: { c: 2 } };',
180
+ expected: 'const nestedObj = { a: 1, b: { c: 2 } } as const;',
181
+ },
182
+ {
183
+ name: 'object with array',
184
+ source: 'const objWithArray = { a: 1, b: [1, 2, 3] };',
185
+ expected: 'const objWithArray = { a: 1, b: [1, 2, 3] } as const;',
186
+ },
187
+ {
188
+ name: 'array with objects',
189
+ source: 'const arrayWithObj = [{ a: 1 }, { b: 2 }];',
190
+ expected: 'const arrayWithObj = [{ a: 1 }, { b: 2 }] as const;',
191
+ },
192
+ {
193
+ name: 'should remove as const from string literal inside object and add as const to top level',
194
+ source: 'const obj = { key: "value" as const };',
195
+ expected: 'const obj = { key: "value" } as const;',
196
+ },
197
+ {
198
+ name: 'multiple variable declarations',
199
+ source: 'const a = [1, 2], b = { c: 3 };',
200
+ expected: 'const a = [1, 2] as const, b = { c: 3 } as const;',
201
+ },
202
+ {
203
+ name: 'should remove nested as const from object literal',
204
+ source: 'const obj = { a: { b: 1 } as const, c: 2 } as const;',
205
+ expected: 'const obj = { a: { b: 1 }, c: 2 } as const;',
206
+ },
207
+ {
208
+ name: 'should remove nested as const from mixed literals',
209
+ source: 'let mixed = [{ a: 1 } as const, [2] as const] as const;',
210
+ expected: 'let mixed = [{ a: 1 }, [2]] as const;',
211
+ },
212
+ {
213
+ name: 'should remove deeply nested as const',
214
+ source: 'let deep = [[{ x: 1 as const }] as const] as const;',
215
+ expected: 'let deep = [[{ x: 1 }]] as const;',
216
+ },
217
+ {
218
+ name: 'should remove multiple primitive as const inside literals and add as const to top level',
219
+ source:
220
+ 'const mixed = [1 as const, { val: "str" as const }, false as const];',
221
+ expected: 'const mixed = [1, { val: "str" }, false] as const;',
222
+ },
223
+ {
224
+ name: 'should remove primitive as const even if parent already has as const',
225
+ source: 'const arr = [1 as const, 2] as const;',
226
+ expected: 'const arr = [1, 2] as const;',
227
+ },
228
+ {
229
+ name: 'should remove primitive as const in nested object and add as const to top level',
230
+ source: 'const obj = { data: { value: 123 as const } };',
231
+ expected: 'const obj = { data: { value: 123 } } as const;',
232
+ },
233
+ ])('$name', testFn);
234
+ });
235
+
236
+ describe('Spread syntax', () => {
237
+ test.each([
238
+ {
239
+ name: 'simple spread in array',
240
+ source: dedent`
241
+ const flag = true as boolean;
242
+ const a = [0, ...[{ x: 1 }, 2]];
243
+ `,
244
+ expected: dedent`
245
+ const flag = true as boolean;
246
+ const a = [0, ...[{ x: 1 }, 2]] as const;
247
+ `,
248
+ },
249
+ {
250
+ name: 'simple spread in array with as const inside',
251
+ source: dedent`
252
+ const flag = true as boolean;
253
+ const a = [0, ...([{ x: 1 } as const, 2] as const)] as const;
254
+ `,
255
+ expected: dedent`
256
+ const flag = true as boolean;
257
+ const a = [0, ...[{ x: 1 }, 2]] as const;
258
+ `,
259
+ },
260
+ {
261
+ name: 'simple spread in array with ternary expression inside',
262
+ source: dedent`
263
+ const flag = true as boolean;
264
+ const b = [0, ...(flag ? ([1, 2]) : ([]))];
265
+ `,
266
+ expected: dedent`
267
+ const flag = true as boolean;
268
+ const b = [0, ...(flag ? ([1, 2] as const) : ([] as const))] as const;
269
+ `,
270
+ },
271
+ {
272
+ name: 'simple spread in array with ternary expression inside',
273
+ source: dedent`
274
+ const flag = true as boolean;
275
+ const c = [0, ...([1, flag ? [2] : [3]] as const)] as const;
276
+ `,
277
+ expected: dedent`
278
+ const flag = true as boolean;
279
+ const c = [0, ...([1, flag ? [2] as const : [3] as const])] as const;
280
+ `,
281
+ },
282
+ {
283
+ name: 'Nested as const inside spread operator with conditional should be kept',
284
+ source: dedent`
285
+ const flag = true as boolean;
323
286
 
324
- // Tests for removing nested as const
325
- {
326
- name: 'should remove nested as const from array literal',
327
- source: 'const arr = [[1, 2] as const, [3, 4]] as const;',
328
- expected: 'const arr = [[1, 2], [3, 4]] as const;',
329
- },
330
- {
331
- name: 'should remove nested as const from object literal',
332
- source: 'const obj = { a: { b: 1 } as const, c: 2 } as const;',
333
- expected: 'const obj = { a: { b: 1 }, c: 2 } as const;',
334
- },
335
- {
336
- name: 'should remove nested as const from mixed literals',
337
- source: 'const mixed = [{ a: 1 } as const, [2] as const] as const;',
338
- expected: 'const mixed = [{ a: 1 }, [2]] as const;',
339
- },
340
- {
341
- name: 'should remove deeply nested as const',
342
- source: 'const deep = [[{ x: 1 as const }] as const] as const;',
343
- expected: 'const deep = [[{ x: 1 }]] as const;',
344
- },
287
+ type Elem =
288
+ | Readonly<{ a: 'str0' }>
289
+ | Readonly<{ b: 'str1' }>
290
+ | Readonly<{ c: 'str2' }>;
345
291
 
346
- // Tests for removing as const from primitives within literals
347
- {
348
- name: 'should remove as const from number literal inside array and add as const to top level',
349
- source: 'const arr = [1 as const, 2];',
350
- expected: 'const arr = [1, 2] as const;',
351
- },
352
- {
353
- name: 'should remove as const from string literal inside object and add as const to top level',
354
- source: 'const obj = { key: "value" as const };',
355
- expected: 'const obj = { key: "value" } as const;',
356
- },
357
- {
358
- name: 'should remove as const from boolean literal inside array and add as const to top level',
359
- source: 'const arr = [true as const, false];',
360
- expected: 'const arr = [true, false] as const;',
361
- },
362
- {
363
- name: 'should remove multiple primitive as const inside literals and add as const to top level',
364
- source:
365
- 'const mixed = [1 as const, { val: "str" as const }, false as const];',
366
- expected: 'const mixed = [1, { val: "str" }, false] as const;',
367
- },
368
- {
369
- name: 'should remove primitive as const even if parent already has as const',
370
- source: 'const arr = [1 as const, 2] as const;',
371
- expected: 'const arr = [1, 2] as const;',
372
- },
373
- {
374
- name: 'should remove primitive as const in nested object and add as const to top level',
375
- source: 'const obj = { data: { value: 123 as const } };',
376
- expected: 'const obj = { data: { value: 123 } } as const;',
377
- },
292
+ const a = [
293
+ { a: 'str0' },
294
+ ...(flag ? ([{ b: 'str1' }, { c: 'str2' }] as const) : []),
295
+ ] as const satisfies readonly Elem[];
296
+ `,
297
+ expected: dedent`
298
+ const flag = true as boolean;
378
299
 
379
- {
380
- name: 'arrow function',
381
- source: dedent`
382
- export const some = <const S,>(value: S): Some<S> =>
383
- ({
384
- type: SomeTypeSymbol,
385
- value
386
- }) as const;
387
- `,
388
- expected: dedent`
389
- export const some = <const S,>(value: S): Some<S> =>
390
- ({
391
- type: SomeTypeSymbol,
392
- value
393
- }) as const;
394
- `,
395
- },
396
- {
397
- name: 'Nested as const inside spread operator with conditional should be kept',
398
- source: dedent`
399
- const flag = true as boolean;
300
+ type Elem =
301
+ | Readonly<{ a: 'str0' }>
302
+ | Readonly<{ b: 'str1' }>
303
+ | Readonly<{ c: 'str2' }>;
400
304
 
401
- type Elem =
402
- | Readonly<{ a: 'str0' }>
403
- | Readonly<{ b: 'str1' }>
404
- | Readonly<{ c: 'str2' }>;
305
+ const a = [
306
+ { a: 'str0' },
307
+ ...(flag ? ([{ b: 'str1' }, { c: 'str2' }] as const) : [] as const),
308
+ ] as const satisfies readonly Elem[];
309
+ `,
310
+ },
311
+ {
312
+ name: 'Spread with conditional without as const should add as const to inner array',
313
+ source: dedent`
314
+ const flag = true as boolean;
405
315
 
406
- const a = [
407
- { a: 'str0' },
408
- ...(flag ? ([{ b: 'str1' }, { c: 'str2' }] as const) : []),
409
- ] as const satisfies readonly Elem[];
410
- `,
411
- expected: dedent`
412
- const flag = true as boolean;
316
+ type Elem =
317
+ | Readonly<{ a: 'str0' }>
318
+ | Readonly<{ b: 'str1' }>
319
+ | Readonly<{ c: 'str2' }>;
413
320
 
414
- type Elem =
415
- | Readonly<{ a: 'str0' }>
416
- | Readonly<{ b: 'str1' }>
417
- | Readonly<{ c: 'str2' }>;
321
+ const a = [
322
+ { a: 'str0' },
323
+ ...(flag ? [{ b: 'str1' }, { c: 'str2' }] : []),
324
+ ] as const satisfies readonly Elem[];
325
+ `,
326
+ expected: dedent`
327
+ const flag = true as boolean;
418
328
 
419
- const a = [
420
- { a: 'str0' },
421
- ...(flag ? ([{ b: 'str1' }, { c: 'str2' }] as const) : []),
422
- ] as const satisfies readonly Elem[];
423
- `,
424
- },
425
- {
426
- name: 'Spread with conditional without as const should add as const to inner array',
427
- source: dedent`
428
- const flag = true as boolean;
329
+ type Elem =
330
+ | Readonly<{ a: 'str0' }>
331
+ | Readonly<{ b: 'str1' }>
332
+ | Readonly<{ c: 'str2' }>;
429
333
 
430
- type Elem =
431
- | Readonly<{ a: 'str0' }>
432
- | Readonly<{ b: 'str1' }>
433
- | Readonly<{ c: 'str2' }>;
334
+ const a = [
335
+ { a: 'str0' },
336
+ ...(flag ? [{ b: 'str1' }, { c: 'str2' }] as const : [] as const),
337
+ ] as const satisfies readonly Elem[];
338
+ `,
339
+ },
340
+ {
341
+ name: 'Simple spread without conditional should remove inner as const',
342
+ source: dedent`
343
+ const a = [
344
+ 1,
345
+ ...[2, 3] as const,
346
+ ] as const;
347
+ `,
348
+ expected: dedent`
349
+ const a = [
350
+ 1,
351
+ ...[2, 3],
352
+ ] as const;
353
+ `,
354
+ },
355
+ {
356
+ name: 'Multiple simple spreads should remove inner as const',
357
+ source: dedent`
358
+ const c = [
359
+ ...[1, 2] as const,
360
+ ...[3, 4] as const,
361
+ ] as const;
362
+ `,
363
+ expected: dedent`
364
+ const c = [
365
+ ...[1, 2],
366
+ ...[3, 4],
367
+ ] as const;
368
+ `,
369
+ },
370
+ {
371
+ name: 'Nested as const inside spread operator without conditional statement should be removed',
372
+ source: dedent`
373
+ const flag = true as boolean;
434
374
 
435
- const a = [
436
- { a: 'str0' },
437
- ...(flag ? [{ b: 'str1' }, { c: 'str2' }] : []),
438
- ] as const satisfies readonly Elem[];
439
- `,
440
- expected: dedent`
441
- const flag = true as boolean;
375
+ type Elem =
376
+ | Readonly<{ a: 'str0' }>
377
+ | Readonly<{ b: 'str1' }>
378
+ | Readonly<{ c: 'str2' }>;
442
379
 
443
- type Elem =
444
- | Readonly<{ a: 'str0' }>
445
- | Readonly<{ b: 'str1' }>
446
- | Readonly<{ c: 'str2' }>;
380
+ const a = [
381
+ { a: 'str0' },
382
+ ...([{ b: 'str1' }, { c: 'str2' }] as const),
383
+ ] as const satisfies readonly Elem[];
384
+ `,
385
+ expected: dedent`
386
+ const flag = true as boolean;
447
387
 
448
- const a = [
449
- { a: 'str0' },
450
- ...(flag ? [{ b: 'str1' }, { c: 'str2' }] as const : []),
451
- ] as const satisfies readonly Elem[];
452
- `,
453
- },
454
- {
455
- name: 'Simple spread without conditional should remove inner as const',
456
- source: dedent`
457
- const a = [
458
- 1,
459
- ...[2, 3] as const,
460
- ] as const;
461
- `,
462
- expected: dedent`
463
- const a = [
464
- 1,
465
- ...[2, 3],
466
- ] as const;
467
- `,
468
- },
469
- {
470
- name: 'Multiple simple spreads should remove inner as const',
471
- source: dedent`
472
- const c = [
473
- ...[1, 2] as const,
474
- ...[3, 4] as const,
475
- ] as const;
476
- `,
477
- expected: dedent`
478
- const c = [
479
- ...[1, 2],
480
- ...[3, 4],
481
- ] as const;
482
- `,
483
- },
484
- {
485
- name: 'Nested as const inside spread operator without conditional statement should be removed',
486
- source: dedent`
487
- const flag = true as boolean;
388
+ type Elem =
389
+ | Readonly<{ a: 'str0' }>
390
+ | Readonly<{ b: 'str1' }>
391
+ | Readonly<{ c: 'str2' }>;
488
392
 
489
- type Elem =
490
- | Readonly<{ a: 'str0' }>
491
- | Readonly<{ b: 'str1' }>
492
- | Readonly<{ c: 'str2' }>;
393
+ const a = [
394
+ { a: 'str0' },
395
+ ...[{ b: 'str1' }, { c: 'str2' }],
396
+ ] as const satisfies readonly Elem[];
397
+ `,
398
+ },
399
+ {
400
+ name: 'Object with spread operator',
401
+ source: 'const obj = { a: 1, ...{ b: 2 } };',
402
+ expected: 'const obj = { a: 1, ...{ b: 2 } } as const;',
403
+ },
404
+ ])('$name', testFn);
405
+ });
406
+
407
+ describe('Function contexts', () => {
408
+ test.each([
409
+ {
410
+ name: 'function return statement',
411
+ source: 'function foo() { return [1, 2, 3]; }',
412
+ expected: 'function foo() { return [1, 2, 3] as const; }',
413
+ },
414
+ {
415
+ name: 'arrow function return',
416
+ source: 'const foo = () => ({ a: 1, b: 2 });',
417
+ expected: 'const foo = () => ({ a: 1, b: 2 } as const);',
418
+ },
419
+ {
420
+ name: 'array already has as const',
421
+ source: 'const arr = [1, 2, 3] as const;',
422
+ expected: 'const arr = [1, 2, 3] as const;',
423
+ },
424
+ {
425
+ name: 'object already has as const',
426
+ source: 'const obj = { a: 1 } as const;',
427
+ expected: 'const obj = { a: 1 } as const;',
428
+ },
429
+ {
430
+ name: 'array in function parameter default value',
431
+ source: 'function foo(a = [1, 2]) { return a; }',
432
+ expected: 'function foo(a = [1, 2] as const) { return a; }',
433
+ },
434
+ {
435
+ name: 'object in function parameter default value',
436
+ source: 'function foo(a = { b: 1 }) { return a; }',
437
+ expected: 'function foo(a = { b: 1 } as const) { return a; }',
438
+ },
439
+ {
440
+ name: 'object in function call argument (not transformed with avoidInFunctionArgs)',
441
+ source: 'const a = foo({ b: 1 });',
442
+ expected: 'const a = foo({ b: 1 });',
443
+ },
444
+ {
445
+ name: 'arrow function with generic type parameter',
446
+ source: dedent`
447
+ export const some = <const S,>(value: S): Some<S> =>
448
+ ({
449
+ type: SomeTypeSymbol,
450
+ value
451
+ });
452
+ `,
453
+ expected: dedent`
454
+ export const some = <const S,>(value: S): Some<S> =>
455
+ ({
456
+ type: SomeTypeSymbol,
457
+ value
458
+ }) as const;
459
+ `,
460
+ },
461
+ ])('$name', testFn);
462
+ });
493
463
 
494
- const a = [
495
- { a: 'str0' },
496
- ...([{ b: 'str1' }, { c: 'str2' }] as const),
497
- ] as const satisfies readonly Elem[];
498
- `,
499
- expected: dedent`
500
- const flag = true as boolean;
464
+ describe('Transformer ignore comments', () => {
465
+ test.each([
466
+ {
467
+ name: 'generic transformer-ignore-next-line for array',
468
+ source: dedent`
469
+ // transformer-ignore-next-line
470
+ const skippedArray = [1, 2, 3];
471
+ `,
472
+ expected: dedent`
473
+ // transformer-ignore-next-line
474
+ const skippedArray = [1, 2, 3];
475
+ `,
476
+ },
477
+ {
478
+ name: 'generic transformer-ignore-next-line for object',
479
+ source: dedent`
480
+ // transformer-ignore-next-line
481
+ const skippedObject = { a: 1, b: "hello" };
482
+ `,
483
+ expected: dedent`
484
+ // transformer-ignore-next-line
485
+ const skippedObject = { a: 1, b: "hello" };
486
+ `,
487
+ },
488
+ {
489
+ name: 'transformer-ignore-next-line only affects immediate next line',
490
+ source: dedent`
491
+ const transformedArray = [10, 20]; // This should be transformed
492
+ // transformer-ignore-next-line
493
+ const skippedObject = { x: true }; // This should be skipped
494
+ const transformedObject = { y: false }; // This should be transformed
495
+ `,
496
+ expected: dedent`
497
+ const transformedArray = [10, 20] as const; // This should be transformed
498
+ // transformer-ignore-next-line
499
+ const skippedObject = { x: true }; // This should be skipped
500
+ const transformedObject = { y: false } as const; // This should be transformed
501
+ `,
502
+ },
503
+ {
504
+ name: 'file scope transformer-ignore',
505
+ source: dedent`
506
+ /* transformer-ignore */
507
+ const transformedArray = [10, 20]; // This should be skipped
508
+ const skippedObject = { x: true }; // This should be skipped
509
+ const transformedObject = { y: false }; // This should be skipped
510
+ `,
511
+ expected: dedent`
512
+ /* transformer-ignore */
513
+ const transformedArray = [10, 20]; // This should be skipped
514
+ const skippedObject = { x: true }; // This should be skipped
515
+ const transformedObject = { y: false }; // This should be skipped
516
+ `,
517
+ },
518
+ {
519
+ name: 'transformer-specific ignore (next line)',
520
+ source: dedent`
521
+ const a = [1, 2, 3];
522
+ // transformer-ignore-next-line append-as-const
523
+ const b = [4, 5, 6];
524
+ const c = [7, 8, 9];
525
+ `,
526
+ expected: dedent`
527
+ const a = [1, 2, 3] as const;
528
+ // transformer-ignore-next-line append-as-const
529
+ const b = [4, 5, 6];
530
+ const c = [7, 8, 9] as const;
531
+ `,
532
+ },
533
+ {
534
+ name: 'transformer-specific ignore (file scope)',
535
+ source: dedent`
536
+ /* transformer-ignore append-as-const */
537
+ const a = [1, 2, 3];
538
+ const b = { x: 1 };
539
+ `,
540
+ expected: dedent`
541
+ /* transformer-ignore append-as-const */
542
+ const a = [1, 2, 3];
543
+ const b = { x: 1 };
544
+ `,
545
+ },
546
+ {
547
+ name: 'multiple transformers in ignore comment',
548
+ source: dedent`
549
+ const a = [1, 2, 3];
550
+ // transformer-ignore-next-line append-as-const, replace-any-with-unknown
551
+ const b = [4, 5, 6];
552
+ const c = [7, 8, 9];
553
+ `,
554
+ expected: dedent`
555
+ const a = [1, 2, 3] as const;
556
+ // transformer-ignore-next-line append-as-const, replace-any-with-unknown
557
+ const b = [4, 5, 6];
558
+ const c = [7, 8, 9] as const;
559
+ `,
560
+ },
561
+ {
562
+ name: 'wrong transformer name should not affect transformation',
563
+ source: dedent`
564
+ const a = [1, 2, 3];
565
+ // transformer-ignore-next-line some-other-transformer
566
+ const b = [4, 5, 6];
567
+ const c = [7, 8, 9];
568
+ `,
569
+ expected: dedent`
570
+ const a = [1, 2, 3] as const;
571
+ // transformer-ignore-next-line some-other-transformer
572
+ const b = [4, 5, 6] as const;
573
+ const c = [7, 8, 9] as const;
574
+ `,
575
+ },
576
+ ])('$name', testFn);
577
+ });
501
578
 
502
- type Elem =
503
- | Readonly<{ a: 'str0' }>
504
- | Readonly<{ b: 'str1' }>
505
- | Readonly<{ c: 'str2' }>;
579
+ describe('ignorePrefixes option (mut_ prefix)', () => {
580
+ test.each([
581
+ {
582
+ name: 'single variable with mut_ prefix',
583
+ source: 'const mut_foo = [1, 2, 3];',
584
+ expected: 'const mut_foo = [1, 2, 3];',
585
+ },
586
+ {
587
+ name: 'multiple variables with mut_ prefix',
588
+ source: 'const mut_a = [1, 2], mut_b = { c: 3 };',
589
+ expected: 'const mut_a = [1, 2], mut_b = { c: 3 };',
590
+ },
591
+ ])('$name', testFn);
592
+ });
506
593
 
507
- const a = [
508
- { a: 'str0' },
509
- ...[{ b: 'str1' }, { c: 'str2' }],
510
- ] as const satisfies readonly Elem[];
511
- `,
512
- },
513
- ])('$name', testFn);
594
+ describe('Additional edge cases', () => {
595
+ test.each([
596
+ {
597
+ name: 'type annotation (should be skipped)',
598
+ source: 'const a = 1 as 1;',
599
+ expected: 'const a = 1 as 1;',
600
+ },
601
+ {
602
+ name: 'variable reference',
603
+ source: 'const a = 1; const b = a;',
604
+ expected: 'const a = 1; const b = a;',
605
+ },
606
+ {
607
+ name: 'function call',
608
+ source: 'const result = foo();',
609
+ expected: 'const result = foo();',
610
+ },
611
+ {
612
+ name: 'method call',
613
+ source: 'const result = obj.method();',
614
+ expected: 'const result = obj.method();',
615
+ },
616
+ {
617
+ name: 'binary expression',
618
+ source: 'const sum = a + b;',
619
+ expected: 'const sum = a + b;',
620
+ },
621
+ {
622
+ name: 'new expression',
623
+ source: 'const date = new Date();',
624
+ expected: 'const date = new Date();',
625
+ },
626
+ {
627
+ name: 'class declaration',
628
+ source: 'class MyClass { prop = 1; }',
629
+ expected: 'class MyClass { prop = 1 as const; }',
630
+ },
631
+ {
632
+ name: 'class declaration with mut_ prefix',
633
+ source: 'class MyClass { mut_prop = 1; }',
634
+ expected: 'class MyClass { mut_prop = 1; }',
635
+ },
636
+ {
637
+ name: 'arrow function',
638
+ source: dedent`
639
+ export const some = <const S,>(value: S): Some<S> =>
640
+ ({
641
+ type: SomeTypeSymbol,
642
+ value
643
+ }) as const;
644
+ `,
645
+ expected: dedent`
646
+ export const some = <const S,>(value: S): Some<S> =>
647
+ ({
648
+ type: SomeTypeSymbol,
649
+ value
650
+ }) as const;
651
+ `,
652
+ },
653
+ {
654
+ name: 'Object with computed property',
655
+ source: "const obj = { ['key']: [1, 2] };",
656
+ expected: "const obj = { ['key']: [1, 2] } as const;",
657
+ },
658
+ {
659
+ name: 'Parenthesized array expression',
660
+ source: 'const arr = ([1, 2]);',
661
+ expected: 'const arr = [1, 2] as const;',
662
+ },
663
+ {
664
+ name: 'Parenthesized object expression',
665
+ source: 'const obj = ({ a: 1 });',
666
+ expected: 'const obj = { a: 1 } as const;',
667
+ },
668
+ {
669
+ name: 'Object with method that returns array (method body not transformed)',
670
+ source: 'const obj = { method() { return [1, 2]; } };',
671
+ expected: 'const obj = { method() { return [1, 2]; } } as const;',
672
+ },
673
+ {
674
+ name: 'Regex literal with as const should remove as const',
675
+ source: 'const regex = /test/ as const;',
676
+ expected: 'const regex = /test/;',
677
+ },
678
+ {
679
+ name: 'satisfies without as const should add as const',
680
+ source: 'const a = [1, 2] satisfies readonly number[];',
681
+ expected: 'const a = [1, 2] as const satisfies readonly number[];',
682
+ },
683
+ {
684
+ name: 'Object with getter (getter body not transformed)',
685
+ source: 'const obj = { get value() { return [1, 2]; } };',
686
+ expected: 'const obj = { get value() { return [1, 2]; } } as const;',
687
+ },
688
+ {
689
+ name: 'Object with setter',
690
+ source: 'const obj = { set value(v) { } };',
691
+ expected: 'const obj = { set value(v) { } } as const;',
692
+ },
693
+ ])('$name', testFn);
694
+ });
514
695
  });