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

Handle nested classes in migrations #52309

Closed
wants to merge 2 commits into from
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,7 @@ export class AnalyzedFile {
* @param analyzedFiles Map in which to store the results.
*/
export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, AnalyzedFile>) {
for (const node of sourceFile.statements) {
if (!ts.isClassDeclaration(node)) {
continue;
}

forEachClass(sourceFile, node => {
// Note: we have a utility to resolve the Angular decorators from a class declaration already.
// We don't use it here, because it requires access to the type checker which makes it more
// time-consuming to run internally.
Expand All @@ -86,7 +82,7 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
null;

if (!metadata) {
continue;
return;
}

for (const prop of metadata.properties) {
Expand All @@ -112,7 +108,7 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
break;
}
}
}
});
}

/**
Expand Down Expand Up @@ -183,3 +179,13 @@ class TextRangeCollector extends RecursiveVisitor {
super.visitText(text, null);
}
}

/** Executes a callback on each class declaration in a file. */
function forEachClass(sourceFile: ts.SourceFile, callback: (node: ts.ClassDeclaration) => void) {
sourceFile.forEachChild(function walk(node) {
if (ts.isClassDeclaration(node)) {
callback(node);
}
node.forEachChild(walk);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@ import {AnalyzedFile, boundngif, CaseCollector, ElementCollector, ElementToMigra
* @param analyzedFiles Map in which to store the results.
*/
export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, AnalyzedFile>) {
for (const node of sourceFile.statements) {
if (!ts.isClassDeclaration(node)) {
continue;
}

forEachClass(sourceFile, node => {
// Note: we have a utility to resolve the Angular decorators from a class declaration already.
// We don't use it here, because it requires access to the type checker which makes it more
// time-consuming to run internally.
Expand All @@ -38,7 +34,7 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
null;

if (!metadata) {
continue;
return;
}

for (const prop of metadata.properties) {
Expand All @@ -64,7 +60,7 @@ export function analyze(sourceFile: ts.SourceFile, analyzedFiles: Map<string, An
break;
}
}
}
});
}

/**
Expand Down Expand Up @@ -483,3 +479,13 @@ function getSwitchCaseBlock(
return `${lbString}@case (${etm.attr.value}) {${lbString}${lbSpaces}${
tmpl.slice(elStart, attrStart) + tmpl.slice(valEnd, elEnd)}${lbString}}`;
}

/** Executes a callback on each class declaration in a file. */
function forEachClass(sourceFile: ts.SourceFile, callback: (node: ts.ClassDeclaration) => void) {
sourceFile.forEachChild(function walk(node) {
if (ts.isClassDeclaration(node)) {
callback(node);
}
node.forEachChild(walk);
});
}
19 changes: 19 additions & 0 deletions packages/core/schematics/test/block_template_entities_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,23 @@ describe('Block template entities migration', () => {

expect(content).toBe('My email is admin&#64;test.com');
});

it('should migrate a component that is not at the top level', async () => {
writeFile('/comp.ts', `
import {Component} from '@angular/core';

function foo() {
@Component({
template: \`<div><span>My email is admin@test.com</span></div><h1>This is a brace }</h1>\`
})
class Comp {}
}
`);

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

expect(content).toContain(
'template: `<div><span>My email is admin&#64;test.com</span></div><h1>This is a brace &#125;</h1>`');
});
});
124 changes: 124 additions & 0 deletions packages/core/schematics/test/control_flow_migration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,51 @@ describe('control flow migration', () => {
`</div>`,
].join('\n'));
});

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

function foo() {
@Component({
imports: [NgIf],
template: \`<div><span *ngIf="toggle">This should be hidden</span></div>\`
})
class Comp {
toggle = false;
}
}
`);

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

expect(content).toContain(
'template: `<div>@if (toggle) {<span>This should be hidden</span>}</div>`');
});

it('should migrate a nested class', async () => {
writeFile('/comp.ts', `
import {Component} from '@angular/core';
import {NgIf} from '@angular/common';
function foo() {
@Component({
imports: [NgIf],
template: \`<div><span *ngIf="toggle">This should be hidden</span></div>\`
})
class Comp {
toggle = false;
}
}
`);

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

expect(content).toContain(
'template: `<div>@if (toggle) {<span>This should be hidden</span>}</div>`');
});
});

describe('ngFor', () => {
Expand Down Expand Up @@ -870,6 +915,59 @@ describe('control flow migration', () => {
expect(content).toContain(
'template: `@for (item of items; track item) {<ng-container [bindMe]="stuff"><p>{{item.text}}</p></ng-container>}`');
});

it('should migrate a nested class', async () => {
writeFile('/comp.ts', `
import {Component} from '@angular/core';
import {NgFor} from '@angular/common';
interface Item {
id: number;
text: string;
}

function foo() {
@Component({
imports: [NgFor],
template: \`<ul><li *ngFor="let item of items">{{item.text}}</li></ul>\`
})
class Comp {
items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}];
}
}
`);

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

expect(content).toContain(
'template: `<ul>@for (item of items; track item) {<li>{{item.text}}</li>}</ul>`');
});

it('should migrate a nested class', async () => {
writeFile('/comp.ts', `
import {Component} from '@angular/core';
import {NgFor} from '@angular/common';
interface Item {
id: number;
text: string;
}
function foo() {
@Component({
imports: [NgFor],
template: \`<ul><li *ngFor="let item of items">{{item.text}}</li></ul>\`
})
class Comp {
items: Item[] = [{id: 1, text: 'blah'},{id: 2, text: 'stuff'}];
}
}
`);

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

expect(content).toContain(
'template: `<ul>@for (item of items; track item) {<li>{{item.text}}</li>}</ul>`');
});
});

describe('ngSwitch', () => {
Expand Down Expand Up @@ -1131,6 +1229,32 @@ describe('control flow migration', () => {
expect(content).toContain(
'template: `<div>@switch (testOpts) { @case (1) { <p>Option 1</p> } @case (2) { <p>Option 2</p> } @default { <p>Option 3</p> }}</div>');
});

it('should migrate a nested class', async () => {
writeFile(
'/comp.ts',
`
import {Component} from '@angular/core';
import {ngSwitch, ngSwitchCase} from '@angular/common';
function foo() {
@Component({
template: \`<div [ngSwitch]="testOpts">` +
`<p *ngSwitchCase="1">Option 1</p>` +
`<p *ngSwitchCase="2">Option 2</p>` +
`</div>\`
})
class Comp {
testOpts = "1";
}
}
`);

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

expect(content).toContain(
'template: `<div>@switch (testOpts) { @case (1) { <p>Option 1</p> } @case (2) { <p>Option 2</p> }}</div>`');
});
});

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