From ebe0c168e26894e5ee27e417d5efaa3eb0247145 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 10 Sep 2024 18:32:35 +0000 Subject: [PATCH] refactor(migrations): share reference migration code of signal input migration (#57766) The reference migration code of the signal input migration should be re-usable as the queries migration needs the exact same. This builds on top of the shared reference resolution logic from previous commits. Similarly this commit introduces a small "host" for providing necessary configurable information about what references should be migrated; supporting e.g. "input incompatibilities". This is important as e.g. queries may also be incompatible for migration and references to such, should not be migrated. PR Close #57766 --- .../src/passes/5_migrate_ts_references.ts | 82 ++-------------- .../passes/7_migrate_template_references.ts | 19 ++-- .../src/passes/8_migrate_host_bindings.ts | 18 ++-- .../passes/9_migrate_ts_type_references.ts | 28 +++--- .../helpers}/create_block_arrow_function.ts | 2 +- .../helpers}/object_expansion_refs.ts | 14 +-- .../helpers}/standard_reference.ts | 23 +++-- .../migrate_host_bindings.ts | 62 ++++++++++++ .../migrate_template_references.ts | 57 +++++++++++ .../migrate_ts_references.ts | 96 +++++++++++++++++++ .../migrate_ts_type_references.ts | 77 +++++++++++++++ .../reference_migration_host.ts | 18 ++++ .../signal-migration/src/phase_analysis.ts | 5 +- .../signal-migration/src/phase_migrate.ts | 33 +++++-- 14 files changed, 395 insertions(+), 139 deletions(-) rename packages/core/schematics/migrations/signal-migration/src/passes/{migrate_ts_reference => reference_migration/helpers}/create_block_arrow_function.ts (98%) rename packages/core/schematics/migrations/signal-migration/src/passes/{migrate_ts_reference => reference_migration/helpers}/object_expansion_refs.ts (94%) rename packages/core/schematics/migrations/signal-migration/src/passes/{migrate_ts_reference => reference_migration/helpers}/standard_reference.ts (88%) create mode 100644 packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_host_bindings.ts create mode 100644 packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_template_references.ts create mode 100644 packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_references.ts create mode 100644 packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_type_references.ts create mode 100644 packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/reference_migration_host.ts diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/5_migrate_ts_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/5_migrate_ts_references.ts index 16bc8a7e83f..0267b0505e3 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/5_migrate_ts_references.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/5_migrate_ts_references.ts @@ -7,89 +7,23 @@ */ import ts from 'typescript'; -import {KnownInputs} from '../input_detection/known_inputs'; -import {MigrationResult} from '../result'; -import { - migrateBindingElementInputReference, - IdentifierOfBindingElement, -} from './migrate_ts_reference/object_expansion_refs'; -import { - migrateStandardTsReference, - NarrowableTsReferences, -} from './migrate_ts_reference/standard_reference'; +import {migrateTypeScriptReferences} from './reference_migration/migrate_ts_references'; import {ProgramInfo} from '../../../../utils/tsurge'; -import {ClassFieldUniqueKey} from './reference_resolution/known_fields'; -import {isTsReference} from './reference_resolution/reference_kinds'; +import {Reference} from './reference_resolution/reference_kinds'; +import {ClassFieldDescriptor} from './reference_resolution/known_fields'; +import {ReferenceMigrationHost} from './reference_migration/reference_migration_host'; /** * Phase that migrates TypeScript input references to be signal compatible. * * The phase takes care of control flow analysis and generates temporary variables * where needed to ensure narrowing continues to work. E.g. - * - * ``` - * someMethod() { - * if (this.input) { - * this.input.charAt(0); - * } - * } - * ``` - * - * will be transformed into: - * - * ``` - * someMethod() { - * const input_1 = this.input(); - * if (input_1) { - * input_1.charAt(0); - * } - * } - * ``` */ -export function pass5__migrateTypeScriptReferences( - result: MigrationResult, +export function pass5__migrateTypeScriptReferences( + host: ReferenceMigrationHost, + references: Reference[], checker: ts.TypeChecker, - knownInputs: KnownInputs, info: ProgramInfo, ) { - const tsReferencesWithNarrowing = new Map(); - const tsReferencesInBindingElements = new Set(); - - const seenIdentifiers = new WeakSet(); - - for (const reference of result.references) { - // This pass only deals with TS references. - if (!isTsReference(reference)) { - continue; - } - // Skip references to incompatible inputs. - if (knownInputs.get(reference.target)!.isIncompatible()) { - continue; - } - // Never attempt to migrate write references. - // Those usually invalidate the target input most of the time, but in - // best-effort mode they are not. - if (reference.from.isWrite) { - continue; - } - // Skip duplicate references. E.g. in batching. - if (seenIdentifiers.has(reference.from.node)) { - continue; - } - seenIdentifiers.add(reference.from.node); - - const targetKey = reference.target.key; - - if (reference.from.isPartOfElementBinding) { - tsReferencesInBindingElements.add(reference.from.node as IdentifierOfBindingElement); - } else { - if (!tsReferencesWithNarrowing.has(targetKey)) { - tsReferencesWithNarrowing.set(targetKey, {accesses: []}); - } - tsReferencesWithNarrowing.get(targetKey)!.accesses.push(reference.from.node); - } - } - - migrateBindingElementInputReference(tsReferencesInBindingElements, info, result); - migrateStandardTsReference(tsReferencesWithNarrowing, checker, result, info); + migrateTypeScriptReferences(host, references, checker, info); } diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/7_migrate_template_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/7_migrate_template_references.ts index 9666b86f1a5..ee2b1758335 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/7_migrate_template_references.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/7_migrate_template_references.ts @@ -6,28 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {MigrationResult} from '../result'; -import {KnownInputs} from '../input_detection/known_inputs'; import {Replacement, TextUpdate} from '../../../../utils/tsurge'; -import {isTemplateReference} from './reference_resolution/reference_kinds'; - +import {ReferenceMigrationHost} from './reference_migration/reference_migration_host'; +import {ClassFieldDescriptor} from './reference_resolution/known_fields'; +import {isTemplateReference, Reference} from './reference_resolution/reference_kinds'; /** * Phase that migrates Angular template references to * unwrap signals. */ -export function pass7__migrateTemplateReferences( - result: MigrationResult, - knownInputs: KnownInputs, +export function pass7__migrateTemplateReferences( + host: ReferenceMigrationHost, + references: Reference[], ) { const seenFileReferences = new Set(); - for (const reference of result.references) { + for (const reference of references) { // This pass only deals with HTML template references. if (!isTemplateReference(reference)) { continue; } // Skip references to incompatible inputs. - if (knownInputs.get(reference.target)!.isIncompatible()) { + if (!host.shouldMigrateReferencesToField(reference.target)) { continue; } @@ -43,7 +42,7 @@ export function pass7__migrateTemplateReferences( ? `: ${reference.from.read.name}()` : `()`; - result.replacements.push( + host.replacements.push( new Replacement( reference.from.templateFile, new TextUpdate({ diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/8_migrate_host_bindings.ts b/packages/core/schematics/migrations/signal-migration/src/passes/8_migrate_host_bindings.ts index bba42e1b617..458e8ffe19c 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/8_migrate_host_bindings.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/8_migrate_host_bindings.ts @@ -7,29 +7,29 @@ */ import ts from 'typescript'; +import {ReferenceMigrationHost} from './reference_migration/reference_migration_host'; +import {ClassFieldDescriptor} from './reference_resolution/known_fields'; +import {isHostBindingReference, Reference} from './reference_resolution/reference_kinds'; import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../utils/tsurge'; -import {MigrationResult} from '../result'; -import {KnownInputs} from '../input_detection/known_inputs'; -import {isHostBindingReference} from './reference_resolution/reference_kinds'; /** * Phase that migrates Angular host binding references to * unwrap signals. */ -export function pass8__migrateHostBindings( - result: MigrationResult, - knownInputs: KnownInputs, +export function pass8__migrateHostBindings( + host: ReferenceMigrationHost, + references: Reference[], info: ProgramInfo, ) { const seenReferences = new WeakMap>(); - for (const reference of result.references) { + for (const reference of references) { // This pass only deals with host binding references. if (!isHostBindingReference(reference)) { continue; } // Skip references to incompatible inputs. - if (knownInputs.get(reference.target)!.isIncompatible()) { + if (!host.shouldMigrateReferencesToField(reference.target)) { continue; } @@ -52,7 +52,7 @@ export function pass8__migrateHostBindings( ? `: ${reference.from.read.name}()` : `()`; - result.replacements.push( + host.replacements.push( new Replacement( projectFile(bindingField.getSourceFile(), info), new TextUpdate({position: readEndPos, end: readEndPos, toInsert: appendText}), diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/9_migrate_ts_type_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/9_migrate_ts_type_references.ts index 958438209c2..a71bff055bc 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/9_migrate_ts_type_references.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/9_migrate_ts_type_references.ts @@ -6,13 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import ts from 'typescript'; -import {KnownInputs} from '../input_detection/known_inputs'; -import {MigrationResult} from '../result'; -import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../utils/tsurge'; -import assert from 'assert'; import {ImportManager} from '@angular/compiler-cli/src/ngtsc/translator'; -import {isTsClassTypeReference} from './reference_resolution/reference_kinds'; +import assert from 'assert'; +import ts from 'typescript'; +import {ReferenceMigrationHost} from './reference_migration/reference_migration_host'; +import {ClassFieldDescriptor} from './reference_resolution/known_fields'; +import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../utils/tsurge'; +import {isTsClassTypeReference, Reference} from './reference_resolution/reference_kinds'; /** * Migrates TypeScript "ts.Type" references. E.g. @@ -20,21 +20,21 @@ import {isTsClassTypeReference} from './reference_resolution/reference_kinds'; * - `Partial` will be converted to `UnwrapSignalInputs>`. in Catalyst test files. */ -export function pass9__migrateTypeScriptTypeReferences( - result: MigrationResult, - knownInputs: KnownInputs, +export function pass9__migrateTypeScriptTypeReferences( + host: ReferenceMigrationHost, + references: Reference[], importManager: ImportManager, info: ProgramInfo, ) { const seenTypeNodes = new WeakSet(); - for (const reference of result.references) { + for (const reference of references) { // This pass only deals with TS input class type references. if (!isTsClassTypeReference(reference)) { continue; } // Skip references to classes that are not fully migrated. - if (knownInputs.getDirectiveInfoForClass(reference.target)?.hasIncompatibleMembers()) { + if (!host.shouldMigrateReferencesToClass(reference.target)) { continue; } // Skip duplicate references. E.g. in batching. @@ -62,17 +62,17 @@ export function pass9__migrateTypeScriptTypeReferences( requestedFile: sf, }); - result.replacements.push( + host.replacements.push( new Replacement( projectFile(sf, info), new TextUpdate({ position: firstArg.getStart(), end: firstArg.getStart(), - toInsert: `${result.printer.printNode(ts.EmitHint.Unspecified, unwrapImportExpr, sf)}<`, + toInsert: `${host.printer.printNode(ts.EmitHint.Unspecified, unwrapImportExpr, sf)}<`, }), ), ); - result.replacements.push( + host.replacements.push( new Replacement( projectFile(sf, info), new TextUpdate({position: firstArg.getEnd(), end: firstArg.getEnd(), toInsert: '>'}), diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/create_block_arrow_function.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/create_block_arrow_function.ts similarity index 98% rename from packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/create_block_arrow_function.ts rename to packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/create_block_arrow_function.ts index d6ee80c0d19..e183ef6447b 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/create_block_arrow_function.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/create_block_arrow_function.ts @@ -7,7 +7,7 @@ */ import ts from 'typescript'; -import {ProjectFile, Replacement, TextUpdate} from '../../../../../utils/tsurge'; +import {ProjectFile, Replacement, TextUpdate} from '../../../../../../utils/tsurge'; /** * Creates replacements to insert the given statement as diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/object_expansion_refs.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/object_expansion_refs.ts similarity index 94% rename from packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/object_expansion_refs.ts rename to packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/object_expansion_refs.ts index 44031317a88..4d3c7534964 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/object_expansion_refs.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/object_expansion_refs.ts @@ -13,11 +13,10 @@ import { ProgramInfo, projectFile, ProjectFile, -} from '../../../../../utils/tsurge'; -import {getBindingElementDeclaration} from '../../utils/binding_elements'; -import {UniqueNamesGenerator} from '../../utils/unique_names'; +} from '../../../../../../utils/tsurge'; +import {getBindingElementDeclaration} from '../../../utils/binding_elements'; +import {UniqueNamesGenerator} from '../../../utils/unique_names'; import assert from 'assert'; -import {MigrationResult} from '../../result'; import {createNewBlockToInsertVariable} from './create_block_arrow_function'; /** An identifier part of a binding element. */ @@ -44,7 +43,8 @@ export interface IdentifierOfBindingElement extends ts.Identifier { export function migrateBindingElementInputReference( tsReferencesInBindingElements: Set, info: ProgramInfo, - result: MigrationResult, + replacements: Replacement[], + printer: ts.Printer, ) { const nameGenerator = new UniqueNamesGenerator(['Input', 'Signal', 'Ref']); @@ -90,13 +90,13 @@ export function migrateBindingElementInputReference( continue; } - result.replacements.push( + replacements.push( new Replacement( file, new TextUpdate({ position: bindingElement.getStart(), end: bindingElement.getEnd(), - toInsert: result.printer.printNode( + toInsert: printer.printNode( ts.EmitHint.Unspecified, newBindingToAccessInputField, sourceFile, diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/standard_reference.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/standard_reference.ts similarity index 88% rename from packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/standard_reference.ts rename to packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/standard_reference.ts index 0991009bad3..de8b48ed8ff 100644 --- a/packages/core/schematics/migrations/signal-migration/src/passes/migrate_ts_reference/standard_reference.ts +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/helpers/standard_reference.ts @@ -7,12 +7,11 @@ */ import ts from 'typescript'; -import {analyzeControlFlow} from '../../flow_analysis'; -import {MigrationResult} from '../../result'; -import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../../utils/tsurge'; -import {traverseAccess} from '../../utils/traverse_access'; -import {UniqueNamesGenerator} from '../../utils/unique_names'; -import {createNewBlockToInsertVariable} from './create_block_arrow_function'; +import {analyzeControlFlow} from '../../../flow_analysis'; +import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../../../utils/tsurge'; +import {traverseAccess} from '../../../utils/traverse_access'; +import {UniqueNamesGenerator} from '../../../utils/unique_names'; +import {createNewBlockToInsertVariable} from '../helpers/create_block_arrow_function'; export interface NarrowableTsReferences { accesses: ts.Identifier[]; @@ -21,8 +20,8 @@ export interface NarrowableTsReferences { export function migrateStandardTsReference( tsReferencesWithNarrowing: Map, checker: ts.TypeChecker, - result: MigrationResult, info: ProgramInfo, + replacements: Replacement[], ) { const nameGenerator = new UniqueNamesGenerator(['Value', 'Val', 'Input']); @@ -38,7 +37,7 @@ export function migrateStandardTsReference( // Unwrap the signal directly. if (recommendedNode === 'preserve') { // Append `()` to unwrap the signal. - result.replacements.push( + replacements.push( new Replacement( projectFile(sf, info), new TextUpdate({ @@ -55,7 +54,7 @@ export function migrateStandardTsReference( // with the temporary variable. if (typeof recommendedNode === 'number') { const replaceNode = traverseAccess(originalNode); - result.replacements.push( + replacements.push( new Replacement( projectFile(sf, info), new TextUpdate({ @@ -94,13 +93,13 @@ export function migrateStandardTsReference( // without a block, convert the arrow function to a block and insert the temporary // variable at the beginning. if (ts.isArrowFunction(parent) && !ts.isBlock(parent.body)) { - result.replacements.push( + replacements.push( ...createNewBlockToInsertVariable(parent, filePath, temporaryVariableStr), ); } else { const leadingSpace = ts.getLineAndCharacterOfPosition(sf, referenceNodeInBlock.getStart()); - result.replacements.push( + replacements.push( new Replacement( filePath, new TextUpdate({ @@ -112,7 +111,7 @@ export function migrateStandardTsReference( ); } - result.replacements.push( + replacements.push( new Replacement( projectFile(sf, info), new TextUpdate({ diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_host_bindings.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_host_bindings.ts new file mode 100644 index 00000000000..bde8519b80d --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_host_bindings.ts @@ -0,0 +1,62 @@ +/** + * @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'; +import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../../utils/tsurge'; +import {ClassFieldDescriptor} from '../reference_resolution/known_fields'; +import {isHostBindingReference, Reference} from '../reference_resolution/reference_kinds'; +import {ReferenceMigrationHost} from './reference_migration_host'; + +/** + * Phase that migrates Angular host binding references to + * unwrap signals. + */ +export function migrateHostBindings( + host: ReferenceMigrationHost, + references: Reference[], + info: ProgramInfo, +) { + const seenReferences = new WeakMap>(); + + for (const reference of references) { + // This pass only deals with host binding references. + if (!isHostBindingReference(reference)) { + continue; + } + // Skip references to incompatible inputs. + if (!host.shouldMigrateReferencesToField(reference.target)) { + continue; + } + + const bindingField = reference.from.hostPropertyNode; + const expressionOffset = bindingField.getStart() + 1; // account for quotes. + const readEndPos = expressionOffset + reference.from.read.sourceSpan.end; + + // Skip duplicate references. Can happen if the host object is shared. + if (seenReferences.get(bindingField)?.has(readEndPos)) { + continue; + } + if (seenReferences.has(bindingField)) { + seenReferences.get(bindingField)!.add(readEndPos); + } else { + seenReferences.set(bindingField, new Set([readEndPos])); + } + + // Expand shorthands like `{bla}` to `{bla: bla()}`. + const appendText = reference.from.isObjectShorthandExpression + ? `: ${reference.from.read.name}()` + : `()`; + + host.replacements.push( + new Replacement( + projectFile(bindingField.getSourceFile(), info), + new TextUpdate({position: readEndPos, end: readEndPos, toInsert: appendText}), + ), + ); + } +} diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_template_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_template_references.ts new file mode 100644 index 00000000000..37e5b2149de --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_template_references.ts @@ -0,0 +1,57 @@ +/** + * @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 {Replacement, TextUpdate} from '../../../../../utils/tsurge'; +import {ClassFieldDescriptor} from '../reference_resolution/known_fields'; +import {isTemplateReference, Reference} from '../reference_resolution/reference_kinds'; +import {ReferenceMigrationHost} from './reference_migration_host'; + +/** + * Phase that migrates Angular template references to + * unwrap signals. + */ +export function migrateTemplateReferences( + host: ReferenceMigrationHost, + references: Reference[], +) { + const seenFileReferences = new Set(); + + for (const reference of references) { + // This pass only deals with HTML template references. + if (!isTemplateReference(reference)) { + continue; + } + // Skip references to incompatible inputs. + if (!host.shouldMigrateReferencesToField(reference.target)) { + continue; + } + + // Skip duplicate references. E.g. if a template is shared. + const fileReferenceId = `${reference.from.templateFile.id}:${reference.from.read.sourceSpan.end}`; + if (seenFileReferences.has(fileReferenceId)) { + continue; + } + seenFileReferences.add(fileReferenceId); + + // Expand shorthands like `{bla}` to `{bla: bla()}`. + const appendText = reference.from.isObjectShorthandExpression + ? `: ${reference.from.read.name}()` + : `()`; + + host.replacements.push( + new Replacement( + reference.from.templateFile, + new TextUpdate({ + position: reference.from.read.sourceSpan.end, + end: reference.from.read.sourceSpan.end, + toInsert: appendText, + }), + ), + ); + } +} diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_references.ts new file mode 100644 index 00000000000..4adaf5d15fb --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_references.ts @@ -0,0 +1,96 @@ +/** + * @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'; +import { + migrateBindingElementInputReference, + IdentifierOfBindingElement, +} from './helpers/object_expansion_refs'; +import {migrateStandardTsReference, NarrowableTsReferences} from './helpers/standard_reference'; +import {ProgramInfo} from '../../../../../utils/tsurge'; +import {ClassFieldDescriptor, ClassFieldUniqueKey} from '../reference_resolution/known_fields'; +import {isTsReference, Reference} from '../reference_resolution/reference_kinds'; +import {ReferenceMigrationHost} from './reference_migration_host'; + +/** + * Migrates TypeScript input references to be signal compatible. + * + * The phase takes care of control flow analysis and generates temporary variables + * where needed to ensure narrowing continues to work. E.g. + * + * ``` + * someMethod() { + * if (this.input) { + * this.input.charAt(0); + * } + * } + * ``` + * + * will be transformed into: + * + * ``` + * someMethod() { + * const input_1 = this.input(); + * if (input_1) { + * input_1.charAt(0); + * } + * } + * ``` + */ +export function migrateTypeScriptReferences( + host: ReferenceMigrationHost, + references: Reference[], + checker: ts.TypeChecker, + info: ProgramInfo, +) { + const tsReferencesWithNarrowing = new Map(); + const tsReferencesInBindingElements = new Set(); + + const seenIdentifiers = new WeakSet(); + + for (const reference of references) { + // This pass only deals with TS references. + if (!isTsReference(reference)) { + continue; + } + // Skip references to incompatible inputs. + if (!host.shouldMigrateReferencesToField(reference.target)) { + continue; + } + // Never attempt to migrate write references. + // Those usually invalidate the target input most of the time, but in + // best-effort mode they are not. + if (reference.from.isWrite) { + continue; + } + // Skip duplicate references. E.g. in batching. + if (seenIdentifiers.has(reference.from.node)) { + continue; + } + seenIdentifiers.add(reference.from.node); + + const targetKey = reference.target.key; + + if (reference.from.isPartOfElementBinding) { + tsReferencesInBindingElements.add(reference.from.node as IdentifierOfBindingElement); + } else { + if (!tsReferencesWithNarrowing.has(targetKey)) { + tsReferencesWithNarrowing.set(targetKey, {accesses: []}); + } + tsReferencesWithNarrowing.get(targetKey)!.accesses.push(reference.from.node); + } + } + + migrateBindingElementInputReference( + tsReferencesInBindingElements, + info, + host.replacements, + host.printer, + ); + migrateStandardTsReference(tsReferencesWithNarrowing, checker, info, host.replacements); +} diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_type_references.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_type_references.ts new file mode 100644 index 00000000000..694f2ca0e46 --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/migrate_ts_type_references.ts @@ -0,0 +1,77 @@ +/** + * @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'; +import {KnownInputs} from '../../input_detection/known_inputs'; +import {MigrationResult} from '../../result'; +import {ProgramInfo, projectFile, Replacement, TextUpdate} from '../../../../../utils/tsurge'; +import assert from 'assert'; +import {ImportManager} from '@angular/compiler-cli/src/ngtsc/translator'; +import {isTsClassTypeReference} from '../reference_resolution/reference_kinds'; + +/** + * Migrates TypeScript "ts.Type" references. E.g. + + * - `Partial` will be converted to `UnwrapSignalInputs>`. + in Catalyst test files. + */ +export function migrateTypeScriptTypeReferences( + result: MigrationResult, + knownInputs: KnownInputs, + importManager: ImportManager, + info: ProgramInfo, +) { + const seenTypeNodes = new WeakSet(); + + for (const reference of result.references) { + // This pass only deals with TS input class type references. + if (!isTsClassTypeReference(reference)) { + continue; + } + // Skip references to classes that are not fully migrated. + if (knownInputs.getDirectiveInfoForClass(reference.target)?.hasIncompatibleMembers()) { + continue; + } + // Skip duplicate references. E.g. in batching. + if (seenTypeNodes.has(reference.from.node)) { + continue; + } + seenTypeNodes.add(reference.from.node); + + if (reference.isPartialReference && reference.isPartOfCatalystFile) { + assert(reference.from.node.typeArguments, 'Expected type arguments for partial reference.'); + assert(reference.from.node.typeArguments.length === 1, 'Expected an argument for reference.'); + + const firstArg = reference.from.node.typeArguments[0]; + const sf = firstArg.getSourceFile(); + + const unwrapImportExpr = importManager.addImport({ + exportModuleSpecifier: 'google3/javascript/angular2/testing/catalyst', + exportSymbolName: 'UnwrapSignalInputs', + requestedFile: sf, + }); + + result.replacements.push( + new Replacement( + projectFile(sf, info), + new TextUpdate({ + position: firstArg.getStart(), + end: firstArg.getStart(), + toInsert: `${result.printer.printNode(ts.EmitHint.Unspecified, unwrapImportExpr, sf)}<`, + }), + ), + ); + result.replacements.push( + new Replacement( + projectFile(sf, info), + new TextUpdate({position: firstArg.getEnd(), end: firstArg.getEnd(), toInsert: '>'}), + ), + ); + } + } +} diff --git a/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/reference_migration_host.ts b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/reference_migration_host.ts new file mode 100644 index 00000000000..fa6409cccd9 --- /dev/null +++ b/packages/core/schematics/migrations/signal-migration/src/passes/reference_migration/reference_migration_host.ts @@ -0,0 +1,18 @@ +/** + * @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'; +import {Replacement} from '../../../../../utils/tsurge'; +import {ClassFieldDescriptor} from '../reference_resolution/known_fields'; + +export interface ReferenceMigrationHost { + shouldMigrateReferencesToField: (descriptor: D) => boolean; + shouldMigrateReferencesToClass: (clazz: ts.ClassDeclaration) => boolean; + replacements: Replacement[]; + printer: ts.Printer; +} diff --git a/packages/core/schematics/migrations/signal-migration/src/phase_analysis.ts b/packages/core/schematics/migrations/signal-migration/src/phase_analysis.ts index 719bf583260..a2d8e7598b9 100644 --- a/packages/core/schematics/migrations/signal-migration/src/phase_analysis.ts +++ b/packages/core/schematics/migrations/signal-migration/src/phase_analysis.ts @@ -117,10 +117,7 @@ export function executeAnalysisPhase( // TODO: Remove this when we support signal narrowing in templates. // https://github.com/angular/angular/pull/55456. if (isTemplateReference(reference)) { - if ( - process.env['MIGRATE_NARROWED_NARROWED_IN_TEMPLATES'] !== '1' && - reference.from.isLikelyPartOfNarrowing - ) { + if (reference.from.isLikelyPartOfNarrowing) { knownInputs.markInputAsIncompatible(reference.target, { reason: InputIncompatibilityReason.PotentiallyNarrowedInTemplateButNoSupportYet, context: null, diff --git a/packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts b/packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts index 87819ad3653..81a66776450 100644 --- a/packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts +++ b/packages/core/schematics/migrations/signal-migration/src/phase_migrate.ts @@ -9,14 +9,16 @@ import {AnalysisProgramInfo} from './analysis_deps'; import {KnownInputs} from './input_detection/known_inputs'; import {MigrationHost} from './migration_host'; -import {pass5__migrateTypeScriptReferences} from './passes/5_migrate_ts_references'; import {pass6__migrateInputDeclarations} from './passes/6_migrate_input_declarations'; -import {pass7__migrateTemplateReferences} from './passes/7_migrate_template_references'; -import {pass8__migrateHostBindings} from './passes/8_migrate_host_bindings'; -import {pass9__migrateTypeScriptTypeReferences} from './passes/9_migrate_ts_type_references'; import {MigrationResult} from './result'; import {pass10_applyImportManager} from './passes/10_apply_import_manager'; import {ImportManager} from '@angular/compiler-cli/src/ngtsc/translator'; +import {InputDescriptor} from './utils/input_id'; +import {ReferenceMigrationHost} from './passes/reference_migration/reference_migration_host'; +import {pass5__migrateTypeScriptReferences} from './passes/5_migrate_ts_references'; +import {pass7__migrateTemplateReferences} from './passes/7_migrate_template_references'; +import {pass8__migrateHostBindings} from './passes/8_migrate_host_bindings'; +import {pass9__migrateTypeScriptTypeReferences} from './passes/9_migrate_ts_type_references'; /** * Executes the migration phase. @@ -40,11 +42,26 @@ export function executeMigrationPhase( generateUniqueIdentifier: () => null, }); + const referenceMigrationHost: ReferenceMigrationHost = { + printer: result.printer, + replacements: result.replacements, + shouldMigrateReferencesToField: (inputDescr) => + knownInputs.has(inputDescr) && knownInputs.get(inputDescr)!.isIncompatible() === false, + shouldMigrateReferencesToClass: (clazz) => + knownInputs.getDirectiveInfoForClass(clazz) !== undefined && + knownInputs.getDirectiveInfoForClass(clazz)!.hasIncompatibleMembers() === false, + }; + // Migrate passes. - pass5__migrateTypeScriptReferences(result, typeChecker, knownInputs, info); + pass5__migrateTypeScriptReferences(referenceMigrationHost, result.references, typeChecker, info); pass6__migrateInputDeclarations(typeChecker, result, knownInputs, importManager, info); - pass7__migrateTemplateReferences(result, knownInputs); - pass8__migrateHostBindings(result, knownInputs, info); - pass9__migrateTypeScriptTypeReferences(result, knownInputs, importManager, info); + pass7__migrateTemplateReferences(referenceMigrationHost, result.references); + pass8__migrateHostBindings(referenceMigrationHost, result.references, info); + pass9__migrateTypeScriptTypeReferences( + referenceMigrationHost, + result.references, + importManager, + info, + ); pass10_applyImportManager(importManager, result, sourceFiles, info); }