Skip to content

Commit 87d00d2

Browse files
devversionAndrewKushnir
authored andcommitted
refactor(migrations): use input() shorthand if possible in input migration (#57368)
In some cases, the migration can detect when `input()` as a shorthand may be usable. This commit adds such detection and migrates inputs to this form when possible. PR Close #57368
1 parent 8475206 commit 87d00d2

File tree

7 files changed

+82
-43
lines changed

7 files changed

+82
-43
lines changed

packages/core/schematics/migrations/signal-migration/src/convert-input/convert_to_signal.ts

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,30 @@
99
import assert from 'assert';
1010
import ts from 'typescript';
1111

12-
import {MigrationHost} from '../migration_host';
1312
import {ConvertInputPreparation} from './prepare_and_check';
1413
import {DecoratorInputTransform} from '../../../../../../compiler-cli/src/ngtsc/metadata';
1514
import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator';
15+
import {removeFromUnionIfPossible} from '../utils/remove_from_union';
1616

1717
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
1818

19+
// TODO: Consider initializations inside the constructor. Those are not migrated right now
20+
// though, as they are writes.
21+
1922
/**
2023
*
2124
* Converts an `@Input()` property declaration to a signal input.
2225
*
2326
* @returns The transformed property declaration, printed as a string.
2427
*/
2528
export function convertToSignalInput(
26-
host: MigrationHost,
2729
node: ts.PropertyDeclaration,
28-
{resolvedMetadata: metadata, resolvedType, isResolvedTypeCheckable}: ConvertInputPreparation,
30+
{
31+
resolvedMetadata: metadata,
32+
resolvedType,
33+
preferShorthandIfPossible,
34+
isUndefinedInitialValue,
35+
}: ConvertInputPreparation,
2936
checker: ts.TypeChecker,
3037
importManager: ImportManager,
3138
): string {
@@ -46,16 +53,33 @@ export function convertToSignalInput(
4653
);
4754
}
4855
if (metadata.transform !== null) {
49-
properties.push(
50-
extractTransformOfInput(metadata.transform, resolvedType, isResolvedTypeCheckable, checker),
51-
);
56+
properties.push(extractTransformOfInput(metadata.transform, resolvedType, checker));
5257
}
5358

5459
optionsLiteral = ts.factory.createObjectLiteralExpression(properties);
5560
}
5661

57-
const strictPropertyInitialization =
58-
!!host.options.strict || !!host.options.strictPropertyInitialization;
62+
// The initial value is `undefined` or none is present:
63+
// - We may be able to use the `input()` shorthand
64+
// - or we use an explicit `undefined` initial value.
65+
if (isUndefinedInitialValue) {
66+
// Shorthand not possible, so explicitly add `undefined`.
67+
if (preferShorthandIfPossible === null) {
68+
initialValue = ts.factory.createIdentifier('undefined');
69+
} else {
70+
resolvedType = preferShorthandIfPossible.originalType;
71+
72+
// When using the `input()` shorthand, try cutting of `undefined` from potential
73+
// union types. `undefined` will be automatically included in the type.
74+
if (ts.isUnionTypeNode(resolvedType)) {
75+
resolvedType = removeFromUnionIfPossible(
76+
resolvedType,
77+
(t) => t.kind !== ts.SyntaxKind.UndefinedKeyword,
78+
);
79+
}
80+
}
81+
}
82+
5983
const inputArgs: ts.Expression[] = [];
6084
const typeArguments: ts.TypeNode[] = [];
6185

@@ -67,19 +91,6 @@ export function convertToSignalInput(
6791
}
6892
}
6993

