From 5d5b2ea72d03ffc2350e66e202cbca0bf491a5f2 Mon Sep 17 00:00:00 2001 From: Suraj Yadav Date: Fri, 24 Jul 2026 00:18:28 +0530 Subject: [PATCH] fix(migrations): correctly detect `then`/`else` keywords in control flow migration The control flow migration determines whether an `*ngIf` uses a `then` and/or `else` clause by regex matching the raw microsyntax string for the literal keywords `then`/`else`. The regexes only checked that the keyword was preceded by a non-word character, but not that it was followed by one. As a result, a template reference name that merely starts with `then` (e.g. `else thenBlock`) or `else` was misidentified as the `then`/`else` keyword itself. This caused the migration to take the wrong code path (e.g. then+else instead of else-only), which in turn made `getTemplateName()` compute a `slice(start, end)` with `start > end`, producing an empty template name. That empty placeholder was never resolved and was silently emitted as an invalid ``, dropping the original template content without any warning. Add a negative lookahead `(?![\w\d])` to both regexes so `then`/`else` are only matched as whole keywords, not as a prefix of a longer template reference name. Fixes #69914 (cherry picked from commit 5ad823139758b4d3a8a021d378b008c3457f8689) --- .../ng-generate/control-flow-migration/ifs.ts | 11 +- .../test/control_flow_migration_spec.ts | 119 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) diff --git a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts index 9bfc983cd49..b6e0f95076a 100644 --- a/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts +++ b/packages/core/schematics/ng-generate/control-flow-migration/ifs.ts @@ -75,8 +75,15 @@ export function migrateIf(template: string): { } function migrateNgIf(etm: ElementToMigrate, tmpl: string, offset: number): Result { - const matchThen = etm.attr.value.match(/[^\w\d];?\s*then/gm); - const matchElse = etm.attr.value.match(/[^\w\d];?\s*else/gm); + // The negative lookahead (?![\w$]) ensures `then`/`else` are matched only as + // whole keywords. Without it, an else/then template reference name that merely + // *starts* with `then`/`else` (e.g. `else thenBlock`) is misidentified as the + // `then` keyword, since `[^\w$];?\s*then` also matches the `then` prefix of + // `thenBlock`. `$` is included alongside `\w` (which already covers digits) + // since it is a valid identifier character in JS/template reference names + // (e.g. `#then$`), but is not part of `\w`. + const matchThen = etm.attr.value.match(/[^\w$];?\s*then(?![\w$])/gm); + const matchElse = etm.attr.value.match(/[^\w$];?\s*else(?![\w$])/gm); if (etm.thenAttr !== undefined || etm.elseAttr !== undefined) { // bound if then / if then else diff --git a/packages/core/schematics/test/control_flow_migration_spec.ts b/packages/core/schematics/test/control_flow_migration_spec.ts index 9b6a7afa835..b2a1ca899f2 100644 --- a/packages/core/schematics/test/control_flow_migration_spec.ts +++ b/packages/core/schematics/test/control_flow_migration_spec.ts @@ -465,6 +465,125 @@ describe('control flow migration (ng update)', () => { ); }); + it('should migrate an if else case where the else template reference name starts with `then`', async () => { + writeFile( + '/comp.ts', + ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + data: any; + } + `, + ); + + writeFile( + '/comp.html', + [ + ``, + `

Hello team member!

`, + `
`, + ``, + `

No team member

`, + `
`, + ].join('\n'), + ); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe( + [ + `@if (data.teamMember) {`, + `

Hello team member!

`, + `} @else {`, + `

No team member

`, + `}\n`, + ].join('\n'), + ); + }); + + it('should migrate an if then case where the then template reference name starts with `else`', async () => { + writeFile( + '/comp.ts', + ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + data: any; + } + `, + ); + + writeFile( + '/comp.html', + [ + ``, + ``, + `

Then content

`, + `
`, + ].join('\n'), + ); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe( + [`@if (data.teamMember) {`, `

Then content

`, `}\n`].join('\n'), + ); + }); + + it('should migrate an if then else case where template reference names start with `then`/`else`', async () => { + writeFile( + '/comp.ts', + ` + import {Component} from '@angular/core'; + import {NgIf} from '@angular/common'; + + @Component({ + templateUrl: './comp.html' + }) + class Comp { + data: any; + } + `, + ); + + writeFile( + '/comp.html', + [ + ``, + ``, + `

Then content

`, + `
`, + ``, + `

Else content

`, + `
`, + ].join('\n'), + ); + + await runMigration(); + const content = tree.readContent('/comp.html'); + + expect(content).toBe( + [ + `@if (data.teamMember) {`, + `

Then content

`, + `} @else {`, + `

Else content

`, + `}\n`, + ].join('\n'), + ); + }); + it('should migrate an if case on a container', async () => { writeFile( '/comp.ts',