Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(migrations): fixes migrations of nested switches in control flow #53010

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/core/schematics/ng-generate/control-flow-migration/cases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {visitAll} from '@angular/compiler';

import {ElementCollector, ElementToMigrate, MigrateError, Result} from './types';
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';

export const boundcase = '[ngSwitchCase]';
export const switchcase = '*ngSwitchCase';
export const nakedcase = 'ngSwitchCase';
export const switchdefault = '*ngSwitchDefault';
export const nakeddefault = 'ngSwitchDefault';

const cases = [
boundcase,
switchcase,
nakedcase,
switchdefault,
nakeddefault,
];

/**
* Replaces structural directive ngSwitch instances with new switch.
* Returns null if the migration failed (e.g. there was a syntax error).
*/
export function migrateCase(template: string): {migrated: string, errors: MigrateError[]} {
let errors: MigrateError[] = [];
let parsed = parseTemplate(template);
if (parsed === null) {
return {migrated: template, errors};
}

let result = template;
const visitor = new ElementCollector(cases);
visitAll(visitor, parsed.rootNodes);
calculateNesting(visitor, hasLineBreaks(template));

// this tracks the character shift from different lengths of blocks from
// the prior directives so as to adjust for nested block replacement during
// migration. Each block calculates length differences and passes that offset
// to the next migrating block to adjust character offsets properly.
let offset = 0;
let nestLevel = -1;
let postOffsets: number[] = [];
for (const el of visitor.elements) {
let migrateResult: Result = {tmpl: result, offsets: {pre: 0, post: 0}};
// applies the post offsets after closing
offset = reduceNestingOffset(el, nestLevel, offset, postOffsets);

if (el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
try {
migrateResult = migrateNgSwitchCase(el, result, offset);
} catch (error: unknown) {
errors.push({type: switchcase, error});
}
} else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
try {
migrateResult = migrateNgSwitchDefault(el, result, offset);
} catch (error: unknown) {
errors.push({type: switchdefault, error});
}
}

result = migrateResult.tmpl;
offset += migrateResult.offsets.pre;
postOffsets.push(migrateResult.offsets.post);
nestLevel = el.nestCount;
}

return {migrated: result, errors};
}

function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number): Result {
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
const condition = etm.attr.value;

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

const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
const endBlock = `${end}${lbString}${leadingSpace}}`;

const defaultBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));

// this should be the difference between the starting element up to the start of the closing
// element and the mainblock sans }
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;

return {tmpl: updatedTmpl, offsets: {pre, post}};
}

function migrateNgSwitchDefault(etm: ElementToMigrate, tmpl: string, offset: number): Result {
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
const leadingSpace = etm.hasLineBreaks ? '' : ' ';

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

const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
const endBlock = `${end}${lbString}${leadingSpace}}`;

const defaultBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));

// this should be the difference between the starting element up to the start of the closing
// element and the mainblock sans }
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;

return {tmpl: updatedTmpl, offsets: {pre, post}};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import ts from 'typescript';

