Skip to content

Commit 00cb333

Browse files
fix(migrations): control flow migration formatting fixes (#53076)
This fix preserves leading indents in inline templates and also adds better handling for self closing tags PR Close #53076
1 parent 92a28a7 commit 00cb333

File tree

5 files changed

+386
-17
lines changed

5 files changed

+386
-17
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ function buildIfBlock(etm: ElementToMigrate, tmpl: string, offset: number): Resu
112112
function buildStandardIfElseBlock(
113113
etm: ElementToMigrate, tmpl: string, elseString: string, offset: number): Result {
114114
// includes the mandatory semicolon before as
115-
const condition = etm.getCondition(elseString)
115+
const condition = etm.getCondition()
116116
.replace(' as ', '; as ')
117117
// replace 'let' with 'as' whatever spaces are between ; and 'let'
118118
.replace(/;\s*let/g, '; as');
@@ -160,7 +160,7 @@ function buildStandardIfThenElseBlock(
160160
etm: ElementToMigrate, tmpl: string, thenString: string, elseString: string,
161161
offset: number): Result {
162162
// includes the mandatory semicolon before as
163-
const condition = etm.getCondition(thenString)
163+
const condition = etm.getCondition()
164164
.replace(' as ', '; as ')
165165
// replace 'let' with 'as' whatever spaces are between ; and 'let'
166166
.replace(/;\s*let/g, '; as');

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function migrateTemplate(
2323
format: boolean = true): {migrated: string, errors: MigrateError[]} {
2424
let errors: MigrateError[] = [];
2525
let migrated = template;
26-
if (templateType === 'template') {
26+
if (templateType === 'template' || templateType === 'templateUrl') {
2727
const ifResult = migrateIf(template);
2828
const forResult = migrateFor(ifResult.migrated);
2929
const switchResult = migrateSwitch(forResult.migrated);
@@ -32,7 +32,7 @@ export function migrateTemplate(
3232
const changed =
3333
ifResult.changed || forResult.changed || switchResult.changed || caseResult.changed;
3434
if (format && changed) {
35-
migrated = formatTemplate(migrated);
35+
migrated = formatTemplate(migrated, templateType);
3636
}
3737
file.removeCommonModule = canRemoveCommonModule(template);
3838

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,22 @@ export class ElementToMigrate {
114114
this.forAttrs = forAttrs;
115115
}
116116

117-
getCondition(targetStr: string): string {
118-
const targetLocation = this.attr.value.indexOf(targetStr);
119-
return this.attr.value.slice(0, targetLocation);
117+
getCondition(): string {
118+
const chunks = this.attr.value.split(';');
119+
let letVar = chunks.find(c => c.search(/\s*let\s/) > -1);
120+
return chunks[0] + (letVar ? ';' + letVar : '');
120121
}
121122

122123
getTemplateName(targetStr: string, secondStr?: string): string {
123124
const targetLocation = this.attr.value.indexOf(targetStr);
124125
if (secondStr) {
125126
const secondTargetLocation = this.attr.value.indexOf(secondStr);
126-
return this.attr.value.slice(targetLocation + targetStr.length, secondTargetLocation).trim();
127+
return this.attr.value.slice(targetLocation + targetStr.length, secondTargetLocation)
128+
.trim()
129+
.split(';')[0]
130+
.trim();
127131
}
128-
return this.attr.value.slice(targetLocation + targetStr.length).trim();
132+
return this.attr.value.slice(targetLocation + targetStr.length).trim().split(';')[0].trim();
129133
}
130134

131135
start(offset: number): number {
@@ -175,7 +179,7 @@ export class AnalyzedFile {
175179
/** Returns the ranges in the order in which they should be migrated. */
176180
getSortedRanges(): Range[] {
177181
const templateRanges = this.ranges.slice()
178-
.filter(x => x.type === 'template')
182+
.filter(x => x.type !== 'import')
179183
.sort((aStart, bStart) => bStart.start - aStart.start);
180184
const importRanges = this.ranges.slice()
181185
.filter(x => x.type === 'import')

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

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function analyzeDecorators(
160160
// Leave the end as undefined which means that the range is until the end of the file.
161161
if (ts.isStringLiteralLike(prop.initializer)) {
162162
const path = join(dirname(sourceFile.fileName), prop.initializer.text);
163-
AnalyzedFile.addRange(path, analyzedFiles, {start: 0, node: prop, type: 'template'});
163+
AnalyzedFile.addRange(path, analyzedFiles, {start: 0, node: prop, type: 'templateUrl'});
164164
}
165165
break;
166166
}
@@ -429,11 +429,16 @@ export function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number
429429
return {start, middle, end};
430430
}
431431

432+
const selfClosingList = 'input|br|img|base|wbr|area|col|embed|hr|link|meta|param|source|track';
433+
432434
/**
433435
* re-indents all the lines in the template properly post migration
434436
*/
435-
export function formatTemplate(tmpl: string): string {
437+
export function formatTemplate(tmpl: string, templateType: string): string {
436438
if (tmpl.indexOf('\n') > -1) {
439+
// tracks if a self closing element opened without closing yet
440+
let openSelfClosingEl = false;
441+
437442
// match any type of control flow block as start of string ignoring whitespace
438443
// @if | @switch | @case | @default | @for | } @else
439444
const openBlockRegex = /^\s*\@(if|switch|case|default|for)|^\s*\}\s\@else/;
@@ -442,45 +447,93 @@ export function formatTemplate(tmpl: string): string {
442447
// <div thing="stuff" [binding]="true"> || <div thing="stuff" [binding]="true"
443448
const openElRegex = /^\s*<([a-z0-9]+)(?![^>]*\/>)[^>]*>?/;
444449

450+
// regex for matching a self closing html element that has no />
451+
// <div thing="stuff" [binding]="true"> || <div thing="stuff" [binding]="true"
452+
const selfClosingRegex = new RegExp(`^\\s*<(${selfClosingList}).+\\/?>`);
453+
454+
// regex for matching a self closing html element that is on multi lines
455+
// <div thing="stuff" [binding]="true"> || <div thing="stuff" [binding]="true"
456+
const openSelfClosingRegex = new RegExp(`^\\s*<(${selfClosingList})(?![^>]*\\/>)[^>]*$`);
457+
445458
// match closing block or else block
446459
// } | } @else
447460
const closeBlockRegex = /^\s*\}\s*$|^\s*\}\s\@else/;
448461

449462
// matches closing of an html element
450463
// </element>
451-
const closeElRegex = /\s*<\/([a-z0-9\-]+)*>/;
464+
const closeElRegex = /\s*<\/([a-zA-Z0-9\-]+)*>/;
452465

453466
// matches closing of a self closing html element when the element is on multiple lines
454467
// [binding]="value" />
455-
const closeMultiLineElRegex = /^\s*([a-z0-9\-\[\]]+)?=?"?([^<]+)?"?\s?\/>$/;
468+
const closeMultiLineElRegex = /^\s*([a-zA-Z0-9\-\[\]]+)?=?"?([^<]+)?"?\s?\/>$/;
469+
470+
// matches closing of a self closing html element when the element is on multiple lines
471+
// with no / in the closing: [binding]="value">
472+
const closeSelfClosingMultiLineRegex = /^\s*([a-zA-Z0-9\-\[\]]+)?=?"?([^\/<]+)?"?\s?>$/;
456473

457474
// matches an open and close of an html element on a single line with no breaks
458475
// <div>blah</div>
459-
const singleLineElRegex = /^\s*<([a-z0-9]+)(?![^>]*\/>)[^>]*>.*<\/([a-z0-9\-]+)*>/;
476+
const singleLineElRegex = /\s*<([a-zA-Z0-9]+)(?![^>]*\/>)[^>]*>.*<\/([a-zA-Z0-9\-]+)*>/;
460477
const lines = tmpl.split('\n');
461478
const formatted = [];
479+
// the indent applied during formatting
462480
let indent = '';
481+
// the pre-existing indent in an inline template that we'd like to preserve
482+
let mindent = '';
463483
for (let [index, line] of lines.entries()) {
464484
if (line.trim() === '' && index !== 0 && index !== lines.length - 1) {
465485
// skip blank lines except if it's the first line or last line
486+
// this preserves leading and trailing spaces if they are already present
466487
continue;
467488
}
489+
// preserves the indentation of an inline template
490+
if (templateType === 'template' && index <= 1) {
491+
// first real line of an inline template
492+
const ind = line.search(/\S/);
493+
mindent = (ind > -1) ? line.slice(0, ind) : '';
494+
}
495+
496+
// if a block closes, an element closes, and it's not an element on a single line or the end
497+
// of a self closing tag
468498
if ((closeBlockRegex.test(line) ||
469499
(closeElRegex.test(line) &&
470500
(!singleLineElRegex.test(line) && !closeMultiLineElRegex.test(line)))) &&
471501
indent !== '') {
472502
// close block, reduce indent
473503
indent = indent.slice(2);
474504
}
475-
formatted.push(indent + line.trim());
505+
506+
formatted.push(mindent + indent + line.trim());
507+
508+
// this matches any self closing element that actually has a />
476509
if (closeMultiLineElRegex.test(line)) {
477510
// multi line self closing tag
478511
indent = indent.slice(2);
512+
if (openSelfClosingEl) {
513+
openSelfClosingEl = false;
514+
}
515+
}
516+
517+
// this matches a self closing element that doesn't have a / in the >
518+
if (closeSelfClosingMultiLineRegex.test(line) && openSelfClosingEl) {
519+
openSelfClosingEl = false;
520+
indent = indent.slice(2);
479521
}
480-
if ((openBlockRegex.test(line) || openElRegex.test(line)) && !singleLineElRegex.test(line)) {
522+
523+
// this matches an open control flow block, an open HTML element, but excludes single line
524+
// self closing tags
525+
if ((openBlockRegex.test(line) || openElRegex.test(line)) && !singleLineElRegex.test(line) &&
526+
!selfClosingRegex.test(line) && !openSelfClosingRegex.test(line)) {
481527
// open block, increase indent
482528
indent += ' ';
483529
}
530+
531+
// This is a self closing element that is definitely not fully closed and is on multiple lines
532+
if (openSelfClosingRegex.test(line)) {
533+
openSelfClosingEl = true;
534+
// add to the indent for the properties on it to look nice
535+
indent += ' ';
536+
}
484537
}
485538
tmpl = formatted.join('\n');
486539
}

0 commit comments

Comments
 (0)