refactor(core): missing-injectable migration should respect providers of directives and components (#33011)

Currenly the `missing-injectable` migration only migrates providers referenced from
`@NgModule` definitions. The schematic currently does not cover the migration for
providers referenced in `@Directive` or `@Component` definitions.

We need to handle the following keys for directives/components:

- `@Directive` -> `providers`
- `@Component` -> `providers` and `viewProviders`.

This commit ensures that the migration handles providers for these
definitions.

PR Close #33011
This commit is contained in:
Paul Gschwendtner 2019-10-05 23:41:37 +02:00 committed by Miško Hevery
parent 52483bf680
commit 5557dec120
8 changed files with 881 additions and 695 deletions

View File

@ -9,14 +9,16 @@
import {RuleFailure, Rules} from 'tslint';
import * as ts from 'typescript';
import {NgDefinitionCollector} from '../missing-injectable/definition_collector';
import {TslintUpdateRecorder} from '../missing-injectable/google3/tslint_update_recorder';
import {NgModuleCollector} from '../missing-injectable/module_collector';
import {MissingInjectableTransform} from '../missing-injectable/transform';
/**
* TSLint rule that flags classes which are declared as providers in NgModules but
* aren't decorated with any Angular decorator.
* TSLint rule that flags classes which are declared as providers in "NgModule",
* "Directive" or "Component" definitions while not being decorated with any
* Angular decorator (e.g. "@Injectable").
*/
export class Rule extends Rules.TypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
@ -24,24 +26,24 @@ export class Rule extends Rules.TypedRule {
const typeChecker = program.getTypeChecker();
const sourceFiles = program.getSourceFiles().filter(
s => !s.isDeclarationFile && !program.isSourceFileFromExternalLibrary(s));
const moduleCollector = new NgModuleCollector(typeChecker);
const definitionCollector = new NgDefinitionCollector(typeChecker);
const failures: RuleFailure[] = [];
// Analyze source files by detecting all NgModule definitions.
sourceFiles.forEach(sourceFile => moduleCollector.visitNode(sourceFile));
// Analyze source files by detecting all "NgModule", "Directive" or
// "Component" definitions.
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));
const {resolvedModules} = moduleCollector;
const {resolvedModules, resolvedDirectives} = definitionCollector;
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder);
const updateRecorders = new Map<ts.SourceFile, TslintUpdateRecorder>();
resolvedModules.forEach(module => {
transformer.migrateModule(module).forEach(({message, node}) => {
// Only report failures for the current source file that is visited.
if (node.getSourceFile() === sourceFile) {
failures.push(
new RuleFailure(node.getSourceFile(), node.getStart(), 0, message, ruleName));
}
});
[...transformer.migrateModules(resolvedModules),
...transformer.migrateDirectives(resolvedDirectives),
].forEach(({message, node}) => {
// Only report failures for the current source file that is visited.
if (node.getSourceFile() === sourceFile) {
failures.push(new RuleFailure(node.getSourceFile(), node.getStart(), 0, message, ruleName));
}
});
// Record the changes collected in the import manager and NgModule manager.

View File

@ -18,12 +18,21 @@ export interface ResolvedNgModule {
providersExpr: ts.Expression|null;
}
export interface ResolvedDirective {
name: string;
node: ts.ClassDeclaration;
decorator: NgDecorator;
providersExpr: ts.Expression|null;
viewProvidersExpr: ts.Expression|null;
}
/**
* Visitor that walks through specified TypeScript nodes and collects all
* found NgModule definitions.
* found NgModule, Directive or Component definitions.
*/
export class NgModuleCollector {
export class NgDefinitionCollector {
resolvedModules: ResolvedNgModule[] = [];
resolvedDirectives: ResolvedDirective[] = [];
constructor(public typeChecker: ts.TypeChecker) {}
@ -41,13 +50,40 @@ export class NgModuleCollector {
}
const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
const directiveDecorator =
ngDecorators.find(({name}) => name === 'Component' || name == 'Directive');
const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule');
if (ngModuleDecorator) {
this._visitNgModuleClass(node, ngModuleDecorator);
} else if (directiveDecorator) {
this._visitDirectiveClass(node, directiveDecorator);
}
}
private _visitDirectiveClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
const decoratorCall = decorator.node.expression;
const metadata = decoratorCall.arguments[0];
if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
return;
}
const providersNode = metadata.properties.filter(ts.isPropertyAssignment)
.find(p => getPropertyNameText(p.name) === 'providers');
const viewProvidersNode = metadata.properties.filter(ts.isPropertyAssignment)
.find(p => getPropertyNameText(p.name) === 'viewProviders');
this.resolvedDirectives.push({
name: node.name ? node.name.text : 'default',
node,
decorator,
providersExpr: providersNode !== undefined ? providersNode.initializer : null,
viewProvidersExpr: viewProvidersNode !== undefined ? viewProvidersNode.initializer : null,
});
}
private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
const decoratorCall = decorator.node.expression;
const metadata = decoratorCall.arguments[0];

View File

@ -16,13 +16,13 @@ export class TslintUpdateRecorder implements UpdateRecorder {
constructor(private ruleName: string, private sourceFile: ts.SourceFile) {}
addClassDecorator(node: ts.ClassDeclaration, decoratorText: string, moduleName: string) {
addClassDecorator(node: ts.ClassDeclaration, decoratorText: string, className: string) {
// Adding a decorator should be the last replacement. Replacements/rule failures
// are handled in reverse and in case a decorator and import are inserted at
// the start of the file, the class decorator should come after the import.
this.failures.unshift(new RuleFailure(
this.sourceFile, node.getStart(), 0, `Class needs to be decorated with ` +
`"${decoratorText}" because it has been provided by "${moduleName}".`,
`"${decoratorText}" because it has been provided by "${className}".`,
this.ruleName, Replacement.appendText(node.getStart(), `${decoratorText}\n`)));
}
@ -42,7 +42,7 @@ export class TslintUpdateRecorder implements UpdateRecorder {
`Import needs to be updated to import symbols: "${newNamedBindings}"`, this.ruleName, fix));
}
replaceDecorator(decorator: ts.Node, newText: string, moduleName: string): void {
replaceDecorator(decorator: ts.Node, newText: string, className: string): void {
const fix = [
Replacement.deleteText(decorator.getStart(), decorator.getWidth()),
Replacement.appendText(decorator.getStart(), newText),
@ -50,7 +50,7 @@ export class TslintUpdateRecorder implements UpdateRecorder {
this.failures.push(new RuleFailure(
this.sourceFile, decorator.getStart(), decorator.getEnd(),
`Decorator needs to be replaced with "${newText}" because it has been provided ` +
`by "${moduleName}"`,
`by "${className}"`,
this.ruleName, fix));
}

View File

@ -12,7 +12,7 @@ import * as ts from 'typescript';
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {createMigrationCompilerHost} from '../../utils/typescript/compiler_host';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';
import {NgModuleCollector} from './module_collector';
import {NgDefinitionCollector} from './definition_collector';
import {MissingInjectableTransform} from './transform';
import {UpdateRecorder} from './update_recorder';
@ -53,25 +53,25 @@ function runMissingInjectableMigration(
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const moduleCollector = new NgModuleCollector(typeChecker);
const definitionCollector = new NgDefinitionCollector(typeChecker);
const sourceFiles = program.getSourceFiles().filter(
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
// Analyze source files by detecting all modules.
sourceFiles.forEach(sourceFile => moduleCollector.visitNode(sourceFile));
// Analyze source files by detecting all modules, directives and components.
sourceFiles.forEach(sourceFile => definitionCollector.visitNode(sourceFile));
const {resolvedModules} = moduleCollector;
const {resolvedModules, resolvedDirectives} = definitionCollector;
const transformer = new MissingInjectableTransform(typeChecker, getUpdateRecorder);
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();
resolvedModules.forEach(module => {
transformer.migrateModule(module).forEach(({message, node}) => {
const nodeSourceFile = node.getSourceFile();
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
const {line, character} =
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
});
[...transformer.migrateModules(resolvedModules),
...transformer.migrateDirectives(resolvedDirectives),
].forEach(({message, node}) => {
const nodeSourceFile = node.getSourceFile();
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
const {line, character} =
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
});
// Record the changes collected in the import manager and transformer.

View File

@ -13,10 +13,11 @@ import * as ts from 'typescript';
import {getAngularDecorators} from '../../utils/ng_decorators';
import {ResolvedDirective, ResolvedNgModule} from './definition_collector';
import {ImportManager} from './import_manager';
import {ResolvedNgModule} from './module_collector';
import {UpdateRecorder} from './update_recorder';
/** Name of decorators which imply that a given class does not need to be migrated. */
const NO_MIGRATE_DECORATORS = ['Injectable', 'Directive', 'Component', 'Pipe'];
@ -42,6 +43,24 @@ export class MissingInjectableTransform {
recordChanges() { this.importManager.recordChanges(); }
/**
* Migrates all specified NgModule's by walking through referenced providers
* and decorating them with "@Injectable" if needed.
*/
migrateModules(modules: ResolvedNgModule[]): AnalysisFailure[] {
return modules.reduce(
(failures, node) => failures.concat(this.migrateModule(node)), [] as AnalysisFailure[]);
}
/**
* Migrates all specified directives by walking through referenced providers
* and decorating them with "@Injectable" if needed.
*/
migrateDirectives(directives: ResolvedDirective[]): AnalysisFailure[] {
return directives.reduce(
(failures, node) => failures.concat(this.migrateDirective(node)), [] as AnalysisFailure[]);
}
/** Migrates a given NgModule by walking through the referenced providers. */
migrateModule(module: ResolvedNgModule): AnalysisFailure[] {
if (module.providersExpr === null) {
@ -60,11 +79,43 @@ export class MissingInjectableTransform {
return this._visitProviderResolvedValue(evaluatedExpr, module);
}
/**
* Migrates a given directive by walking through defined providers. This method
* also handles components with "viewProviders" defined.
*/
migrateDirective(directive: ResolvedDirective): AnalysisFailure[] {
const failures: AnalysisFailure[] = [];
// Migrate "providers" on directives and components if defined.
if (directive.providersExpr) {
const evaluatedExpr = this.partialEvaluator.evaluate(directive.providersExpr);
if (!Array.isArray(evaluatedExpr)) {
return [
{node: directive.providersExpr, message: `Providers are not statically analyzable.`}
];
}
failures.push(...this._visitProviderResolvedValue(evaluatedExpr, directive));
}
// Migrate "viewProviders" on components if defined.
if (directive.viewProvidersExpr) {
const evaluatedExpr = this.partialEvaluator.evaluate(directive.viewProvidersExpr);
if (!Array.isArray(evaluatedExpr)) {
return [
{node: directive.viewProvidersExpr, message: `Providers are not statically analyzable.`}
];
}
failures.push(...this._visitProviderResolvedValue(evaluatedExpr, directive));
}
return failures;
}
/**
* Migrates a given provider class if it is not decorated with
* any Angular decorator.
*/
migrateProviderClass(node: ts.ClassDeclaration, module: ResolvedNgModule) {
migrateProviderClass(node: ts.ClassDeclaration, context: ResolvedNgModule|ResolvedDirective) {
if (this.visitedProviderClasses.has(node)) {
return;
}
@ -93,9 +144,9 @@ export class MissingInjectableTransform {
const existingInjectDecorator =
ngDecorators !== null ? ngDecorators.find(d => d.name === 'Inject') : null;
if (existingInjectDecorator) {
updateRecorder.replaceDecorator(existingInjectDecorator.node, newDecoratorText, module.name);
updateRecorder.replaceDecorator(existingInjectDecorator.node, newDecoratorText, context.name);
} else {
updateRecorder.addClassDecorator(node, newDecoratorText, module.name);
updateRecorder.addClassDecorator(node, newDecoratorText, context.name);
}
}

View File

@ -16,7 +16,7 @@ import * as ts from 'typescript';
export interface UpdateRecorder {
addNewImport(start: number, importText: string): void;
updateExistingImport(namedBindings: ts.NamedImports, newNamedBindings: string): void;
addClassDecorator(node: ts.ClassDeclaration, text: string, moduleName: string): void;
replaceDecorator(node: ts.Decorator, newText: string, moduleName: string): void;
addClassDecorator(node: ts.ClassDeclaration, text: string, className: string): void;
replaceDecorator(node: ts.Decorator, newText: string, className: string): void;
commitUpdate(): void;
}

View File

@ -45,183 +45,224 @@ describe('Google3 missing injectable tslint rule', () => {
function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); }
it('should create proper failures for missing injectable providers', () => {
writeFile('index.ts', `
import { NgModule } from '@angular/core';
describe('NgModule', () => createTests('NgModule', 'providers'));
describe('Directive', () => createTests('Directive', 'providers'));
export class A {}
describe('Component', () => {
createTests('Component', 'providers');
createTests('Component', 'viewProviders');
@NgModule({providers: [A]})
export class AppModule {}
`);
it('should migrate all providers defined in "viewProviders" and "providers" in the ' +
'same component',
() => {
writeFile('/index.ts', `
import {Component} from '@angular/core';
export class MyService {}
export class MySecondService {}
@Component({
providers: [MyService],
viewProviders: [MySecondService],
})
export class TestClass {}
`);
const linter = runTSLint(false);
const failures = linter.getResult().failures;
const result = runTSLint().getResult();
expect(failures.length).toBe(2);
expect(failures[0].getFailure())
.toMatch(/Class needs to be decorated with "@Injectable\(\)".*provided by "AppModule"/);
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 6});
expect(failures[1].getFailure()).toMatch(/Import needs to be updated to import.*Injectable/);
expect(failures[1].getStartPosition().getLineAndCharacter()).toEqual({line: 1, character: 13});
expect(result.errorCount).toBe(0);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MySecondService/);
expect(getFile('/index.ts')).toContain(`{ Component, Injectable } from '@angular/core`);
});
});
it('should update provider classes which need to be migrated in Ivy', () => {
writeFile('/index.ts', `
import {Pipe, Directive, Component, NgModule} from '@angular/core';
@Pipe()
export class WithPipe {}
function createTests(
type: 'NgModule' | 'Directive' | 'Component', propName: 'providers' | 'viewProviders') {
it('should create proper failures for missing injectable providers', () => {
writeFile('index.ts', `
import { ${type} } from '@angular/core';
export class A {}
@${type}({${propName}: [A]})
export class TestClass {}
`);
const linter = runTSLint(false);
const failures = linter.getResult().failures;
expect(failures.length).toBe(2);
expect(failures[0].getFailure())
.toMatch(/Class needs to be decorated with "@Injectable\(\)".*provided by "TestClass"/);
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 8});
expect(failures[1].getFailure()).toMatch(/Import needs to be updated to import.*Injectable/);
expect(failures[1].getStartPosition().getLineAndCharacter())
.toEqual({line: 1, character: 15});
});
it('should update provider classes which need to be migrated in Ivy', () => {
writeFile('/index.ts', `
import {Pipe, Directive, Component, NgModule} from '@angular/core';
@Directive()
export class WithDirective {}
@Pipe()
export class WithPipe {}
@Directive()
export class WithDirective {}
@Component()
export class WithComponent {}
export class MyServiceA {}
export class MyServiceB {}
export class MyServiceC {}
export class MyServiceD {}
export class MyServiceE {}
export class MyServiceF {}
export class MyServiceG {}
@${type}({${propName}: [
WithPipe,
[
WithDirective,
WithComponent,
MyServiceA,
]
MyServiceB,
{provide: MyServiceC},
{provide: null, useClass: MyServiceD},
{provide: null, useExisting: MyServiceE},
{provide: MyServiceF, useFactory: () => null},
{provide: MyServiceG, useValue: null},
]})
export class TestClass {}
`);
runTSLint();
expect(getFile('/index.ts')).toMatch(/'@angular\/core';\s+@Pipe\(\)\s+export class WithPipe/);
expect(getFile('/index.ts'))
.toMatch(/WithPipe {}\s+@Directive\(\)\s+export class WithDirective/);
expect(getFile('/index.ts'))
.toMatch(/WithDirective {}\s+@Component\(\)\s+export class WithComponent/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceA/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceB/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceC/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceD/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceE/);
expect(getFile('/index.ts')).toMatch(/MyServiceE {}\s+export class MyServiceF/);
expect(getFile('/index.ts')).toMatch(/MyServiceF {}\s+export class MyServiceG/);
});
it(`should migrate provider once if referenced in multiple ${type} definitions`, () => {
writeFile('/index.ts', `
import {${type}} from '@angular/core';
@Component()
export class WithComponent {}
export class ServiceA {}
@${type}({${propName}: [ServiceA]})
export class TestClass {}
`);
writeFile('/second.ts', `
import {${type}} from '@angular/core';
import {ServiceA} from './index';
export class ServiceB {}
@${type}({${propName}: [ServiceA, ServiceB]})
export class TestClass2 {}
`);
runTSLint();
expect(getFile('/index.ts'))
.toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class ServiceA/);
expect(getFile('/index.ts')).toContain(`{ ${type}, Injectable } from '@angular/core`);
expect(getFile('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
expect(getFile('/second.ts')).toContain(`{ ${type}, Injectable } from '@angular/core`);
});
it('should warn if a referenced provider could not be resolved', () => {
writeFile('/index.ts', `
import {${type}} from '@angular/core';
@${type}({${propName}: [NotPresent]})
export class TestClass {}
`);
const linter = runTSLint();
const failures = linter.getResult().failures;
expect(failures.length).toBe(1);
expect(failures[0].getFailure()).toMatch(/Provider is not statically analyzable./);
expect(failures[0].getStartPosition().getLineAndCharacter())
.toEqual({line: 3, character: 14 + type.length + propName.length});
});
it(`should warn if the "${propName}" value could not be resolved`, () => {
writeFile('/index.ts', `
import {${type}} from '@angular/core';
@${type}({${propName}: NOT_ANALYZABLE)
export class TestClass {}
`);
const linter = runTSLint();
const failures = linter.getResult().failures;
expect(failures.length).toBe(1);
expect(failures[0].getFailure()).toMatch(/Providers.*not statically analyzable./);
expect(failures[0].getStartPosition().getLineAndCharacter())
.toEqual({line: 3, character: 13 + type.length + propName.length});
});
it('should create new import for @Injectable when migrating provider', () => {
writeFile('/index.ts', `
import {${type}} from '@angular/core';
import {MyService, MySecondService} from './service';
@${type}({${propName}: [MyService, MySecondService]})
export class TestClass {}
`);
writeFile('/service.ts', `export class MyService {}
export class MyServiceA {}
export class MyServiceB {}
export class MyServiceC {}
export class MyServiceD {}
export class MyServiceE {}
export class MyServiceF {}
export class MyServiceG {}
@NgModule({providers: [
WithPipe,
[
WithDirective,
WithComponent,
MyServiceA,
]
MyServiceB,
{provide: MyServiceC},
{provide: null, useClass: MyServiceD},
{provide: null, useExisting: MyServiceE},
{provide: MyServiceF, useFactory: () => null},
{provide: MyServiceG, useValue: null},
]})
export class MyModule {}
`);
export class MySecondService {}
`);
runTSLint();
runTSLint();
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MySecondService/);
expect(getFile('/service.ts')).toMatch(/import { Injectable } from "@angular\/core";/);
});
expect(getFile('/index.ts')).toMatch(/'@angular\/core';\s+@Pipe\(\)\s+export class WithPipe/);
expect(getFile('/index.ts'))
.toMatch(/WithPipe {}\s+@Directive\(\)\s+export class WithDirective/);
expect(getFile('/index.ts'))
.toMatch(/WithDirective {}\s+@Component\(\)\s+export class WithComponent/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceA/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceB/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceC/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceD/);
expect(getFile('/index.ts')).toMatch(/@Injectable\(\)\s+export class MyServiceE/);
expect(getFile('/index.ts')).toMatch(/MyServiceE {}\s+export class MyServiceF/);
expect(getFile('/index.ts')).toMatch(/MyServiceF {}\s+export class MyServiceG/);
});
it('should remove @Inject decorator for providers which are migrated', () => {
writeFile('/index.ts', `
import {${type}} from '@angular/core';
import {MyService} from './service';
@${type}({${propName}: [MyService]})
export class TestClass {}
`);
it('should migrate provider once if referenced in multiple NgModule definitions', () => {
writeFile('/index.ts', `
import {NgModule} from '@angular/core';
export class ServiceA {}
@NgModule({providers: [ServiceA]})
export class MyModule {}
`);
writeFile('/second.ts', `
import {NgModule} from '@angular/core';
import {ServiceA} from './index';
writeFile('/service.ts', `
import {Inject} from '@angular/core';
export class ServiceB {}
@NgModule({providers: [ServiceA, ServiceB]})
export class SecondModule {}
`);
@Inject()
export class MyService {}
`);
runTSLint();
runTSLint();
expect(getFile('/index.ts'))
.toMatch(/@angular\/core';\s+@Injectable\(\)\s+export class ServiceA/);
expect(getFile('/index.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
expect(getFile('/second.ts')).toMatch(/@Injectable\(\)\s+export class ServiceB/);
expect(getFile('/second.ts')).toMatch(/{ NgModule, Injectable } from '@angular\/core/);
});
expect(getFile('/service.ts')).toMatch(/core';\s+@Injectable\(\)\s+export class MyService/);
expect(getFile('/service.ts'))
.toMatch(/import { Inject, Injectable } from '@angular\/core';/);
});
}
it('should warn if a referenced provider could not be resolved', () => {
writeFile('/index.ts', `
import {NgModule} from '@angular/core';
@NgModule({providers: [NotPresent]})
export class MyModule {}
`);
const linter = runTSLint();
const failures = linter.getResult().failures;
expect(failures.length).toBe(1);
expect(failures[0].getFailure()).toMatch(/Provider is not statically analyzable./);
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 29});
});
it('should warn if the module providers could not be resolved', () => {
writeFile('/index.ts', `
import {NgModule} from '@angular/core';
@NgModule({providers: NOT_ANALYZABLE)
export class MyModule {}
`);
const linter = runTSLint();
const failures = linter.getResult().failures;
expect(failures.length).toBe(1);
expect(failures[0].getFailure()).toMatch(/Providers of module.*not statically analyzable./);
expect(failures[0].getStartPosition().getLineAndCharacter()).toEqual({line: 3, character: 28});
});
it('should create new import for @Injectable when migrating provider', () => {
writeFile('/index.ts', `
import {NgModule} from '@angular/core';
import {MyService, MySecondService} from './service';
@NgModule({providers: [MyService, MySecondService]})
export class MyModule {}
`);
writeFile('/service.ts', `export class MyService {}
export class MySecondService {}
`);
runTSLint();
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MyService/);
expect(getFile('/service.ts')).toMatch(/@Injectable\(\)\s+export class MySecondService/);
expect(getFile('/service.ts')).toMatch(/import { Injectable } from "@angular\/core";/);
});
it('should remove @Inject decorator for providers which are migrated', () => {
writeFile('/index.ts', `
import {NgModule} from '@angular/core';
import {MyService} from './service';
@NgModule({providers: [MyService]})
export class MyModule {}
`);
writeFile('/service.ts', `
import {Inject} from '@angular/core';
@Inject()
export class MyService {}
`);
runTSLint();
expect(getFile('/service.ts')).toMatch(/core';\s+@Injectable\(\)\s+export class MyService/);
expect(getFile('/service.ts')).toMatch(/import { Inject, Injectable } from '@angular\/core';/);
});
});