Skip to content

Commit 4c878f9

Browse files
thePunderWomandylhunn
authored andcommitted
fix(migrations): Add support for nested structures inside a switch statement (#52358)
This updates the code to handle switches more elegantly in line with how the other blocks are handled. This allows nesting to be handled just like other blocks. PR Close #52358
1 parent 938007d commit 4c878f9

File tree

3 files changed

+204
-150
lines changed

3 files changed

+204
-150
lines changed

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

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,23 @@ export const boundngif = '[ngIf]';
1313
export const nakedngif = 'ngIf';
1414
export const ngfor = '*ngFor';
1515
export const ngswitch = '[ngSwitch]';
16+
export const boundcase = '[ngSwitchCase]';
17+
export const switchcase = '*ngSwitchCase';
18+
export const nakedcase = 'ngSwitchCase';
19+
export const switchdefault = '*ngSwitchDefault';
20+
export const nakeddefault = 'ngSwitchDefault';
1621

1722
const attributesToMigrate = [
1823
ngif,
1924
nakedngif,
2025
boundngif,
2126
ngfor,
2227
ngswitch,
23-
];
24-
25-
const casesToMigrate = [
26-
'[ngSwitchCase]',
27-
'*ngSwitchCase',
28-
'ngSwitchCase',
29-
'*ngSwitchDefault',
30-
'ngSwitchDefault',
28+
boundcase,
29+
switchcase,
30+
nakedcase,
31+
switchdefault,
32+
nakeddefault,
3133
];
3234

3335
/**
@@ -61,7 +63,7 @@ export class ElementToMigrate {
6163
el: Element;
6264
attr: Attribute;
6365
nestCount = 0;
64-
lineBreaks = false;
66+
hasLineBreaks = false;
6567

6668
constructor(el: Element, attr: Attribute) {
6769
this.el = el;
@@ -173,19 +175,3 @@ export class ElementCollector extends RecursiveVisitor {
173175
super.visitElement(el, null);
174176
}
175177
}
176-
177-
export class CaseCollector extends RecursiveVisitor {
178-
readonly elements: ElementToMigrate[] = [];
179-
180-
override visitElement(el: Element): void {
181-
if (el.attrs.length > 0) {
182-
for (const attr of el.attrs) {
183-
if (casesToMigrate.includes(attr.name)) {
184-
this.elements.push(new ElementToMigrate(el, attr));
185-
}
186-
}
187-
}
188-
189-
super.visitElement(el, null);
190-
}
191-
}

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

Lines changed: 96 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {HtmlParser, Node, ParseTreeResult, visitAll} from '@angular/compiler';
1010
import {dirname, join} from 'path';
1111
import ts from 'typescript';
1212

13-
import {AnalyzedFile, boundngif, CaseCollector, ElementCollector, ElementToMigrate, MigrateError, nakedngif, ngfor, ngif, ngswitch, Result, Template} from './types';
13+
import {AnalyzedFile, boundcase, boundngif, ElementCollector, ElementToMigrate, MigrateError, nakedcase, nakeddefault, nakedngif, ngfor, ngif, ngswitch, Result, switchcase, switchdefault, Template} from './types';
1414

1515
/**
1616
* Analyzes a source file to find file that need to be migrated and the text ranges within them.
@@ -134,10 +134,12 @@ export function migrateTemplate(template: string): {migrated: string|null, error
134134

135135
// start from top of template
136136
// loop through each element
137+
visitor.elements[0].hasLineBreaks = hasLineBreaks;
137138
let prevElEnd = visitor.elements[0]?.el.sourceSpan.end.offset ?? result.length - 1;
138139
let nestedQueue: number[] = [prevElEnd];
139140
for (let i = 1; i < visitor.elements.length; i++) {
140141
let currEl = visitor.elements[i];
142+
currEl.hasLineBreaks = hasLineBreaks;
141143
currEl.nestCount = getNestedCount(currEl, nestedQueue);
142144
if (currEl.el.sourceSpan.end.offset !== nestedQueue[nestedQueue.length - 1]) {
143145
nestedQueue.push(currEl.el.sourceSpan.end.offset);
@@ -165,19 +167,32 @@ export function migrateTemplate(template: string): {migrated: string|null, error
165167
// these are all migratable nodes
166168
if (el.attr.name === ngif || el.attr.name === nakedngif || el.attr.name === boundngif) {
167169
try {
168-
migrateResult = migrateNgIf(el, visitor.templates, result, offset, hasLineBreaks);
170+
migrateResult = migrateNgIf(el, visitor.templates, result, offset);
169171
} catch (error: unknown) {
170172
errors.push({type: ngif, error});
171173
}
172174
} else if (el.attr.name === ngfor) {
173175
try {
174-
migrateResult = migrateNgFor(el, result, offset, hasLineBreaks);
176+
migrateResult = migrateNgFor(el, result, offset);
175177
} catch (error: unknown) {
176178
errors.push({type: ngfor, error});
177179
}
178180
} else if (el.attr.name === ngswitch) {
179181
try {
180-
migrateResult = migrateNgSwitch(el, result, offset, hasLineBreaks);
182+
migrateResult = migrateNgSwitch(el, result, offset);
183+
} catch (error: unknown) {
184+
errors.push({type: ngswitch, error});
185+
}
186+
} else if (
187+
el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
188+
try {
189+
migrateResult = migrateNgSwitchCase(el, result, offset);
190+
} catch (error: unknown) {
191+
errors.push({type: ngswitch, error});
192+
}
193+
} else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
194+
try {
195+
migrateResult = migrateNgSwitchDefault(el, result, offset);
181196
} catch (error: unknown) {
182197
errors.push({type: ngswitch, error});
183198
}
@@ -198,26 +213,24 @@ export function migrateTemplate(template: string): {migrated: string|null, error
198213
}
199214

200215
function migrateNgIf(
201-
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, offset: number,
202-
hasLineBreaks: boolean): Result {
216+
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string,
217+
offset: number): Result {
203218
const matchThen = etm.attr.value.match(/;\s+then/gm);
204219
const matchElse = etm.attr.value.match(/;\s+else/gm);
205220

206221
if (matchThen && matchThen.length > 0) {
207-
return buildIfThenElseBlock(
208-
etm, ngTemplates, tmpl, matchThen[0], matchElse![0], offset, hasLineBreaks);
222+
return buildIfThenElseBlock(etm, ngTemplates, tmpl, matchThen[0], matchElse![0], offset);
209223
} else if (matchElse && matchElse.length > 0) {
210224
// just else
211-
return buildIfElseBlock(etm, ngTemplates, tmpl, matchElse[0], offset, hasLineBreaks);
225+
return buildIfElseBlock(etm, ngTemplates, tmpl, matchElse[0], offset);
212226
}
213227

214-
return buildIfBlock(etm, tmpl, offset, hasLineBreaks);
228+
return buildIfBlock(etm, tmpl, offset);
215229
}
216230

217-
function buildIfBlock(
218-
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
231+
function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Result {
219232
// includes the mandatory semicolon before as
220-
const lbString = hasLineBreaks ? lb : '';
233+
const lbString = etm.hasLineBreaks ? lb : '';
221234
const condition = etm.attr.value.replace(' as ', '; as ');
222235

223236
const originals = getOriginals(etm, tmpl, offset);
@@ -239,9 +252,9 @@ function buildIfBlock(
239252

240253
function buildIfElseBlock(
241254
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, elseString: string,
242-
offset: number, hasLineBreaks: boolean): Result {
255+
offset: number): Result {
243256
// includes the mandatory semicolon before as
244-
const lbString = hasLineBreaks ? lb : '';
257+
const lbString = etm.hasLineBreaks ? lb : '';
245258
const condition = etm.getCondition(elseString).replace(' as ', '; as ');
246259

247260
const originals = getOriginals(etm, tmpl, offset);
@@ -270,9 +283,9 @@ function buildIfElseBlock(
270283

271284
function buildIfThenElseBlock(
272285
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, thenString: string,
273-
elseString: string, offset: number, hasLineBreaks: boolean): Result {
286+
elseString: string, offset: number): Result {
274287
const condition = etm.getCondition(thenString).replace(' as ', '; as ');
275-
const lbString = hasLineBreaks ? lb : '';
288+
const lbString = etm.hasLineBreaks ? lb : '';
276289

277290
const originals = getOriginals(etm, tmpl, offset);
278291

@@ -302,13 +315,12 @@ function buildIfThenElseBlock(
302315
return {tmpl: updatedTmpl, offsets: {pre, post}};
303316
}
304317

305-
function migrateNgFor(
306-
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
318+
function migrateNgFor(etm: ElementToMigrate, tmpl: string, offset: number): Result {
307319
const aliasWithEqualRegexp = /=\s+(count|index|first|last|even|odd)/gm;
308320
const aliasWithAsRegexp = /(count|index|first|last|even|odd)\s+as/gm;
309321
const aliases = [];
310-
const lbString = hasLineBreaks ? lb : '';
311-
const lbSpaces = hasLineBreaks ? `${lb} ` : '';
322+
const lbString = etm.hasLineBreaks ? lb : '';
323+
const lbSpaces = etm.hasLineBreaks ? `${lb} ` : '';
312324
const parts = etm.attr.value.split(';');
313325

314326
const originals = getOriginals(etm, tmpl, offset);
@@ -380,7 +392,8 @@ function getOriginals(
380392

381393
function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
382394
{start: string, middle: string, end: string} {
383-
if (etm.el.name === 'ng-container' && etm.el.attrs.length === 1) {
395+
if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
396+
etm.el.attrs.length === 1) {
384397
// this is the case where we're migrating and there's no need to keep the ng-container
385398
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
386399
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
@@ -389,7 +402,10 @@ function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
389402
}
390403

391404
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset;
392-
const valEnd = etm.attr.valueSpan!.end.offset + 1 - offset;
405+
const valEnd =
406+
(etm.attr.valueSpan ? (etm.attr.valueSpan.end.offset + 1) : etm.attr.keySpan!.end.offset) -
407+
offset;
408+
393409
let childStart = valEnd;
394410
let childEnd = valEnd;
395411

@@ -400,83 +416,80 @@ function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
400416

401417
let start = tmpl.slice(etm.start(offset), attrStart);
402418
start += tmpl.slice(valEnd, childStart);
419+
403420
const middle = tmpl.slice(childStart, childEnd);
404421
const end = tmpl.slice(childEnd, etm.end(offset));
405422

406423
return {start, middle, end};
407424
}
408425

409-
function migrateNgSwitch(
410-
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
426+
function migrateNgSwitch(etm: ElementToMigrate, tmpl: string, offset: number): Result {
427+
const lbString = etm.hasLineBreaks ? lb : '';
411428
const condition = etm.attr.value;
412-
const startBlock = `@switch (${condition}) {`;
413-
const lbString = hasLineBreaks ? lb : '';
414429

415-
const {openTag, closeTag, children} = getSwitchBlockElements(etm, tmpl, offset);
416-
const cases = getSwitchCases(children, tmpl, offset, hasLineBreaks);
417-
const switchBlock = openTag + startBlock + cases.join('') + `${lbString}}` + closeTag;
430+
const originals = getOriginals(etm, tmpl, offset);
431+
432+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
433+
const startBlock = `${start}${lbString}@switch (${condition}) {`;
434+
const endBlock = `}${lbString}${end}`;
435+
436+
const switchBlock = startBlock + middle + endBlock;
418437
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + switchBlock + tmpl.slice(etm.end(offset));
419-
const pre = etm.length() - switchBlock.length;
420438

421-
return {tmpl: updatedTmpl, offsets: {pre, post: 0}};
422-
}
439+
// this should be the difference between the starting element up to the start of the closing
440+
// element and the mainblock sans }
441+
const pre = originals.start.length - startBlock.length;
442+
const post = originals.end.length - endBlock.length;
423443

424-
function getSwitchBlockElements(etm: ElementToMigrate, tmpl: string, offset: number) {
425-
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset;
426-
const valEnd = etm.attr.valueSpan!.end.offset + 1 - offset;
427-
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
428-
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
429-
let openTag = (etm.el.name === 'ng-container') ?
430-
'' :
431-
tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
432-
if (tmpl.slice(childStart, childStart + 1) === lb) {
433-
openTag += lb;
434-
}
435-
let closeTag = (etm.el.name === 'ng-container') ? '' : tmpl.slice(childEnd, etm.end(offset));
436-
if (tmpl.slice(childEnd - 1, childEnd) === lb) {
437-
closeTag = lb + closeTag;
438-
}
439-
return {
440-
openTag,
441-
closeTag,
442-
children: etm.el.children,
443-
};
444+
return {tmpl: updatedTmpl, offsets: {pre, post}};
444445
}
445446

446-
function getSwitchCases(children: Node[], tmpl: string, offset: number, hasLineBreaks: boolean) {
447-
const collector = new CaseCollector();
448-
visitAll(collector, children);
449-
return collector.elements.map(etm => getSwitchCaseBlock(etm, tmpl, offset, hasLineBreaks));
447+
function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number): Result {
448+
// includes the mandatory semicolon before as
449+
const lbString = etm.hasLineBreaks ? lb : '';
450+
const lbSpaces = etm.hasLineBreaks ? ' ' : '';
451+
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
452+
const condition = etm.attr.value;
453+
454+
const originals = getOriginals(etm, tmpl, offset);
455+
456+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
457+
const startBlock =
458+
`${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${lbSpaces}${start}`;
459+
const endBlock = `${end}${lbString}${leadingSpace}}`;
460+
461+
const defaultBlock = startBlock + middle + endBlock;
462+
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
463+
464+
// this should be the difference between the starting element up to the start of the closing
465+
// element and the mainblock sans }
466+
const pre = originals.start.length - startBlock.length;
467+
const post = originals.end.length - endBlock.length;
468+
469+
return {tmpl: updatedTmpl, offsets: {pre, post}};
450470
}
451471

452-
function getSwitchCaseBlock(
453-
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): string {
454-
let elStart = etm.el.sourceSpan?.start.offset - offset;
455-
let elEnd = etm.el.sourceSpan?.end.offset - offset;
456-
const lbString = hasLineBreaks ? '\n ' : ' ';
457-
const lbSpaces = hasLineBreaks ? ' ' : '';
458-
let shift = 0;
472+
function migrateNgSwitchDefault(etm: ElementToMigrate, tmpl: string, offset: number): Result {
473+
// includes the mandatory semicolon before as
474+
const lbString = etm.hasLineBreaks ? lb : '';
475+
const lbSpaces = etm.hasLineBreaks ? ' ' : '';
476+
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
459477

460-
if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
461-
etm.el.attrs.length === 1) {
462-
// no need to keep the containers
463-
elStart = etm.el.children[0].sourceSpan.start.offset - offset;
464-
elEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
465-
// account for the `>` that isn't needed
466-
shift += 1;
467-
}
478+
const originals = getOriginals(etm, tmpl, offset);
468479

469-
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset + shift;
470-
// ngSwitchDefault case has no valueSpan and relies on the end of the key
471-
if (etm.attr.name === '*ngSwitchDefault' || etm.attr.name === 'ngSwitchDefault') {
472-
const attrEnd = etm.attr.keySpan!.end.offset - offset + shift;
473-
return `${lbString}@default {${lbString}${lbSpaces}${
474-
tmpl.slice(elStart, attrStart) + tmpl.slice(attrEnd, elEnd)}${lbString}}`;
475-
}
476-
// ngSwitchCase has a valueSpan
477-
let valEnd = etm.attr.valueSpan!.end.offset + 1 - offset + shift;
478-
return `${lbString}@case (${etm.attr.value}) {${lbString}${lbSpaces}${
479-
tmpl.slice(elStart, attrStart) + tmpl.slice(valEnd, elEnd)}${lbString}}`;
480+
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
481+
const startBlock = `${leadingSpace}@default {${leadingSpace}${lbString}${lbSpaces}${start}`;
482+
const endBlock = `${end}${lbString}${leadingSpace}}`;
483+
484+
const defaultBlock = startBlock + middle + endBlock;
485+
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));
486+
487+
// this should be the difference between the starting element up to the start of the closing
488+
// element and the mainblock sans }
489+
const pre = originals.start.length - startBlock.length;
490+
const post = originals.end.length - endBlock.length;
491+
492+
return {tmpl: updatedTmpl, offsets: {pre, post}};
480493
}
481494

482495
/** Executes a callback on each class declaration in a file. */

0 commit comments

Comments
 (0)