fix(core): undecorated-classes-with-decorated-fields migration does not decorate derived classes (#35339)
The `undecorated-classes-with-decorated-fields` migration has been
introduced with 904a2018e0
, but misses
logic for decorating derived classes of undecorated classes which use
Angular features. Example scenario:
```ts
export abstract class MyBaseClass {
@Input() someInput = true;
}
export abstract class BaseClassTwo extends MyBaseClass {}
@Component(...)
export class MyButton extends BaseClassTwo {}
```
Both abstract classes would need to be migrated. Previously, the migration
only added `@Directive()` to `MyBaseClass`, but with this change, it
also decorates `BaseClassTwo`.
This is necessary because the Angular Compiler requires `BaseClassTwo` to
have a directive definition when it flattens the directive metadata for
`MyButton` in order to perform type checking. Technically, not decorating
`BaseClassTwo` does not break at runtime.
We basically want to enforce consistent use of `@Directive` to simplify the
mental model. [See the migration guide](https://angular.io/guide/migration-undecorated-classes#migrating-classes-that-use-field-decorators).
Fixes #34376.
PR Close #35339
This commit is contained in:

committed by
Kara Erickson

parent
2366480250
commit
32eafef6a7
@ -64,7 +64,7 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
|
||||
|
||||
expect(failures.length).toBe(1);
|
||||
expect(failures[0])
|
||||
.toBe('Classes with decorated fields must have an Angular decorator as well.');
|
||||
.toBe('Class needs to be decorated with "@Directive()" because it uses Angular features.');
|
||||
});
|
||||
|
||||
it(`should add an import for Directive if there isn't one already`, () => {
|
||||
@ -97,6 +97,27 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
|
||||
expect(getFile('/index.ts')).toContain(`import { Directive, Input } from '@angular/core';`);
|
||||
});
|
||||
|
||||
it('should not generate conflicting imports there is a different `Directive` symbol', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { HostBinding } from '@angular/core';
|
||||
|
||||
export class Directive {
|
||||
// Simulates a scenario where a library defines a class named "Directive".
|
||||
// We don't want to generate a conflicting import.
|
||||
}
|
||||
|
||||
export class MyLibrarySharedBaseClass {
|
||||
@HostBinding('class.active') isActive: boolean;
|
||||
}
|
||||
`);
|
||||
|
||||
runTSLint(true);
|
||||
const fileContent = getFile('/index.ts');
|
||||
expect(fileContent)
|
||||
.toContain(`import { HostBinding, Directive as Directive_1 } from '@angular/core';`);
|
||||
expect(fileContent).toMatch(/@Directive_1\(\)\s+export class MyLibrarySharedBaseClass/);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Input', () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input } from '@angular/core';
|
||||
@ -229,4 +250,35 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
|
||||
expect(getFile('/index.ts')).toContain(`@Directive()\nexport class Base {`);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated derived classes of a migrated class', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Directive, NgModule } from '@angular/core';
|
||||
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class DerivedA extends Base {}
|
||||
export class DerivedB extends DerivedA {}
|
||||
export class DerivedC extends DerivedB {}
|
||||
|
||||
@Directive({selector: 'my-comp'})
|
||||
export class MyComp extends DerivedC {}
|
||||
|
||||
export class MyCompWrapped extends MyComp {}
|
||||
|
||||
@NgModule({declarations: [MyComp, MyCompWrapped]})
|
||||
export class AppModule {}
|
||||
`);
|
||||
|
||||
runTSLint(true);
|
||||
const fileContent = getFile('/index.ts');
|
||||
expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedB/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/);
|
||||
expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/);
|
||||
expect(fileContent).toMatch(/}\s+export class MyCompWrapped/);
|
||||
});
|
||||
});
|
||||
|
@ -74,6 +74,27 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
.toContain(`import { Directive, Input } from '@angular/core';`);
|
||||
});
|
||||
|
||||
it('should not generate conflicting imports there is a different `Directive` symbol', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { HostBinding } from '@angular/core';
|
||||
|
||||
export class Directive {
|
||||
// Simulates a scenario where a library defines a class named "Directive".
|
||||
// We don't want to generate a conflicting import.
|
||||
}
|
||||
|
||||
export class MyLibrarySharedBaseClass {
|
||||
@HostBinding('class.active') isActive: boolean;
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
const fileContent = tree.readContent('/index.ts');
|
||||
expect(fileContent)
|
||||
.toContain(`import { HostBinding, Directive as Directive_1 } from '@angular/core';`);
|
||||
expect(fileContent).toMatch(/@Directive_1\(\)\s+export class MyLibrarySharedBaseClass/);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Input', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input } from '@angular/core';
|
||||
@ -206,6 +227,38 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated derived classes of a migrated class', async() => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Directive, NgModule } from '@angular/core';
|
||||
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class DerivedA extends Base {}
|
||||
export class DerivedB extends DerivedA {}
|
||||
export class DerivedC extends DerivedB {}
|
||||
|
||||
@Directive({selector: 'my-comp'})
|
||||
export class MyComp extends DerivedC {}
|
||||
|
||||
export class MyCompWrapped extends MyComp {}
|
||||
|
||||
@NgModule({declarations: [MyComp, MyCompWrapped]})
|
||||
export class AppModule {}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
const fileContent = tree.readContent('/index.ts');
|
||||
expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedB/);
|
||||
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/);
|
||||
expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/);
|
||||
expect(fileContent).toMatch(/}\s+export class MyCompWrapped/);
|
||||
});
|
||||
|
||||
function writeFile(filePath: string, contents: string) {
|
||||
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||
}
|
||||
|
Reference in New Issue
Block a user