Skip to content

Commit e88a12d

Browse files
thePunderWomanalxhub
authored andcommitted
fix(migrations): cf migration validate structure of ngswitch before migrating (#53530)
This fix handles the common case where an ngswitch might have invalid syntax post migration. This is likely due to using elements other than case or default underneath the ngswitchcase. This will fail out of the migration for that file when these cases are detected with a useful console message. fixes: #53234 PR Close #53530
1 parent c9a1c6f commit e88a12d

File tree

6 files changed

+89
-8
lines changed

6 files changed

+89
-8
lines changed

packages/core/schematics/ng-generate/control-flow-migration/cases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const nakedcase = 'ngSwitchCase';
1717
export const switchdefault = '*ngSwitchDefault';
1818
export const nakeddefault = 'ngSwitchDefault';
1919

20-
const cases = [
20+
export const cases = [
2121
boundcase,
2222
switchcase,
2323
nakedcase,

packages/core/schematics/ng-generate/control-flow-migration/migration.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ export function migrateTemplate(
2828
const ifResult = migrateIf(template);
2929
const forResult = migrateFor(ifResult.migrated);
3030
const switchResult = migrateSwitch(forResult.migrated);
31+
if (switchResult.errors.length > 0) {
32+
return {migrated: template, errors: switchResult.errors};
33+
}
3134
const caseResult = migrateCase(switchResult.migrated);
3235
const templateResult = processNgTemplates(caseResult.migrated);
3336
if (templateResult.err !== undefined) {

packages/core/schematics/ng-generate/control-flow-migration/switches.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {visitAll} from '@angular/compiler';
9+
import {Element, Node, Text, visitAll} from '@angular/compiler';
1010

11+
import {cases} from './cases';
1112
import {ElementCollector, ElementToMigrate, endMarker, MigrateError, Result, startMarker} from './types';
1213
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
1314

@@ -65,11 +66,35 @@ export function migrateSwitch(template: string):
6566
return {migrated: result, errors, changed};
6667
}
6768

69+
function assertValidSwitchStructure(children: Node[]): void {
70+
for (const child of children) {
71+
if (child instanceof Text && child.value.trim() !== '') {
72+
throw new Error(
73+
`Text node: "${child.value}" would result in invalid migrated @switch block structure. ` +
74+
`@switch can only have @case or @default as children.`);
75+
} else if (child instanceof Element) {
76+
let hasCase = false;
77+
for (const attr of child.attrs) {
78+
if (cases.includes(attr.name)) {
79+
hasCase = true;
80+
}
81+
}
82+
if (!hasCase) {
83+
throw new Error(
84+
`Element node: "${
85+
child.name}" would result in invalid migrated @switch block structure. ` +
86+
`@switch can only have @case or @default as children.`);
87+
}
88+
}
89+
}
90+
}
91+
6892
function migrateNgSwitch(etm: ElementToMigrate, tmpl: string, offset: number): Result {
6993
const lbString = etm.hasLineBreaks ? '\n' : '';
7094
const condition = etm.attr.value;
7195

7296
const originals = getOriginals(etm, tmpl, offset);
97+
assertValidSwitchStructure(originals.childNodes);
7398

7499
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
75100
const startBlock = `${startMarker}${start}${lbString}@switch (${condition}) {`;

packages/core/schematics/ng-generate/control-flow-migration/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Attribute, Element, ParseError, ParseTreeResult, RecursiveVisitor, Text} from '@angular/compiler';
9+
import {Attribute, Element, ParseTreeResult, RecursiveVisitor, Text} from '@angular/compiler';
1010
import ts from 'typescript';
1111

1212
export const ngtemplate = 'ng-template';

packages/core/schematics/ng-generate/control-flow-migration/util.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Attribute, HtmlParser, ParseTreeResult, visitAll} from '@angular/compiler';
9+
import {Attribute, HtmlParser, Node, ParseTreeResult, visitAll} from '@angular/compiler';
1010
import {dirname, join} from 'path';
1111
import ts from 'typescript';
1212

13-
import {AnalyzedFile, CommonCollector, ElementCollector, ElementToMigrate, endMarker, startMarker, MigrateError, ParseResult, Template, TemplateCollector} from './types';
13+
import {AnalyzedFile, CommonCollector, ElementCollector, ElementToMigrate, endMarker, ParseResult, startMarker, Template, TemplateCollector} from './types';
1414

1515
const importRemovals = [
1616
'NgIf', 'NgIfElse', 'NgIfThenElse', 'NgFor', 'NgForOf', 'NgForTrackBy', 'NgSwitch',
@@ -415,7 +415,7 @@ export function removeImports(
415415
* processing
416416
*/
417417
export function getOriginals(etm: ElementToMigrate, tmpl: string, offset: number):
418-
{start: string, end: string, childLength: number} {
418+
{start: string, end: string, childLength: number, children: string[], childNodes: Node[]} {
419419
// original opening block
420420
if (etm.el.children.length > 0) {
421421
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
@@ -428,13 +428,25 @@ export function getOriginals(etm: ElementToMigrate, tmpl: string, offset: number
428428
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset,
429429
etm.el.sourceSpan.end.offset - offset);
430430
const childLength = childEnd - childStart;
431-
return {start, end, childLength};
431+
return {
432+
start,
433+
end,
434+
childLength,
435+
children: getOriginalChildren(etm.el.children, tmpl, offset),
436+
childNodes: etm.el.children
437+
};
432438
}
433439
// self closing or no children
434440
const start =
435441
tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
436442
// original closing block
437-
return {start, end: '', childLength: 0};
443+
return {start, end: '', childLength: 0, children: [], childNodes: []};
444+
}
445+
446+
function getOriginalChildren(children: Node[], tmpl: string, offset: number) {
447+
return children.map(child => {
448+
return tmpl.slice(child.sourceSpan.start.offset - offset, child.sourceSpan.end.offset - offset);
449+
});
438450
}
439451

440452
function isI18nTemplate(etm: ElementToMigrate, i18nAttr: Attribute|undefined): boolean {

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5082,5 +5082,46 @@ describe('control flow migration', () => {
50825082
expect(warnings).toContain(
50835083
'Please check the template for valid HTML structures and run the migration again.');
50845084
});
5085+
5086+
it('should not migrate a template that would result in invalid switch block contents',
5087+
async () => {
5088+
writeFile('/comp.ts', `
5089+
import {Component} from '@angular/core';
5090+
5091+
@Component({
5092+
templateUrl: './comp.html'
5093+
})
5094+
class Comp {
5095+
testOpts = 2;
5096+
}
5097+
`);
5098+
5099+
writeFile('/comp.html', [
5100+
`<div [ngSwitch]="testOpts">`,
5101+
`<strong>`,
5102+
`<p *ngSwitchCase="1">Option 1</p>`,
5103+
`<p *ngSwitchCase="2">Option 2</p>`,
5104+
`<p *ngSwitchDefault>Option 3</p>`,
5105+
`</strong>`,
5106+
`</div>`,
5107+
].join('\n'));
5108+
5109+
await runMigration();
5110+
const content = tree.readContent('/comp.html');
5111+
expect(content).toBe([
5112+
`<div [ngSwitch]="testOpts">`,
5113+
`<strong>`,
5114+
`<p *ngSwitchCase="1">Option 1</p>`,
5115+
`<p *ngSwitchCase="2">Option 2</p>`,
5116+
`<p *ngSwitchDefault>Option 3</p>`,
5117+
`</strong>`,
5118+
`</div>`,
5119+
].join('\n'));
5120+
5121+
expect(warnOutput.join(' '))
5122+
.toContain(
5123+
`Element node: "strong" would result in invalid migrated @switch block structure. ` +
5124+
`@switch can only have @case or @default as children.`);
5125+
});
50855126
});
50865127
});

0 commit comments

Comments
 (0)