70-
// If we have no initial value but strict property initialization is enabled, we
71-
// need to add an explicit value. Alternatively, if we have an explicit type, we
72-
// need to add an explicit initial value as per the API signature of `input()`.
73-
if (initialValue === undefined && (strictPropertyInitialization || resolvedType !== undefined)) {
74-
// TODO: Consider initializations inside the constructor. Those are not migrated right now
75-
// though, as they are writes.
76-
77-
// TODO: We can use the `input()` shorthand if there is a question mark?
78-
// We can assume `undefined` is part of the type already, either already was included, or
79-
// we added synthetically as part of the preparation.
80-
initialValue = ts.factory.createIdentifier('undefined');
81-
}
82-
8394
// Always add an initial value when the input is optional, and we have one, or we need one
8495
// to be able to pass options as the second argument.
8596
if (!metadata.required && (initialValue !== undefined || optionsLiteral !== null)) {
@@ -128,7 +139,6 @@ export function convertToSignalInput(
128139
function extractTransformOfInput(
129140
transform: DecoratorInputTransform,
130141
resolvedType: ts.TypeNode | undefined,
131-
isResolvedTypeCheckable: boolean,
132142
checker: ts.TypeChecker,
133143
): ts.PropertyAssignment {
134144
assert(ts.isExpression(transform.node), `Expected transform to be an expression.`);
@@ -138,7 +148,10 @@ function extractTransformOfInput(
138148
// In some cases, the transform function is not compatible because with decorator inputs,
139149
// those were not checked. We cast the transform to `any` and add a TODO.
140150
// TODO: Insert a TODO and capture this in the design doc.
141-
if (resolvedType !== undefined && isResolvedTypeCheckable) {
151+
if (resolvedType !== undefined && !ts.isSyntheticExpression(resolvedType)) {
152+
// Note: If the type is synthetic, we cannot check, and we accept that in the worst case
153+
// we will create code that is not necessarily compiling. This is unlikely, but notably
154+
// the errors would be correct and valuable.
142155
const transformType = checker.getTypeAtLocation(transform.node);
143156
const transformSignature = transformType.getCallSignatures()[0];
144157
assert(transformSignature !== undefined, 'Expected transform to be an invoke-able.');

packages/core/schematics/migrations/signal-migration/src/convert-input/prepare_and_check.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import {InputNode} from '../input_detection/input_node';
2020
*/
2121
export interface ConvertInputPreparation {
2222
resolvedType: ts.TypeNode | undefined;
23-
isResolvedTypeCheckable: boolean;
23+
preferShorthandIfPossible: {originalType: ts.TypeNode} | null;
24+
isUndefinedInitialValue: boolean;
2425
resolvedMetadata: ExtractedInput;
2526
}
2627

@@ -53,18 +54,27 @@ export function prepareAndCheckForConversion(
5354
metadata.required = true;
5455
}
5556

57+
const isUndefinedInitialValue =
58+
node.initializer === undefined ||
59+
(ts.isIdentifier(node.initializer) && node.initializer.text === 'undefined');
5660
let typeToAdd: ts.TypeNode | undefined = node.type;
57-
let isResolvedTypeCheckable = true;
61+
let preferShorthandIfPossible: {originalType: ts.TypeNode} | null = null;
5862

59-
// If the input was using `@Input() bla?: string;`, then we try to explicitly
60-
// add `undefined` as type, if it's not part of the type already.
63+
// If there is no initial value, or it's `undefined`, we can prefer the `input()`
64+
// shorthand which automatically uses `undefined` as initial value, and includes it
65+
// in the input type.
66+
if (!metadata.required && node.type !== undefined && isUndefinedInitialValue) {
67+
preferShorthandIfPossible = {originalType: node.type};
68+
}
69+
70+
// If the input is using `@Input() bla?: string;` with the "optional question mark",
71+
// then we try to explicitly add `undefined` as type, if it's not part of the type already.
72+
// This is ensuring correctness, as `bla?` automatically includes `undefined` currently.
6173
if (
6274
node.type !== undefined &&
6375
node.questionToken !== undefined &&
6476
!checker.isTypeAssignableTo(checker.getUndefinedType(), checker.getTypeFromTypeNode(node.type))
6577
) {
66-
// Synthetic types are never checkable.
67-
isResolvedTypeCheckable = false;
6878
typeToAdd = ts.factory.createUnionTypeNode([
6979
node.type,
7080
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
@@ -74,9 +84,6 @@ export function prepareAndCheckForConversion(
7484
// Attempt to extract type from input initial value. No explicit type, but input is required.
7585
// Hence we need an explicit type, or fall back to `typeof`.
7686
if (typeToAdd === undefined && initialValue !== undefined && metadata.required) {
77-
// Synthetic types are never checkable.
78-
isResolvedTypeCheckable = false;
79-
8087
const propertyType = checker.getTypeAtLocation(node);
8188
if (propertyType.flags & ts.TypeFlags.Boolean) {
8289
typeToAdd = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
@@ -108,7 +115,8 @@ export function prepareAndCheckForConversion(
108115

109116
return {
110117
resolvedMetadata: metadata,
111-
isResolvedTypeCheckable,
112118
resolvedType: typeToAdd,
119+
preferShorthandIfPossible,
120+
isUndefinedInitialValue,
113121
};
114122
}

packages/core/schematics/migrations/signal-migration/src/passes/6_migrate_input_declarations.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {MigrationResult} from '../result';
1111
import {Replacement} from '../replacement';
1212
import {convertToSignalInput} from '../convert-input/convert_to_signal';
1313
import assert from 'assert';
14-
import {MigrationHost} from '../migration_host';
1514
import {KnownInputs} from '../input_detection/known_inputs';
1615
import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator';
1716

@@ -20,7 +19,6 @@ import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator
2019
* manages imports within the given file.
2120
*/
2221
export function pass6__migrateInputDeclarations(
23-
host: MigrationHost,
2422
checker: ts.TypeChecker,
2523
result: MigrationResult,
2624
knownInputs: KnownInputs,
@@ -46,7 +44,7 @@ export function pass6__migrateInputDeclarations(
4644
new Replacement(
4745
input.node.getStart(),
4846
input.node.getEnd(),
49-
convertToSignalInput(host, input.node, metadata, checker, importManager),
47+
convertToSignalInput(input.node, metadata, checker, importManager),
5048
),
5149
);
5250
}

packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function executeMigrationPhase(
4141

4242
// Migrate passes.
4343
pass5__migrateTypeScriptReferences(result, typeChecker, knownInputs);
44-
pass6__migrateInputDeclarations(host, typeChecker, result, knownInputs, importManager);
44+
pass6__migrateInputDeclarations(typeChecker, result, knownInputs, importManager);
4545
pass7__migrateTemplateReferences(host, result, knownInputs);
4646
pass8__migrateHostBindings(result, knownInputs);
4747
pass9__migrateTypeScriptTypeReferences(result, knownInputs, importManager);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
export function removeFromUnionIfPossible(
12+
union: ts.UnionTypeNode,
13+
filter: (v: ts.TypeNode) => boolean,
14+
): ts.UnionTypeNode {
15+
const filtered = union.types.filter(filter);
16+
if (filtered.length === union.types.length) {
17+
return union;
18+
}
19+
return ts.factory.updateUnionTypeNode(union, ts.factory.createNodeArray(filtered));
20+
}

packages/core/schematics/migrations/signal-migration/test/golden.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ export class AppComponent {
374374
input = input<string | null>(null);
375375
bla = input.required<boolean, string | boolean>({ transform: disabledTransform });
376376
narrowableMultipleTimes = input<Vehicle | null>(null);
377-
withUndefinedInput = input<string | undefined>(undefined);
377+
withUndefinedInput = input<string>();
378378
@Input() incompatible: string | null = null;
379379

380380
private _bla: any;
@@ -720,7 +720,7 @@ import { Directive, input } from '@angular/core';
720720

721721
@Directive()
722722
class OptionalInput {
723-
bla = input<string | undefined>(undefined);
723+
bla = input<string>();
724724
}
725725
@@@@@@ problematic_type_reference.ts @@@@@@
726726

@@ -1065,5 +1065,5 @@ class WithJsdoc {
10651065
*/
10661066
simpleInput = input.required<string>();
10671067

1068-
withCommentInside = input</* intended */ boolean | undefined>(undefined);
1068+
withCommentInside = input</* intended */ boolean>();
10691069
}

packages/core/schematics/migrations/signal-migration/test/golden_best_effort.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ export class AppComponent {
374374
input = input<string | null>(null);
375375
bla = input.required<boolean, string | boolean>({ transform: disabledTransform });
376376
narrowableMultipleTimes = input<Vehicle | null>(null);
377-
withUndefinedInput = input<string | undefined>(undefined);
377+
withUndefinedInput = input<string>();
378378
incompatible = input<string | null>(null);
379379

380380
private _bla: any;
@@ -720,7 +720,7 @@ import { Directive, input } from '@angular/core';
720720

721721
@Directive()
722722
class OptionalInput {
723-
bla = input<string | undefined>(undefined);
723+
bla = input<string>();
724724
}
725725
@@@@@@ problematic_type_reference.ts @@@@@@
726726

@@ -1065,5 +1065,5 @@ class WithJsdoc {
10651065
*/
10661066
simpleInput = input.required<string>();
10671067

1068-
withCommentInside = input</* intended */ boolean | undefined>(undefined);
1068+
withCommentInside = input</* intended */ boolean>();
10691069
}

0 commit comments

Comments
 (0)