Skip to content

Commit 492ad46

Browse files
thePunderWomanAndrewKushnir
authored andcommitted
fix(migrations): fixes migrations of nested switches in control flow (#53010)
This separates out the NgSwitch migration pass from the NgSwitchCase / Default pass, which makes nested switch migrations work. fixes: #53009 PR Close #53010
1 parent 13bf5b7 commit 492ad46

File tree

4 files changed

+178
-69
lines changed

4 files changed

+178
-69
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {visitAll} from '@angular/compiler';
10+
11+
import {ElementCollector, ElementToMigrate, MigrateError, Result} from './types';
12+
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
13+
14+
export const boundcase = '[ngSwitchCase]';
15+
export const switchcase = '*ngSwitchCase';
16+
export const nakedcase = 'ngSwitchCase';
17+
export const switchdefault = '*ngSwitchDefault';
18+
export const nakeddefault = 'ngSwitchDefault';
19+
20+
const cases = [
21+
boundcase,
22+
switchcase,
23+
nakedcase,
24+
switchdefault,
25+
nakeddefault,
26+
];
27+
28+
/**
29+
* Replaces structural directive ngSwitch instances with new switch.
30+
* Returns null if the migration failed (e.g. there was a syntax error).
31+
*/
32+
export function migrateCase(template: string): {migrated: string, errors: MigrateError[]} {
33+
let errors: MigrateError[] = [];
34+
let parsed = parseTemplate(template);
35+
if (parsed === null) {
36+
return {migrated: template, errors};
37+
}
38+
39+
let result = template;
40+
const visitor = new ElementCollector(cases);
41+
visitAll(visitor, parsed.rootNodes);
42+
calculateNesting(visitor, hasLineBreaks(template));
43+
44+
// this tracks the character shift from different lengths of blocks from
45+
// the prior directives so as to adjust for nested block replacement during
46+
// migration. Each block calculates length differences and passes that offset
47+
// to the next migrating block to adjust character offsets properly.
48+
let offset = 0;
49+
let nestLevel = -1;
50+
let postOffsets: number[] = [];
51+
for (const el of visitor.elements) {
52+
let migrateResult: Result = {tmpl: result, offsets: {pre: 0, post: 0}};
53+
// applies the post offsets after closing
54+
offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);
55+
56+
if (el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
57+
try {
58+
migrateResult = migrateNgSwitchCase(el, result, offset);
59+
} catch (error: unknown) {
60+
errors.push({type: switchcase, error});
61+
}
62+
} else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
63+
try {
64+
migrateResult = migrateNgSwitchDefault(el, result, offset);
65+
} catch (error: unknown) {
66+
errors.push({type: switchdefault, error});
67+
}
68+
}
69+
70+
result = migrateResult.tmpl;
71+
offset += migrateResult.offsets.pre;
72+
postOffsets.push(migrateResult.offsets.post);
73+
nestLevel = el.nestCount;
74+
}
75+
76+
return {migrated: result, errors};
77+
}
78+
79+
function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number): Result {
80+
// includes the mandatory semicolon before as
81+
const lbString = etm.hasLineBreaks ? '\n' : '';
82+
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
83+
const condition = etm.attr.value;
84+
85+
const originals = getOriginals(etm, tmpl, offset);
86+
87+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
88+
const startBlock = `${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
89+
const endBlock = `${end}${lbString}${leadingSpace}}`;
90+
91+
const defaultBlock = startBlock + middle + endBlock;
92+
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
93+
94+
// this should be the difference between the starting element up to the start of the closing
95+
// element and the mainblock sans }
96+
const pre = originals.start.length - startBlock.length;
97+
const post = originals.end.length - endBlock.length;
98+
99+
return {tmpl: updatedTmpl, offsets: {pre, post}};
100+
}
101+
102+
function migrateNgSwitchDefault(etm: ElementToMigrate, tmpl: string, offset: number): Result {
103+
// includes the mandatory semicolon before as
104+
const lbString = etm.hasLineBreaks ? '\n' : '';
105+
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
106+
107+
const originals = getOriginals(etm, tmpl, offset);
108+
109+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
110+
const startBlock = `${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
111+
const endBlock = `${end}${lbString}${leadingSpace}}`;
112+
113+
const defaultBlock = startBlock + middle + endBlock;
114+
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
115+
116+
// this should be the difference between the starting element up to the start of the closing
117+
// element and the mainblock sans }
118+
const pre = originals.start.length - startBlock.length;
119+
const post = originals.end.length - endBlock.length;
120+
121+
return {tmpl: updatedTmpl, offsets: {pre, post}};
122+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import ts from 'typescript';
1010

11+
import {migrateCase} from './cases';
1112
import {migrateFor} from './fors';
1213
import {migrateIf} from './ifs';
1314
import {migrateSwitch} from './switches';
@@ -26,7 +27,8 @@ export function migrateTemplate(
2627
const ifResult = migrateIf(template);
2728
const forResult = migrateFor(ifResult.migrated);
2829
const switchResult = migrateSwitch(forResult.migrated);
29-
migrated = processNgTemplates(switchResult.migrated);
30+
const caseResult = migrateCase(switchResult.migrated);
31+
migrated = processNgTemplates(caseResult.migrated);
3032
if (format) {
3133
migrated = formatTemplate(migrated);
3234
}
@@ -36,6 +38,7 @@ export function migrateTemplate(
3638
...ifResult.errors,
3739
...forResult.errors,
3840
...switchResult.errors,
41+
...caseResult.errors,
3942
];
4043
} else {
4144
migrated = removeImports(template, node, file.removeCommonModule);

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

Lines changed: 0 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,9 @@ import {ElementCollector, ElementToMigrate, MigrateError, Result} from './types'
1212
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';
1313

1414
export const ngswitch = '[ngSwitch]';
15-
export const boundcase = '[ngSwitchCase]';
16-
export const switchcase = '*ngSwitchCase';
17-
export const nakedcase = 'ngSwitchCase';
18-
export const switchdefault = '*ngSwitchDefault';
19-
export const nakeddefault = 'ngSwitchDefault';
2015

2116
const switches = [
2217
ngswitch,
23-
boundcase,
24-
switchcase,
25-
nakedcase,
26-
switchdefault,
27-
nakeddefault,
2818
];
2919

3020
/**
@@ -61,19 +51,6 @@ export function migrateSwitch(template: string): {migrated: string, errors: Migr
6151
} catch (error: unknown) {
6252
errors.push({type: ngswitch, error});
6353
}
64-
} else if (
65-
el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
66-
try {
67-
migrateResult = migrateNgSwitchCase(el, result, offset);
68-
} catch (error: unknown) {
69-
errors.push({type: ngswitch, error});
70-
}
71-
} else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
72-
try {
73-
migrateResult = migrateNgSwitchDefault(el, result, offset);
74-
} catch (error: unknown) {
75-
errors.push({type: ngswitch, error});
76-
}
7754
}
7855

7956
result = migrateResult.tmpl;
@@ -105,48 +82,3 @@ function migrateNgSwitch(etm: ElementToMigrate, tmpl: string, offset: number): R
10582

10683
return {tmpl: updatedTmpl, offsets: {pre, post}};
10784
}
108-
109-
function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number): Result {
110-
// includes the mandatory semicolon before as
111-
const lbString = etm.hasLineBreaks ? '\n' : '';
112-
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
113-
const condition = etm.attr.value;
114-
115-
const originals = getOriginals(etm, tmpl, offset);
116-
117-
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
118-
const startBlock = `${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
119-
const endBlock = `${end}${lbString}${leadingSpace}}`;
120-
121-
const defaultBlock = startBlock + middle + endBlock;
122-
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
123-
124-
// this should be the difference between the starting element up to the start of the closing
125-
// element and the mainblock sans }
126-
const pre = originals.start.length - startBlock.length;
127-
const post = originals.end.length - endBlock.length;
128-
129-
return {tmpl: updatedTmpl, offsets: {pre, post}};
130-
}
131-
132-
function migrateNgSwitchDefault(etm: ElementToMigrate, tmpl: string, offset: number): Result {
133-
// includes the mandatory semicolon before as
134-
const lbString = etm.hasLineBreaks ? '\n' : '';
135-
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
136-
137-
const originals = getOriginals(etm, tmpl, offset);
138-
139-
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
140-
const startBlock = `${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
141-
const endBlock = `${end}${lbString}${leadingSpace}}`;
142-
143-
const defaultBlock = startBlock + middle + endBlock;
144-
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
145-
146-
// this should be the difference between the starting element up to the start of the closing
147-
// element and the mainblock sans }
148-
const pre = originals.start.length - startBlock.length;
149-
const post = originals.end.length - endBlock.length;
150-
151-
return {tmpl: updatedTmpl, offsets: {pre, post}};
152-
}

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,6 +2125,58 @@ describe('control flow migration', () => {
21252125
expect(content).toContain(
21262126
'template: `<div>@switch (testOpts) { @case (1) { <p>Option 1</p> } @case (2) { <p>Option 2</p> }}</div>`');
21272127
});
2128+
2129+
it('should migrate nested switches', async () => {
2130+
writeFile('/comp.ts', `
2131+
import {Component} from '@angular/core';
2132+
import {NgIf} from '@angular/common';
2133+
2134+
@Component({
2135+
imports: [NgFor, NgIf],
2136+
templateUrl: './comp.html'
2137+
})
2138+
class Comp {
2139+
show = false;
2140+
nest = true;
2141+
again = true;
2142+
more = true;
2143+
}`);
2144+
2145+
writeFile('/comp.html', [
2146+
`<div [ngSwitch]="thing">`,
2147+
` <div *ngSwitchCase="'item'" [ngSwitch]="anotherThing">`,
2148+
` <img *ngSwitchCase="'png'" src="/img.png" alt="PNG" />`,
2149+
` <img *ngSwitchDefault src="/default.jpg" alt="default" />`,
2150+
` </div>`,
2151+
` <img *ngSwitchDefault src="/default.jpg" alt="default" />`,
2152+
`</div>`,
2153+
].join('\n'));
2154+
2155+
await runMigration();
2156+
const content = tree.readContent('/comp.html');
2157+
2158+
expect(content).toBe([
2159+
`<div>`,
2160+
` @switch (thing) {`,
2161+
` @case ('item') {`,
2162+
` <div>`,
2163+
` @switch (anotherThing) {`,
2164+
` @case ('png') {`,
2165+
` <img src="/img.png" alt="PNG" />`,
2166+
` }`,
2167+
` @default {`,
2168+
` <img src="/default.jpg" alt="default" />`,
2169+
` }`,
2170+
` }`,
2171+
` </div>`,
2172+
` }`,
2173+
` @default {`,
2174+
` <img src="/default.jpg" alt="default" />`,
2175+
` }`,
2176+
` }`,
2177+
`</div>`,
2178+
].join('\n'));
2179+
});
21282180
});
21292181

21302182
describe('nested structures', () => {

0 commit comments

Comments
 (0)