mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Andrew Kushnir
parent
84752069f2
commit
87d00d26ff
+36
-23
@@ -9,13 +9,16 @@
|
||||
import assert from 'assert';
|
||||
import ts from 'typescript';
|
||||
|
||||
import {MigrationHost} from '../migration_host';
|
||||
import {ConvertInputPreparation} from './prepare_and_check';
|
||||
import {DecoratorInputTransform} from '../../../../../../compiler-cli/src/ngtsc/metadata';
|
||||
import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator';
|
||||
import {removeFromUnionIfPossible} from '../utils/remove_from_union';
|
||||
|
||||
const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
||||
|
||||
// TODO: Consider initializations inside the constructor. Those are not migrated right now
|
||||
// though, as they are writes.
|
||||
|
||||
/**
|
||||
*
|
||||
* Converts an `@Input()` property declaration to a signal input.
|
||||
@@ -23,9 +26,13 @@ const printer = ts.createPrinter({newLine: ts.NewLineKind.LineFeed});
|
||||
* @returns The transformed property declaration, printed as a string.
|
||||
*/
|
||||
export function convertToSignalInput(
|
||||
host: MigrationHost,
|
||||
node: ts.PropertyDeclaration,
|
||||
{resolvedMetadata: metadata, resolvedType, isResolvedTypeCheckable}: ConvertInputPreparation,
|
||||
{
|
||||
resolvedMetadata: metadata,
|
||||
resolvedType,
|
||||
preferShorthandIfPossible,
|
||||
isUndefinedInitialValue,
|
||||
}: ConvertInputPreparation,
|
||||
checker: ts.TypeChecker,
|
||||
importManager: ImportManager,
|
||||
): string {
|
||||
@@ -46,16 +53,33 @@ export function convertToSignalInput(
|
||||
);
|
||||
}
|
||||
if (metadata.transform !== null) {
|
||||
properties.push(
|
||||
extractTransformOfInput(metadata.transform, resolvedType, isResolvedTypeCheckable, checker),
|
||||
);
|
||||
properties.push(extractTransformOfInput(metadata.transform, resolvedType, checker));
|
||||
}
|
||||
|
||||
optionsLiteral = ts.factory.createObjectLiteralExpression(properties);
|
||||
}
|
||||
|
||||
const strictPropertyInitialization =
|
||||
!!host.options.strict || !!host.options.strictPropertyInitialization;
|
||||
// The initial value is `undefined` or none is present:
|
||||
// - We may be able to use the `input()` shorthand
|
||||
// - or we use an explicit `undefined` initial value.
|
||||
if (isUndefinedInitialValue) {
|
||||
// Shorthand not possible, so explicitly add `undefined`.
|
||||
if (preferShorthandIfPossible === null) {
|
||||
initialValue = ts.factory.createIdentifier('undefined');
|
||||
} else {
|
||||
resolvedType = preferShorthandIfPossible.originalType;
|
||||
|
||||
// When using the `input()` shorthand, try cutting of `undefined` from potential
|
||||
// union types. `undefined` will be automatically included in the type.
|
||||
if (ts.isUnionTypeNode(resolvedType)) {
|
||||
resolvedType = removeFromUnionIfPossible(
|
||||
resolvedType,
|
||||
(t) => t.kind !== ts.SyntaxKind.UndefinedKeyword,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const inputArgs: ts.Expression[] = [];
|
||||
const typeArguments: ts.TypeNode[] = [];
|
||||
|
||||
@@ -67,19 +91,6 @@ export function convertToSignalInput(
|
||||
}
|
||||
}
|
||||
|
||||
// If we have no initial value but strict property initialization is enabled, we
|
||||
// need to add an explicit value. Alternatively, if we have an explicit type, we
|
||||
// need to add an explicit initial value as per the API signature of `input()`.
|
||||
if (initialValue === undefined && (strictPropertyInitialization || resolvedType !== undefined)) {
|
||||
// TODO: Consider initializations inside the constructor. Those are not migrated right now
|
||||
// though, as they are writes.
|
||||
|
||||
// TODO: We can use the `input()` shorthand if there is a question mark?
|
||||
// We can assume `undefined` is part of the type already, either already was included, or
|
||||
// we added synthetically as part of the preparation.
|
||||
initialValue = ts.factory.createIdentifier('undefined');
|
||||
}
|
||||
|
||||
// Always add an initial value when the input is optional, and we have one, or we need one
|
||||
// to be able to pass options as the second argument.
|
||||
if (!metadata.required && (initialValue !== undefined || optionsLiteral !== null)) {
|
||||
@@ -128,7 +139,6 @@ export function convertToSignalInput(
|
||||
function extractTransformOfInput(
|
||||
transform: DecoratorInputTransform,
|
||||
resolvedType: ts.TypeNode | undefined,
|
||||
isResolvedTypeCheckable: boolean,
|
||||
checker: ts.TypeChecker,
|
||||
): ts.PropertyAssignment {
|
||||
assert(ts.isExpression(transform.node), `Expected transform to be an expression.`);
|
||||
@@ -138,7 +148,10 @@ function extractTransformOfInput(
|
||||
// In some cases, the transform function is not compatible because with decorator inputs,
|
||||
// those were not checked. We cast the transform to `any` and add a TODO.
|
||||
// TODO: Insert a TODO and capture this in the design doc.
|
||||
if (resolvedType !== undefined && isResolvedTypeCheckable) {
|
||||
if (resolvedType !== undefined && !ts.isSyntheticExpression(resolvedType)) {
|
||||
// Note: If the type is synthetic, we cannot check, and we accept that in the worst case
|
||||
// we will create code that is not necessarily compiling. This is unlikely, but notably
|
||||
// the errors would be correct and valuable.
|
||||
const transformType = checker.getTypeAtLocation(transform.node);
|
||||
const transformSignature = transformType.getCallSignatures()[0];
|
||||
assert(transformSignature !== undefined, 'Expected transform to be an invoke-able.');
|
||||
|
||||
+18
-10
@@ -20,7 +20,8 @@ import {InputNode} from '../input_detection/input_node';
|
||||
*/
|
||||
export interface ConvertInputPreparation {
|
||||
resolvedType: ts.TypeNode | undefined;
|
||||
isResolvedTypeCheckable: boolean;
|
||||
preferShorthandIfPossible: {originalType: ts.TypeNode} | null;
|
||||
isUndefinedInitialValue: boolean;
|
||||
resolvedMetadata: ExtractedInput;
|
||||
}
|
||||
|
||||
@@ -53,18 +54,27 @@ export function prepareAndCheckForConversion(
|
||||
metadata.required = true;
|
||||
}
|
||||
|
||||
const isUndefinedInitialValue =
|
||||
node.initializer === undefined ||
|
||||
(ts.isIdentifier(node.initializer) && node.initializer.text === 'undefined');
|
||||
let typeToAdd: ts.TypeNode | undefined = node.type;
|
||||
let isResolvedTypeCheckable = true;
|
||||
let preferShorthandIfPossible: {originalType: ts.TypeNode} | null = null;
|
||||
|
||||
// If the input was using `@Input() bla?: string;`, then we try to explicitly
|
||||
// add `undefined` as type, if it's not part of the type already.
|
||||
// If there is no initial value, or it's `undefined`, we can prefer the `input()`
|
||||
// shorthand which automatically uses `undefined` as initial value, and includes it
|
||||
// in the input type.
|
||||
if (!metadata.required && node.type !== undefined && isUndefinedInitialValue) {
|
||||
preferShorthandIfPossible = {originalType: node.type};
|
||||
}
|
||||
|
||||
// If the input is using `@Input() bla?: string;` with the "optional question mark",
|
||||
// then we try to explicitly add `undefined` as type, if it's not part of the type already.
|
||||
// This is ensuring correctness, as `bla?` automatically includes `undefined` currently.
|
||||
if (
|
||||
node.type !== undefined &&
|
||||
node.questionToken !== undefined &&
|
||||
!checker.isTypeAssignableTo(checker.getUndefinedType(), checker.getTypeFromTypeNode(node.type))
|
||||
) {
|
||||
// Synthetic types are never checkable.
|
||||
isResolvedTypeCheckable = false;
|
||||
typeToAdd = ts.factory.createUnionTypeNode([
|
||||
node.type,
|
||||
ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword),
|
||||
@@ -74,9 +84,6 @@ export function prepareAndCheckForConversion(
|
||||
// Attempt to extract type from input initial value. No explicit type, but input is required.
|
||||
// Hence we need an explicit type, or fall back to `typeof`.
|
||||
if (typeToAdd === undefined && initialValue !== undefined && metadata.required) {
|
||||
// Synthetic types are never checkable.
|
||||
isResolvedTypeCheckable = false;
|
||||
|
||||
const propertyType = checker.getTypeAtLocation(node);
|
||||
if (propertyType.flags & ts.TypeFlags.Boolean) {
|
||||
typeToAdd = ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
||||
@@ -108,7 +115,8 @@ export function prepareAndCheckForConversion(
|
||||
|
||||
return {
|
||||
resolvedMetadata: metadata,
|
||||
isResolvedTypeCheckable,
|
||||
resolvedType: typeToAdd,
|
||||
preferShorthandIfPossible,
|
||||
isUndefinedInitialValue,
|
||||
};
|
||||
}
|
||||
|
||||
+1
-3
@@ -11,7 +11,6 @@ import {MigrationResult} from '../result';
|
||||
import {Replacement} from '../replacement';
|
||||
import {convertToSignalInput} from '../convert-input/convert_to_signal';
|
||||
import assert from 'assert';
|
||||
import {MigrationHost} from '../migration_host';
|
||||
import {KnownInputs} from '../input_detection/known_inputs';
|
||||
import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator';
|
||||
|
||||
@@ -20,7 +19,6 @@ import {ImportManager} from '../../../../../../compiler-cli/src/ngtsc/translator
|
||||
* manages imports within the given file.
|
||||
*/
|
||||
export function pass6__migrateInputDeclarations(
|
||||
host: MigrationHost,
|
||||
checker: ts.TypeChecker,
|
||||
result: MigrationResult,
|
||||
knownInputs: KnownInputs,
|
||||
@@ -46,7 +44,7 @@ export function pass6__migrateInputDeclarations(
|
||||
new Replacement(
|
||||
input.node.getStart(),
|
||||
input.node.getEnd(),
|
||||
convertToSignalInput(host, input.node, metadata, checker, importManager),
|
||||
convertToSignalInput(input.node, metadata, checker, importManager),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export function executeMigrationPhase(
|
||||
|
||||
// Migrate passes.
|
||||
pass5__migrateTypeScriptReferences(result, typeChecker, knownInputs);
|
||||
pass6__migrateInputDeclarations(host, typeChecker, result, knownInputs, importManager);
|
||||
pass6__migrateInputDeclarations(typeChecker, result, knownInputs, importManager);
|
||||
pass7__migrateTemplateReferences(host, result, knownInputs);
|
||||
pass8__migrateHostBindings(result, knownInputs);
|
||||
pass9__migrateTypeScriptTypeReferences(result, knownInputs, importManager);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import ts from 'typescript';
|
||||
|
||||
export function removeFromUnionIfPossible(
|
||||
union: ts.UnionTypeNode,
|
||||
filter: (v: ts.TypeNode) => boolean,
|
||||
): ts.UnionTypeNode {
|
||||
const filtered = union.types.filter(filter);
|
||||
if (filtered.length === union.types.length) {
|
||||
return union;
|
||||
}
|
||||
return ts.factory.updateUnionTypeNode(union, ts.factory.createNodeArray(filtered));
|
||||
}
|
||||
@@ -374,7 +374,7 @@ export class AppComponent {
|
||||
input = input<string | null>(null);
|
||||
bla = input.required<boolean, string | boolean>({ transform: disabledTransform });
|
||||
narrowableMultipleTimes = input<Vehicle | null>(null);
|
||||
withUndefinedInput = input<string | undefined>(undefined);
|
||||
withUndefinedInput = input<string>();
|
||||
@Input() incompatible: string | null = null;
|
||||
|
||||
private _bla: any;
|
||||
@@ -720,7 +720,7 @@ import { Directive, input } from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
class OptionalInput {
|
||||
bla = input<string | undefined>(undefined);
|
||||
bla = input<string>();
|
||||
}
|
||||
@@@@@@ problematic_type_reference.ts @@@@@@
|
||||
|
||||
@@ -1065,5 +1065,5 @@ class WithJsdoc {
|
||||
*/
|
||||
simpleInput = input.required<string>();
|
||||
|
||||
withCommentInside = input</* intended */ boolean | undefined>(undefined);
|
||||
withCommentInside = input</* intended */ boolean>();
|
||||
}
|
||||
|
||||
@@ -374,7 +374,7 @@ export class AppComponent {
|
||||
input = input<string | null>(null);
|
||||
bla = input.required<boolean, string | boolean>({ transform: disabledTransform });
|
||||
narrowableMultipleTimes = input<Vehicle | null>(null);
|
||||
withUndefinedInput = input<string | undefined>(undefined);
|
||||
withUndefinedInput = input<string>();
|
||||
incompatible = input<string | null>(null);
|
||||
|
||||
private _bla: any;
|
||||
@@ -720,7 +720,7 @@ import { Directive, input } from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
class OptionalInput {
|
||||
bla = input<string | undefined>(undefined);
|
||||
bla = input<string>();
|
||||
}
|
||||
@@@@@@ problematic_type_reference.ts @@@@@@
|
||||
|
||||
@@ -1065,5 +1065,5 @@ class WithJsdoc {
|
||||
*/
|
||||
simpleInput = input.required<string>();
|
||||
|
||||
withCommentInside = input</* intended */ boolean | undefined>(undefined);
|
||||
withCommentInside = input</* intended */ boolean>();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user