ts-codemod-lib 3.1.0 → 3.2.0
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 +13 -0
- package/dist/functions/ast-transformers/append-as-const.d.mts +16 -0
- package/dist/functions/ast-transformers/append-as-const.d.mts.map +1 -1
- package/dist/functions/ast-transformers/append-as-const.mjs +101 -0
- package/dist/functions/ast-transformers/append-as-const.mjs.map +1 -1
- package/dist/functions/ast-transformers/convert-to-readonly.mjs +8 -0
- package/dist/functions/ast-transformers/convert-to-readonly.mjs.map +1 -1
- package/package.json +13 -28
- package/src/functions/ast-transformers/append-as-const.mts +251 -2
- package/src/functions/ast-transformers/append-as-const.test.mts +173 -1
- package/src/functions/ast-transformers/convert-to-readonly.mts +20 -0
- package/src/functions/ast-transformers/convert-to-readonly.test.mts +26 -0
|
@@ -20,6 +20,8 @@ export const appendAsConstTransformer = (
|
|
|
20
20
|
const optionsInternal: AppendAsConstTransformerOptionsInternal = {
|
|
21
21
|
applyLevel: options?.applyLevel ?? 'avoidInFunctionArgs',
|
|
22
22
|
ignoredPrefixes: ignorePrefixes,
|
|
23
|
+
removeAsConstForConstTypeParameters:
|
|
24
|
+
options?.removeAsConstForConstTypeParameters ?? true,
|
|
23
25
|
|
|
24
26
|
debugPrint: options?.debug === true ? console.debug : () => {},
|
|
25
27
|
replaceNode:
|
|
@@ -43,6 +45,10 @@ export const appendAsConstTransformer = (
|
|
|
43
45
|
optionsInternal,
|
|
44
46
|
);
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
if (optionsInternal.removeAsConstForConstTypeParameters) {
|
|
50
|
+
removeAsConstInConstTypeParameterArgs(sourceAst, optionsInternal);
|
|
51
|
+
}
|
|
46
52
|
},
|
|
47
53
|
};
|
|
48
54
|
};
|
|
@@ -62,8 +68,22 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
62
68
|
*/
|
|
63
69
|
ignorePrefixes?: string[];
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Whether to remove redundant `as const` assertions from call arguments
|
|
73
|
+
* whose corresponding parameter type is exactly a `const`-modified type
|
|
74
|
+
* parameter.
|
|
75
|
+
*
|
|
76
|
+
* (e.g. `f([1, 2] as const)` becomes `f([1, 2])` for
|
|
77
|
+
* `function f<const T>(x: T): T`, because the `const` type parameter
|
|
78
|
+
* already makes TypeScript infer the argument as if it were annotated
|
|
79
|
+
* with `as const`.)
|
|
80
|
+
*
|
|
81
|
+
* Only applies when the callee resolves to a single call signature within
|
|
82
|
+
* the transformed file itself; imported callees are left as they are.
|
|
83
|
+
*
|
|
84
|
+
* @default true
|
|
85
|
+
*/
|
|
86
|
+
removeAsConstForConstTypeParameters?: boolean;
|
|
67
87
|
|
|
68
88
|
debug?: boolean;
|
|
69
89
|
}>;
|
|
@@ -71,6 +91,7 @@ export type AppendAsConstTransformerOptions = DeepReadonly<{
|
|
|
71
91
|
type AppendAsConstTransformerOptionsInternal = DeepReadonly<{
|
|
72
92
|
applyLevel: 'all' | 'avoidInFunctionArgs';
|
|
73
93
|
ignoredPrefixes: ISet<string>;
|
|
94
|
+
removeAsConstForConstTypeParameters: boolean;
|
|
74
95
|
|
|
75
96
|
debugPrint: (...args: readonly unknown[]) => void;
|
|
76
97
|
replaceNode: (node: tsm.Node, newNodeText: string) => void;
|
|
@@ -364,3 +385,231 @@ const transformNode = (
|
|
|
364
385
|
// return;
|
|
365
386
|
}
|
|
366
387
|
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Removes redundant `as const` assertions from call arguments whose
|
|
391
|
+
* corresponding parameter type is exactly a `const`-modified type parameter
|
|
392
|
+
* (e.g. `f([1, 2] as const)` for `function f<const T>(x: T): T`).
|
|
393
|
+
*
|
|
394
|
+
* A `const` type parameter makes TypeScript treat every inference candidate
|
|
395
|
+
* as if it were annotated with `as const`, so when the parameter type is the
|
|
396
|
+
* bare type parameter itself, removing the assertion cannot change the
|
|
397
|
+
* inferred type. Conservative bail-outs (the `as const` is kept) when:
|
|
398
|
+
*
|
|
399
|
+
* - the callee does not resolve to exactly one call signature (unresolved
|
|
400
|
+
* imports resolve to none, since each file is transformed in an isolated
|
|
401
|
+
* single-file project; overloaded functions resolve to several),
|
|
402
|
+
* - the call has explicit type arguments (inference is not involved),
|
|
403
|
+
* - the argument is a spread argument or follows one (its parameter position
|
|
404
|
+
* is not statically known),
|
|
405
|
+
* - the parameter type is not the bare type parameter (e.g. `readonly T[]`).
|
|
406
|
+
*/
|
|
407
|
+
const removeAsConstInConstTypeParameterArgs = (
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
409
|
+
sourceAst: tsm.SourceFile,
|
|
410
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
411
|
+
): void => {
|
|
412
|
+
// Reversed so that calls nested inside another call's arguments are
|
|
413
|
+
// processed before the outer call replaces (and thereby forgets) them.
|
|
414
|
+
for (const call of sourceAst
|
|
415
|
+
.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)
|
|
416
|
+
.toReversed()) {
|
|
417
|
+
if (call.wasForgotten()) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const args = call.getArguments();
|
|
422
|
+
|
|
423
|
+
// Cheap syntactic pre-check before touching the type checker
|
|
424
|
+
if (!args.some(isAsConstNode)) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (hasDisableNextLineComment(call, TRANSFORMER_NAME)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const variableStatement = call.getFirstAncestorByKind(
|
|
433
|
+
tsm.SyntaxKind.VariableStatement,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (
|
|
437
|
+
variableStatement !== undefined &&
|
|
438
|
+
hasDisableNextLineComment(variableStatement, TRANSFORMER_NAME)
|
|
439
|
+
) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// With explicit type arguments no inference happens, so the `as const`
|
|
444
|
+
// may be load-bearing for assignability.
|
|
445
|
+
if (!Arr.isEmpty(call.getTypeArguments())) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const callTarget = resolveSingleSignatureCallTarget(call);
|
|
450
|
+
|
|
451
|
+
if (callTarget === undefined) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
removeAsConstArgsOfCall(args, callTarget, options);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const removeAsConstArgsOfCall = (
|
|
460
|
+
args: readonly tsm.Node[],
|
|
461
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
462
|
+
callTarget: ResolvedCallTarget,
|
|
463
|
+
options: AppendAsConstTransformerOptionsInternal,
|
|
464
|
+
): void => {
|
|
465
|
+
// Argument positions at or after a spread argument cannot be mapped to a
|
|
466
|
+
// parameter statically.
|
|
467
|
+
const firstSpreadIndex = args.findIndex((a) =>
|
|
468
|
+
a.isKind(tsm.SyntaxKind.SpreadElement),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const mappableArgCount =
|
|
472
|
+
firstSpreadIndex === -1 ? args.length : firstSpreadIndex;
|
|
473
|
+
|
|
474
|
+
for (const [argumentIndex, argument] of args.entries()) {
|
|
475
|
+
if (argumentIndex >= mappableArgCount) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (argument.wasForgotten() || !isAsConstNode(argument)) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (hasDisableNextLineComment(argument, TRANSFORMER_NAME)) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const parameter = getMatchedParameter(callTarget.parameters, argumentIndex);
|
|
488
|
+
|
|
489
|
+
if (
|
|
490
|
+
parameter === undefined ||
|
|
491
|
+
!isBareConstTypeParameterReference(
|
|
492
|
+
parameter,
|
|
493
|
+
callTarget.constTypeParameterNames,
|
|
494
|
+
)
|
|
495
|
+
) {
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
options.debugPrint(
|
|
500
|
+
'removing redundant as const in const type parameter argument',
|
|
501
|
+
argument.getText(),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
options.replaceNode(argument, argument.getExpression().getText());
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
type ResolvedCallTarget = Readonly<{
|
|
509
|
+
parameters: readonly tsm.ParameterDeclaration[];
|
|
510
|
+
constTypeParameterNames: ISet<string>;
|
|
511
|
+
}>;
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolves the callee of a call expression to the declaration of its single
|
|
515
|
+
* call signature, and collects the names of that declaration's
|
|
516
|
+
* `const`-modified type parameters.
|
|
517
|
+
*
|
|
518
|
+
* Returns `undefined` when the callee does not resolve to exactly one call
|
|
519
|
+
* signature, the resolved declaration has no parameters or type parameters,
|
|
520
|
+
* or none of the type parameters carry the `const` modifier.
|
|
521
|
+
*/
|
|
522
|
+
const resolveSingleSignatureCallTarget = (
|
|
523
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
524
|
+
call: tsm.CallExpression,
|
|
525
|
+
): ResolvedCallTarget | undefined => {
|
|
526
|
+
// 0 signatures: the callee is unresolved (e.g. imported, since each file
|
|
527
|
+
// is transformed in an isolated single-file project).
|
|
528
|
+
// 2+ signatures: overloads, where removing `as const` could change which
|
|
529
|
+
// overload is selected.
|
|
530
|
+
if (
|
|
531
|
+
!Arr.isFixedLengthArray(
|
|
532
|
+
1,
|
|
533
|
+
call.getExpression().getType().getCallSignatures(),
|
|
534
|
+
)
|
|
535
|
+
) {
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const declaration = call
|
|
540
|
+
.getProject()
|
|
541
|
+
.getTypeChecker()
|
|
542
|
+
.getResolvedSignature(call)
|
|
543
|
+
?.getDeclaration();
|
|
544
|
+
|
|
545
|
+
if (
|
|
546
|
+
declaration === undefined ||
|
|
547
|
+
!tsm.Node.isParametered(declaration) ||
|
|
548
|
+
!tsm.Node.isTypeParametered(declaration)
|
|
549
|
+
) {
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Only type parameters declared on the call's own signature are inferred
|
|
554
|
+
// at the call site; a `const` type parameter of an enclosing declaration
|
|
555
|
+
// (e.g. a generic class containing this method) is already fixed there.
|
|
556
|
+
const constTypeParameterNames = ISet.create(
|
|
557
|
+
declaration
|
|
558
|
+
.getTypeParameters()
|
|
559
|
+
.filter((tp) => tp.hasModifier(tsm.SyntaxKind.ConstKeyword))
|
|
560
|
+
.map((tp) => tp.getName()),
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
if (constTypeParameterNames.size === 0) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
parameters: declaration.getParameters(),
|
|
569
|
+
constTypeParameterNames,
|
|
570
|
+
} as const;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const getMatchedParameter = (
|
|
574
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
575
|
+
parameters: readonly tsm.ParameterDeclaration[],
|
|
576
|
+
argumentIndex: number,
|
|
577
|
+
): tsm.ParameterDeclaration | undefined => {
|
|
578
|
+
if (argumentIndex < parameters.length) {
|
|
579
|
+
return parameters[argumentIndex];
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const lastParameter = parameters.at(-1);
|
|
583
|
+
|
|
584
|
+
return lastParameter?.isRestParameter() === true ? lastParameter : undefined;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Whether the parameter's declared type is exactly a bare reference to one of
|
|
589
|
+
* the given `const`-modified type parameters (i.e. `x: T`, not `x: readonly
|
|
590
|
+
* T[]` or `x: Wrapper<T>` — there the argument expression's own type is still
|
|
591
|
+
* observable, so the `as const` is kept).
|
|
592
|
+
*/
|
|
593
|
+
const isBareConstTypeParameterReference = (
|
|
594
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
595
|
+
parameter: tsm.ParameterDeclaration,
|
|
596
|
+
constTypeParameterNames: ISet<string>,
|
|
597
|
+
): boolean => {
|
|
598
|
+
const typeNode = parameter.getTypeNode();
|
|
599
|
+
|
|
600
|
+
if (typeNode === undefined) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (!typeNode.isKind(tsm.SyntaxKind.TypeReference)) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const typeName = typeNode.getTypeName();
|
|
609
|
+
|
|
610
|
+
return (
|
|
611
|
+
typeName.isKind(tsm.SyntaxKind.Identifier) &&
|
|
612
|
+
Arr.isEmpty(typeNode.getTypeArguments()) &&
|
|
613
|
+
constTypeParameterNames.has(typeName.getText())
|
|
614
|
+
);
|
|
615
|
+
};
|
|
@@ -11,10 +11,12 @@ import { transformSourceCode } from './transform-source-code.mjs';
|
|
|
11
11
|
const testFn = async ({
|
|
12
12
|
source,
|
|
13
13
|
expected,
|
|
14
|
+
options,
|
|
14
15
|
debug,
|
|
15
16
|
}: Readonly<{
|
|
16
17
|
source: string;
|
|
17
18
|
expected: string;
|
|
19
|
+
options?: Parameters<typeof appendAsConstTransformer>[0];
|
|
18
20
|
debug?: boolean;
|
|
19
21
|
}>): Promise<void> => {
|
|
20
22
|
if (debug !== true) {
|
|
@@ -26,7 +28,9 @@ const testFn = async ({
|
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const transformed = await formatter(
|
|
29
|
-
transformSourceCode(source, false, [
|
|
31
|
+
transformSourceCode(source, false, [
|
|
32
|
+
appendAsConstTransformer({ ...options, debug }),
|
|
33
|
+
]),
|
|
30
34
|
);
|
|
31
35
|
|
|
32
36
|
const expectedFormatted = await formatter(expected);
|
|
@@ -698,6 +702,174 @@ describe(appendAsConstTransformer, () => {
|
|
|
698
702
|
])('$name', testFn);
|
|
699
703
|
});
|
|
700
704
|
|
|
705
|
+
describe('Redundant as const in const type parameter arguments', () => {
|
|
706
|
+
test.each([
|
|
707
|
+
{
|
|
708
|
+
name: 'const type parameter (function declaration) should remove as const',
|
|
709
|
+
source: dedent`
|
|
710
|
+
function f<const T>(x: T): T { return x; }
|
|
711
|
+
const a = f([1, 2] as const);
|
|
712
|
+
`,
|
|
713
|
+
expected: dedent`
|
|
714
|
+
function f<const T>(x: T): T { return x; }
|
|
715
|
+
const a = f([1, 2]);
|
|
716
|
+
`,
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'const type parameter (expression statement call) should remove as const',
|
|
720
|
+
source: dedent`
|
|
721
|
+
function f<const T>(x: T): T { return x; }
|
|
722
|
+
f({ x: 1 } as const);
|
|
723
|
+
`,
|
|
724
|
+
expected: dedent`
|
|
725
|
+
function f<const T>(x: T): T { return x; }
|
|
726
|
+
f({ x: 1 });
|
|
727
|
+
`,
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
name: 'const type parameter (arrow function) should remove as const',
|
|
731
|
+
source: dedent`
|
|
732
|
+
const f = <const S,>(x: S): S => x;
|
|
733
|
+
const a = f([1, 2] as const);
|
|
734
|
+
`,
|
|
735
|
+
expected: dedent`
|
|
736
|
+
const f = <const S,>(x: S): S => x;
|
|
737
|
+
const a = f([1, 2]);
|
|
738
|
+
`,
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: 'const type parameter (object literal method) should remove as const',
|
|
742
|
+
source: dedent`
|
|
743
|
+
const Obj = { m: <const U,>(x: U): U => x } as const;
|
|
744
|
+
const a = Obj.m([1] as const);
|
|
745
|
+
`,
|
|
746
|
+
expected: dedent`
|
|
747
|
+
const Obj = { m: <const U,>(x: U): U => x } as const;
|
|
748
|
+
const a = Obj.m([1]);
|
|
749
|
+
`,
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
name: 'const rest type parameter should remove as const',
|
|
753
|
+
source: dedent`
|
|
754
|
+
function rest<const T extends readonly unknown[]>(...xs: T): T { return xs; }
|
|
755
|
+
const r = rest(1 as const, [2, 3] as const);
|
|
756
|
+
`,
|
|
757
|
+
expected: dedent`
|
|
758
|
+
function rest<const T extends readonly unknown[]>(...xs: T): T { return xs; }
|
|
759
|
+
const r = rest(1, [2, 3]);
|
|
760
|
+
`,
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
name: 'mixed const and non-const type parameters should only remove for const ones',
|
|
764
|
+
source: dedent`
|
|
765
|
+
function h<const A, B>(a: A, b: B): A { return a; }
|
|
766
|
+
const m = h([1] as const, [2] as const);
|
|
767
|
+
`,
|
|
768
|
+
expected: dedent`
|
|
769
|
+
function h<const A, B>(a: A, b: B): A { return a; }
|
|
770
|
+
const m = h([1], [2] as const);
|
|
771
|
+
`,
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'non-const type parameter should keep as const',
|
|
775
|
+
source: dedent`
|
|
776
|
+
function g<T>(x: T): T { return x; }
|
|
777
|
+
const b = g([1, 2] as const);
|
|
778
|
+
`,
|
|
779
|
+
expected: dedent`
|
|
780
|
+
function g<T>(x: T): T { return x; }
|
|
781
|
+
const b = g([1, 2] as const);
|
|
782
|
+
`,
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
name: 'imported callee should keep as const',
|
|
786
|
+
source: dedent`
|
|
787
|
+
import { f } from './other.mjs';
|
|
788
|
+
const a = f([1, 2] as const);
|
|
789
|
+
`,
|
|
790
|
+
expected: dedent`
|
|
791
|
+
import { f } from './other.mjs';
|
|
792
|
+
const a = f([1, 2] as const);
|
|
793
|
+
`,
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: 'overloaded function should keep as const',
|
|
797
|
+
source: dedent`
|
|
798
|
+
function ov<const T>(x: T): T;
|
|
799
|
+
function ov(x: number): number;
|
|
800
|
+
function ov(x: unknown): unknown { return x; }
|
|
801
|
+
const e = ov([1] as const);
|
|
802
|
+
`,
|
|
803
|
+
expected: dedent`
|
|
804
|
+
function ov<const T>(x: T): T;
|
|
805
|
+
function ov(x: number): number;
|
|
806
|
+
function ov(x: unknown): unknown { return x; }
|
|
807
|
+
const e = ov([1] as const);
|
|
808
|
+
`,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'explicit type arguments should keep as const',
|
|
812
|
+
source: dedent`
|
|
813
|
+
function f<const T>(x: T): T { return x; }
|
|
814
|
+
const j = f<readonly [1, 2]>([1, 2] as const);
|
|
815
|
+
`,
|
|
816
|
+
expected: dedent`
|
|
817
|
+
function f<const T>(x: T): T { return x; }
|
|
818
|
+
const j = f<readonly [1, 2]>([1, 2] as const);
|
|
819
|
+
`,
|
|
820
|
+
},
|
|
821
|
+
{
|
|
822
|
+
name: 'non-bare type parameter reference (readonly T[]) should keep as const',
|
|
823
|
+
source: dedent`
|
|
824
|
+
function ro<const T>(xs: readonly T[]): T { return xs[0] as T; }
|
|
825
|
+
const o = ro([1, 2] as const);
|
|
826
|
+
`,
|
|
827
|
+
expected: dedent`
|
|
828
|
+
function ro<const T>(xs: readonly T[]): T { return xs[0] as T; }
|
|
829
|
+
const o = ro([1, 2] as const);
|
|
830
|
+
`,
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: 'argument after spread argument should keep as const',
|
|
834
|
+
source: dedent`
|
|
835
|
+
function f<const T>(a: T, b: T): T { return a; }
|
|
836
|
+
const xs = [1, 2] as const;
|
|
837
|
+
const s = f(...xs, [3] as const);
|
|
838
|
+
`,
|
|
839
|
+
expected: dedent`
|
|
840
|
+
function f<const T>(a: T, b: T): T { return a; }
|
|
841
|
+
const xs = [1, 2] as const;
|
|
842
|
+
const s = f(...xs, [3] as const);
|
|
843
|
+
`,
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: 'transformer-ignore-next-line should keep as const',
|
|
847
|
+
source: dedent`
|
|
848
|
+
function f<const T>(x: T): T { return x; }
|
|
849
|
+
// transformer-ignore-next-line
|
|
850
|
+
const a = f([1, 2] as const);
|
|
851
|
+
`,
|
|
852
|
+
expected: dedent`
|
|
853
|
+
function f<const T>(x: T): T { return x; }
|
|
854
|
+
// transformer-ignore-next-line
|
|
855
|
+
const a = f([1, 2] as const);
|
|
856
|
+
`,
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: 'removeAsConstForConstTypeParameters: false should keep as const',
|
|
860
|
+
source: dedent`
|
|
861
|
+
function f<const T>(x: T): T { return x; }
|
|
862
|
+
const a = f([1, 2] as const);
|
|
863
|
+
`,
|
|
864
|
+
expected: dedent`
|
|
865
|
+
function f<const T>(x: T): T { return x; }
|
|
866
|
+
const a = f([1, 2] as const);
|
|
867
|
+
`,
|
|
868
|
+
options: { removeAsConstForConstTypeParameters: false },
|
|
869
|
+
},
|
|
870
|
+
])('$name', testFn);
|
|
871
|
+
});
|
|
872
|
+
|
|
701
873
|
describe('ignorePrefixes option (mut_ prefix)', () => {
|
|
702
874
|
test.each([
|
|
703
875
|
{
|
|
@@ -1445,6 +1445,26 @@ const transformUnionOrIntersectionTypeNodeImpl = (
|
|
|
1445
1445
|
};
|
|
1446
1446
|
|
|
1447
1447
|
const unionToString = ({
|
|
1448
|
+
types: typesRaw,
|
|
1449
|
+
op,
|
|
1450
|
+
wrapWithReadonly,
|
|
1451
|
+
}: Readonly<{
|
|
1452
|
+
types: readonly string[];
|
|
1453
|
+
op: '&' | '|';
|
|
1454
|
+
wrapWithReadonly: boolean | string;
|
|
1455
|
+
}>): string => {
|
|
1456
|
+
// The members arrive as `getFullText()`, which carries the whitespace in
|
|
1457
|
+
// front of each one, and they are joined with a separator that supplies its
|
|
1458
|
+
// own space. Left alone, that whitespace accumulates: every run of the
|
|
1459
|
+
// transformer adds one more space before every member but the first, so a
|
|
1460
|
+
// file never reaches a fixed point. Comments live in the same trivia and are
|
|
1461
|
+
// not whitespace, so trimming keeps them.
|
|
1462
|
+
const types = typesRaw.map((s) => s.trim());
|
|
1463
|
+
|
|
1464
|
+
return unionToStringImpl({ types, op, wrapWithReadonly });
|
|
1465
|
+
};
|
|
1466
|
+
|
|
1467
|
+
const unionToStringImpl = ({
|
|
1448
1468
|
types,
|
|
1449
1469
|
op,
|
|
1450
1470
|
wrapWithReadonly,
|
|
@@ -4352,3 +4352,29 @@ const toUnionAndIntersectionTestCase = ({
|
|
|
4352
4352
|
name: title(op === '&' ? 'Intersection' : 'Union'),
|
|
4353
4353
|
...testCase(op),
|
|
4354
4354
|
}));
|
|
4355
|
+
|
|
4356
|
+
describe('idempotency', () => {
|
|
4357
|
+
test('a union whose members carry leading whitespace reaches a fixed point', () => {
|
|
4358
|
+
// The members are read with `getFullText()`, which includes the whitespace
|
|
4359
|
+
// in front of each one, and joined with a separator that adds its own. Left
|
|
4360
|
+
// alone the two compound, and a file with `// prettier-ignore` on a long
|
|
4361
|
+
// union grows by one space per member on every run — nothing downstream
|
|
4362
|
+
// normalizes it back.
|
|
4363
|
+
const code = [
|
|
4364
|
+
'// prettier-ignore',
|
|
4365
|
+
'export type P = ',
|
|
4366
|
+
' (0 | 0.1 | 0.2 | 0.3);',
|
|
4367
|
+
'',
|
|
4368
|
+
].join('\n');
|
|
4369
|
+
|
|
4370
|
+
const once = transformSourceCode(code, false, [
|
|
4371
|
+
convertToReadonlyTransformer(),
|
|
4372
|
+
]);
|
|
4373
|
+
|
|
4374
|
+
const twice = transformSourceCode(once, false, [
|
|
4375
|
+
convertToReadonlyTransformer(),
|
|
4376
|
+
]);
|
|
4377
|
+
|
|
4378
|
+
expect(twice).toBe(once);
|
|
4379
|
+
});
|
|
4380
|
+
});
|