diff --git a/packages/core/schematics/migrations/safe-optional-chaining/migration.ts b/packages/core/schematics/migrations/safe-optional-chaining/migration.ts index b116a5e1132c..dc890809d4fc 100644 --- a/packages/core/schematics/migrations/safe-optional-chaining/migration.ts +++ b/packages/core/schematics/migrations/safe-optional-chaining/migration.ts @@ -68,6 +68,8 @@ export interface MigrationConfig { shouldMigrate?: (containingFile: ProjectFile) => boolean; } +const SAFE_NAVIGATION_MIGRATION_FN = '$safeNavigationMigration'; + /** * This migration wraps optional chaining expressions in Angular templates with a call to the * `$safeNavigationMigration()` magic function. This function doesn't exist at runtime, but is @@ -371,6 +373,11 @@ class ExpressionMigrator extends RecursiveAstVisitor { // --------------------------------------------------------------------------- override visitCall(ast: Call, nullSensitive: boolean): any { + if (isSafeNavigationMigrationCall(ast)) { + this.visit(ast.receiver, false); + return; + } + if (nullSensitive && this.hasSafeReceiver(ast.receiver)) { this.addReplacement(ast); } @@ -503,6 +510,16 @@ function isNullishLiteralAST(ast: AST): boolean { ); } +function isSafeNavigationMigrationCall(ast: AST): boolean { + const innerAst = ast instanceof ASTWithSource ? ast.ast : ast; + + return ( + innerAst instanceof Call && + innerAst.receiver instanceof PropertyRead && + innerAst.receiver.name === SAFE_NAVIGATION_MIGRATION_FN + ); +} + /** Returns true if the AST node is a non-null, non-undefined primitive literal. */ function isNonNullishLiteralAST(ast: AST): boolean { const innerAst = ast instanceof ASTWithSource ? ast.ast : ast; diff --git a/packages/core/schematics/migrations/safe-optional-chaining/safe-optional-chaining.spec.ts b/packages/core/schematics/migrations/safe-optional-chaining/safe-optional-chaining.spec.ts index 31d098e32251..7dd446f50b46 100644 --- a/packages/core/schematics/migrations/safe-optional-chaining/safe-optional-chaining.spec.ts +++ b/packages/core/schematics/migrations/safe-optional-chaining/safe-optional-chaining.spec.ts @@ -515,27 +515,27 @@ describe('SafeOptionalChainingMigration', () => { {{ foo?.bar | json }}
+ {{ compute(my?.utility?.service(my?.optional?.argument)) }} `; // First pass: migrate fresh code - const firstPass = await migrateInlineTemplate(input); + const firstPass = await migrateInlineTemplateContent(input); // Verify all expressions are wrapped correctly on first pass expect(firstPass).toContain('{{ compute($safeNavigationMigration(foo?.bar)) }}'); expect(firstPass).toContain('{{ $safeNavigationMigration(foo?.bar) | json }}'); expect(firstPass).toContain(''); expect(firstPass).toContain(''); + expect(firstPass).toContain( + '{{ compute($safeNavigationMigration(my?.utility?.service($safeNavigationMigration(my?.optional?.argument)))) }}', + ); // Second pass: run migration again on already-migrated code - const secondPass = await migrateInlineTemplate(firstPass); + const secondPass = await migrateInlineTemplateContent(firstPass); // Verify no double-wrapping occurred expect(secondPass).not.toContain('$safeNavigationMigration($safeNavigationMigration'); - // The already-wrapped expressions should remain unchanged - expect(secondPass).toContain('{{ compute($safeNavigationMigration(foo?.bar)) }}'); - expect(secondPass).toContain('{{ $safeNavigationMigration(foo?.bar) | json }}'); - expect(secondPass).toContain(''); - expect(secondPass).toContain(''); + expect(normalizeTemplateContent(secondPass)).toBe(normalizeTemplateContent(firstPass)); }); }); @@ -552,13 +552,28 @@ async function migrateInlineTemplate(template: string): Promise