Skip to content

Commit eb7c29c

Browse files
thePunderWomanatscott
authored andcommitted
fix(migrations): cf migration - detect and error when result is invalid i18n nesting (#53638)
This will gracefully error on templates when the resulting template would have invalid i18n nested structures. PR Close #53638
1 parent 817dc1b commit eb7c29c

File tree

3 files changed

+90
-11
lines changed

3 files changed

+90
-11
lines changed

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

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {migrateFor} from './fors';
1313
import {migrateIf} from './ifs';
1414
import {migrateSwitch} from './switches';
1515
import {AnalyzedFile, endI18nMarker, endMarker, MigrateError, startI18nMarker, startMarker} from './types';
16-
import {canRemoveCommonModule, formatTemplate, parseTemplate, processNgTemplates, removeImports} from './util';
16+
import {canRemoveCommonModule, formatTemplate, parseTemplate, processNgTemplates, removeImports, validateI18nStructure, validateMigratedTemplate} from './util';
1717

1818
/**
1919
* Actually migrates a given template to the new syntax
@@ -42,15 +42,9 @@ export function migrateTemplate(
4242
if (changed) {
4343
// determine if migrated template is a valid structure
4444
// if it is not, fail out
45-
const parsed = parseTemplate(migrated);
46-
if (parsed.errors.length > 0) {
47-
const parsingError = {
48-
type: 'parse',
49-
error: new Error(
50-
`The migration resulted in invalid HTML for ${file.sourceFile.fileName}. ` +
51-
`Please check the template for valid HTML structures and run the migration again.`)
52-
};
53-
return {migrated: template, errors: [parsingError]};
45+
const errors = validateMigratedTemplate(migrated, file.sourceFile.fileName);
46+
if (errors.length > 0) {
47+
return {migrated: template, errors};
5448
}
5549
}
5650

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

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Attribute, Element, HtmlParser, Node, ParseTreeResult, visitAll} from '@
1010
import {dirname, join} from 'path';
1111
import ts from 'typescript';
1212

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

1515
const startMarkerRegex = new RegExp(startMarker, 'gm');
1616
const endMarkerRegex = new RegExp(endMarker, 'gm');
@@ -233,6 +233,49 @@ export function parseTemplate(template: string): ParseResult {
233233
return {tree: parsed, errors: []};
234234
}
235235

236+
export function validateMigratedTemplate(migrated: string, fileName: string): MigrateError[] {
237+
const parsed = parseTemplate(migrated);
238+
let errors: MigrateError[] = [];
239+
if (parsed.errors.length > 0) {
240+
errors.push({
241+
type: 'parse',
242+
error: new Error(
243+
`The migration resulted in invalid HTML for ${fileName}. ` +
244+
`Please check the template for valid HTML structures and run the migration again.`)
245+
});
246+
}
247+
if (parsed.tree) {
248+
const i18nError = validateI18nStructure(parsed.tree, fileName);
249+
if (i18nError !== null) {
250+
errors.push({type: 'i18n', error: i18nError});
251+
}
252+
}
253+
return errors;
254+
}
255+
256+
export function validateI18nStructure(parsed: ParseTreeResult, fileName: string): Error|null {
257+
const visitor = new i18nCollector();
258+
visitAll(visitor, parsed.rootNodes);
259+
const parents = visitor.elements.filter(el => el.children.length > 0);
260+
for (const p of parents) {
261+
for (const el of visitor.elements) {
262+
if (el === p) continue;
263+
if (isChildOf(p, el)) {
264+
return new Error(
265+
`i18n Nesting error: The migration would result in invalid i18n nesting for ` +
266+
`${fileName}. Element with i18n attribute "${p.name}" would result having a child of ` +
267+
`element with i18n attribute "${el.name}". Please fix and re-run the migration.`);
268+
}
269+
}
270+
}
271+
return null;
272+
}
273+
274+
function isChildOf(parent: Element, el: Element): boolean {
275+
return parent.sourceSpan.start.offset < el.sourceSpan.start.offset &&
276+
parent.sourceSpan.end.offset > el.sourceSpan.end.offset;
277+
}
278+
236279
/**
237280
* calculates the level of nesting of the items in the collector
238281
*/

packages/core/schematics/test/control_flow_migration_spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5511,5 +5511,47 @@ describe('control flow migration', () => {
55115511
`Element node: "strong" would result in invalid migrated @switch block structure. ` +
55125512
`@switch can only have @case or @default as children.`);
55135513
});
5514+
5515+
it('should not migrate a template that would result in invalid i18n nesting', async () => {
5516+
writeFile('/comp.ts', `
5517+
import {Component} from '@angular/core';
5518+
5519+
@Component({
5520+
templateUrl: './comp.html'
5521+
})
5522+
class Comp {
5523+
testOpts = 2;
5524+
}
5525+
`);
5526+
5527+
writeFile('/comp.html', [
5528+
`<ng-container i18n="messageid">`,
5529+
` <div *ngIf="condition; else elseTmpl">`,
5530+
` If content here`,
5531+
` </div>`,
5532+
`</ng-container>`,
5533+
`<ng-template #elseTmpl i18n="elsemessageid">`,
5534+
` <div>Else content here</div>`,
5535+
`</ng-template>`,
5536+
].join('\n'));
5537+
5538+
await runMigration();
5539+
const content = tree.readContent('/comp.html');
5540+
expect(content).toBe([
5541+
`<ng-container i18n="messageid">`,
5542+
` <div *ngIf="condition; else elseTmpl">`,
5543+
` If content here`,
5544+
` </div>`,
5545+
`</ng-container>`,
5546+
`<ng-template #elseTmpl i18n="elsemessageid">`,
5547+
` <div>Else content here</div>`,
5548+
`</ng-template>`,
5549+
].join('\n'));
5550+
5551+
expect(warnOutput.join(' '))
5552+
.toContain(
5553+
`Element with i18n attribute "ng-container" would result having a child of element with i18n attribute ` +
5554+
`"ng-container". Please fix and re-run the migration.`);
5555+
});
55145556
});
55155557
});

0 commit comments

Comments
 (0)