mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
`<ng-template [ngTemplateOutlet]=""></ng-template>`, 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 5ad8231397)
This commit is contained in:
committed by
Alex Rickabaugh
parent
1caafa6d8a
commit
5d5b2ea72d
@@ -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
|
||||
|
||||
@@ -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',
|
||||
[
|
||||
`<ng-container *ngIf="data.teamMember; else thenBlock">`,
|
||||
` <h2>Hello team member!</h2>`,
|
||||
`</ng-container>`,
|
||||
`<ng-template #thenBlock>`,
|
||||
` <h2>No team member</h2>`,
|
||||
`</ng-template>`,
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe(
|
||||
[
|
||||
`@if (data.teamMember) {`,
|
||||
` <h2>Hello team member!</h2>`,
|
||||
`} @else {`,
|
||||
` <h2>No team member</h2>`,
|
||||
`}\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',
|
||||
[
|
||||
`<ng-container *ngIf="data.teamMember; then elseyBlock"></ng-container>`,
|
||||
`<ng-template #elseyBlock>`,
|
||||
` <h2>Then content</h2>`,
|
||||
`</ng-template>`,
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe(
|
||||
[`@if (data.teamMember) {`, ` <h2>Then content</h2>`, `}\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',
|
||||
[
|
||||
`<ng-container *ngIf="data.teamMember; then thenyBlock; else elseyBlock"></ng-container>`,
|
||||
`<ng-template #thenyBlock>`,
|
||||
` <h2>Then content</h2>`,
|
||||
`</ng-template>`,
|
||||
`<ng-template #elseyBlock>`,
|
||||
` <h2>Else content</h2>`,
|
||||
`</ng-template>`,
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/comp.html');
|
||||
|
||||
expect(content).toBe(
|
||||
[
|
||||
`@if (data.teamMember) {`,
|
||||
` <h2>Then content</h2>`,
|
||||
`} @else {`,
|
||||
` <h2>Else content</h2>`,
|
||||
`}\n`,
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should migrate an if case on a container', async () => {
|
||||
writeFile(
|
||||
'/comp.ts',
|
||||
|
||||
Reference in New Issue
Block a user