Skip to content

Commit 9692aeb

Browse files
thePunderWomandylhunn
authored andcommitted
fix(migrations): Add support for nested structures inside a switch statement (angular#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 angular#52358
1 parent 56598a1 commit 9692aeb

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)