import {migrateCase} from './cases';
import {migrateFor} from './fors';
import {migrateIf} from './ifs';
import {migrateSwitch} from './switches';
Expand All @@ -26,7 +27,8 @@ export function migrateTemplate(
const ifResult = migrateIf(template);
const forResult = migrateFor(ifResult.migrated);
const switchResult = migrateSwitch(forResult.migrated);
migrated = processNgTemplates(switchResult.migrated);
const caseResult = migrateCase(switchResult.migrated);
migrated = processNgTemplates(caseResult.migrated);
if (format) {
migrated = formatTemplate(migrated);
}
Expand All @@ -36,6 +38,7 @@ export function migrateTemplate(
...ifResult.errors,
...forResult.errors,
...switchResult.errors,
...caseResult.errors,
];
} else {
migrated = removeImports(template, node, file.removeCommonModule);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,9 @@ import {ElementCollector, ElementToMigrate, MigrateError, Result} from './types'
import {calculateNesting, getMainBlock, getOriginals, hasLineBreaks, parseTemplate, reduceNestingOffset} from './util';

export const ngswitch = '[ngSwitch]';
export const boundcase = '[ngSwitchCase]';
export const switchcase = '*ngSwitchCase';
export const nakedcase = 'ngSwitchCase';
export const switchdefault = '*ngSwitchDefault';
export const nakeddefault = 'ngSwitchDefault';

const switches = [
ngswitch,
boundcase,
switchcase,
nakedcase,
switchdefault,
nakeddefault,
];

/**
Expand Down Expand Up @@ -61,19 +51,6 @@ export function migrateSwitch(template: string): {migrated: string, errors: Migr
} catch (error: unknown) {
errors.push({type: ngswitch, error});
}
} else if (
el.attr.name === switchcase || el.attr.name === nakedcase || el.attr.name === boundcase) {
try {
migrateResult = migrateNgSwitchCase(el, result, offset);
} catch (error: unknown) {
errors.push({type: ngswitch, error});
}
} else if (el.attr.name === switchdefault || el.attr.name === nakeddefault) {
try {
migrateResult = migrateNgSwitchDefault(el, result, offset);
} catch (error: unknown) {
errors.push({type: ngswitch, error});
}
}

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

return {tmpl: updatedTmpl, offsets: {pre, post}};
}

function migrateNgSwitchCase(etm: ElementToMigrate, tmpl: string, offset: number): Result {
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
const leadingSpace = etm.hasLineBreaks ? '' : ' ';
const condition = etm.attr.value;

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

const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `${leadingSpace}@case (${condition}) {${leadingSpace}${lbString}${start}`;
const endBlock = `${end}${lbString}${leadingSpace}}`;

const defaultBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));

// this should be the difference between the starting element up to the start of the closing
// element and the mainblock sans }
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;

return {tmpl: updatedTmpl, offsets: {pre, post}};
}

function migrateNgSwitchDefault(etm: ElementToMigrate, tmpl: string, offset: number): Result {
// includes the mandatory semicolon before as
const lbString = etm.hasLineBreaks ? '\n' : '';
const leadingSpace = etm.hasLineBreaks ? '' : ' ';

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

const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `${leadingSpace}@default {${leadingSpace}${lbString}${start}`;
const endBlock = `${end}${lbString}${leadingSpace}}`;

const defaultBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + defaultBlock + tmpl.slice(etm.end(offset));

// this should be the difference between the starting element up to the start of the closing
// element and the mainblock sans }
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;

return {tmpl: updatedTmpl, offsets: {pre, post}};
}
52 changes: 52 additions & 0 deletions packages/core/schematics/test/control_flow_migration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2072,6 +2072,58 @@ describe('control flow migration', () => {
expect(content).toContain(
'template: `<div>@switch (testOpts) { @case (1) { <p>Option 1</p> } @case (2) { <p>Option 2</p> }}</div>`');
});

it('should migrate nested switches', async () => {
writeFile('/comp.ts', `
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';

@Component({
imports: [NgFor, NgIf],
templateUrl: './comp.html'
})
class Comp {
show = false;
nest = true;
again = true;
more = true;
}`);

writeFile('/comp.html', [
`<div [ngSwitch]="thing">`,
` <div *ngSwitchCase="'item'" [ngSwitch]="anotherThing">`,
` <img *ngSwitchCase="'png'" src="/img.png" alt="PNG" />`,
` <img *ngSwitchDefault src="/default.jpg" alt="default" />`,
` </div>`,
` <img *ngSwitchDefault src="/default.jpg" alt="default" />`,
`</div>`,
].join('\n'));

await runMigration();
const content = tree.readContent('/comp.html');

expect(content).toBe([
`<div>`,
` @switch (thing) {`,
` @case ('item') {`,
` <div>`,
` @switch (anotherThing) {`,
` @case ('png') {`,
` <img src="/img.png" alt="PNG" />`,
` }`,
` @default {`,
` <img src="/default.jpg" alt="default" />`,
` }`,
` }`,
` </div>`,
` }`,
` @default {`,
` <img src="/default.jpg" alt="default" />`,
` }`,
` }`,
`</div>`,
].join('\n'));
});
});

describe('nested structures', () => {
Expand Down