fix(migrations): do not incorrectly add todo for @Injectable or @Pipe (#37732)
As of v10, the `undecorated-classes-with-decorated-fields` migration generally deals with undecorated classes using Angular features. We intended to run this migation as part of v10 again as undecorated classes with Angular features are no longer supported in planned v11. The migration currently behaves incorrectly in some cases where an `@Injectable` or `@Pipe` decorated classes uses the `ngOnDestroy` lifecycle hook. We incorrectly add a TODO for those classes. This commit fixes that. Additionally, this change makes the migration more robust to not migrate a class if it inherits from a component, pipe injectable or non-abstract directive. We previously did not need this as the undecorated-classes-with-di migration ran before, but this is no longer the case. Last, this commit fixes an issue where multiple TODO's could be added. This happens when multiple Angular CLI build targets have an overlap in source files. Multiple programs then capture the same source file, causing the migration to detect an undecorated class multiple times (i.e. adding a TODO twice). Fixes #37726. PR Close #37732
This commit is contained in:

committed by
Andrew Kushnir

parent
ce879fc416
commit
d12cdb5019
@ -136,24 +136,36 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
|
||||
|
||||
it('should not change decorated classes', () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Component, Output, EventEmitter } from '@angular/core';
|
||||
import { Input, Component, Directive, Pipe, Injectable } from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
export class Base {
|
||||
export class MyComp {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'dir'})
|
||||
export class MyDir {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class Child extends Base {
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
runTSLint(true);
|
||||
const content = getFile('/index.ts');
|
||||
expect(content).toContain(
|
||||
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
|
||||
expect(content).toContain(`@Component({})\n export class Base {`);
|
||||
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
|
||||
expect(content).toMatch(/@Component\({}\)\s+export class MyComp {/);
|
||||
expect(content).toMatch(/@Directive\({selector: 'dir'}\)\s+export class MyDir {/);
|
||||
expect(content).toMatch(/@Injectable\(\)\s+export class MyService {/);
|
||||
expect(content).toMatch(/@Pipe\({name: 'my-pipe'}\)\s+export class MyPipe {/);
|
||||
expect(content).not.toContain('TODO');
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Output', () => {
|
||||
|
@ -9,6 +9,7 @@
|
||||
/**
|
||||
* Template string function that can be used to dedent the resulting
|
||||
* string literal. The smallest common indentation will be omitted.
|
||||
* Additionally, whitespace in empty lines is removed.
|
||||
*/
|
||||
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
let joinedString = '';
|
||||
@ -24,5 +25,7 @@ export function dedent(strings: TemplateStringsArray, ...values: any[]) {
|
||||
|
||||
const minLineIndent = Math.min(...matches.map(el => el.length));
|
||||
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
|
||||
return minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
|
||||
const omitEmptyLineWhitespaceRegex = /^[ \t]+$/gm;
|
||||
const result = minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
|
||||
return result.replace(omitEmptyLineWhitespaceRegex, '');
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
|
||||
import {HostTree} from '@angular-devkit/schematics';
|
||||
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
|
||||
import * as shx from 'shelljs';
|
||||
import {dedent} from './helpers';
|
||||
|
||||
describe('Undecorated classes with decorated fields migration', () => {
|
||||
let runner: SchematicTestRunner;
|
||||
@ -117,26 +118,253 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`);
|
||||
});
|
||||
|
||||
it('should not change decorated classes', async () => {
|
||||
writeFile('/index.ts', `
|
||||
import { Input, Component, Output, EventEmitter } from '@angular/core';
|
||||
it('should not migrate classes decorated with @Component', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({})
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
export class Child extends Base {
|
||||
@Output() clicked = new EventEmitter<void>();
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Derived extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
const content = tree.readContent('/index.ts');
|
||||
expect(content).toContain(
|
||||
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
|
||||
expect(content).toContain(`@Component({})\n export class Base {`);
|
||||
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Component({selector: 'hello', template: 'hello'})
|
||||
export class Derived extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate classes decorated with @Directive', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Directive} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'other'})
|
||||
export class Other extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Directive} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class Base {
|
||||
@Input() isActive: boolean;
|
||||
}
|
||||
|
||||
@Directive({selector: 'other'})
|
||||
export class Other extends Base {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from component', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'my-comp', template: 'my-comp'})
|
||||
export class MyComp {}
|
||||
|
||||
export class WithDisabled extends MyComp {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Input, Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'my-comp', template: 'my-comp'})
|
||||
export class MyComp {}
|
||||
|
||||
export class WithDisabled extends MyComp {
|
||||
@Input() disabled: boolean;
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from pipe', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {}
|
||||
|
||||
export class PipeDerived extends MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {}
|
||||
|
||||
export class PipeDerived extends MyPipe {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from injectable', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
|
||||
export class ServiceDerived extends MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
|
||||
export class ServiceDerived extends MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not migrate when class inherits from directive', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({selector: 'hello'})
|
||||
export class MyDir {}
|
||||
|
||||
export class DirDerived extends MyDir {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Directive} from '@angular/core';
|
||||
|
||||
@Directive({selector: 'hello'})
|
||||
export class MyDir {}
|
||||
|
||||
export class DirDerived extends MyDir {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not add multiple TODOs for ambiguous classes', async () => {
|
||||
writeFile('/angular.json', JSON.stringify({
|
||||
projects: {
|
||||
test: {
|
||||
architect: {
|
||||
build: {options: {tsConfig: './tsconfig.json'}},
|
||||
test: {options: {tsConfig: './tsconfig.json'}},
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
writeFile('/index.ts', dedent`
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
// TODO: Add Angular decorator.
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not report pipe using `ngOnDestroy` as ambiguous', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
transform() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Pipe} from '@angular/core';
|
||||
|
||||
@Pipe({name: 'my-pipe'})
|
||||
export class MyPipe {
|
||||
ngOnDestroy() {}
|
||||
transform() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not report injectable using `ngOnDestroy` as ambiguous', async () => {
|
||||
writeFile('/index.ts', dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
|
||||
await runMigration();
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
expect(tree.readContent('/index.ts')).toBe(dedent`
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class MyService {
|
||||
ngOnDestroy() {}
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should add @Directive to undecorated classes that have @Output', async () => {
|
||||
@ -298,6 +526,8 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
|
||||
await runMigration();
|
||||
const fileContent = tree.readContent('/index.ts');
|
||||
|
||||
expect(warnings.length).toBe(0);
|
||||
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/);
|
||||
@ -305,6 +535,7 @@ describe('Undecorated classes with decorated fields migration', () => {
|
||||
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/);
|
||||
expect(fileContent).not.toContain('TODO: Add Angular decorator');
|
||||
});
|
||||
|
||||
it('should add @Directive to derived undecorated classes of abstract directives', async () => {
|
||||
|
Reference in New Issue
Block a user