592 lines
21 KiB
TypeScript
592 lines
21 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
import * as ts from 'typescript';
|
|
|
|
import {AbsoluteFsPath, absoluteFrom, getFileSystem} from '../../../src/ngtsc/file_system';
|
|
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
|
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
|
|
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
|
|
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
|
|
import {DecorationAnalyses} from '../../src/analysis/types';
|
|
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
|
|
import {MissingInjectableMigration, getAngularCoreDecoratorName} from '../../src/migrations/missing_injectable_migration';
|
|
import {MockLogger} from '../helpers/mock_logger';
|
|
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
|
|
|
|
runInEachFileSystem(() => {
|
|
describe('MissingInjectableMigration', () => {
|
|
let _: typeof absoluteFrom;
|
|
let INDEX_FILENAME: AbsoluteFsPath;
|
|
beforeEach(() => {
|
|
_ = absoluteFrom;
|
|
INDEX_FILENAME = _('/node_modules/test-package/index.js');
|
|
});
|
|
|
|
describe('NgModule', () => runTests('NgModule', 'providers'));
|
|
describe('Directive', () => runTests('Directive', 'providers'));
|
|
|
|
describe('Component', () => {
|
|
runTests('Component', 'providers');
|
|
runTests('Component', 'viewProviders');
|
|
|
|
it('should migrate all providers defined in "viewProviders" and "providers" in the same ' +
|
|
'component',
|
|
() => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {Component} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
export class ServiceC {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: Component, args: [{
|
|
template: "",
|
|
providers: [ServiceA],
|
|
viewProviders: [ServiceB],
|
|
}]
|
|
}
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceB')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceC')).toBe(false);
|
|
});
|
|
});
|
|
|
|
function runTests(
|
|
type: 'NgModule' | 'Directive' | 'Component', propName: 'providers' | 'viewProviders') {
|
|
const args = type === 'Component' ? 'template: "", ' : '';
|
|
|
|
it(`should migrate type provider in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
export class OtherService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'OtherService')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate object literal provider in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
export class OtherService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [{provide: MyService}]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'OtherService')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate object literal provider with forwardRef in ${type}`, async() => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}, forwardRef} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [{provide: forwardRef(() => MyService) }]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
});
|
|
|
|
it(`should not migrate object literal provider with "useValue" in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [{provide: MyService, useValue: null }]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it(`should not migrate object literal provider with "useFactory" in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [{provide: MyService, useFactory: () => null }]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it(`should not migrate object literal provider with "useExisting" in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
export class MyToken {}
|
|
export class MyTokenAlias {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [
|
|
MyService,
|
|
{provide: MyToken, useExisting: MyService},
|
|
{provide: MyTokenAlias, useExisting: MyToken},
|
|
]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'MyToken')).toBe(false);
|
|
expect(hasInjectableDecorator(index, analysis, 'MyTokenAlias')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate object literal provider with "useClass" in ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
export class MyToken {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [{provide: MyToken, useClass: MyService}]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'MyToken')).toBe(false);
|
|
});
|
|
|
|
it('should not migrate provider which is already decorated with @Injectable', () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {Injectable, ${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
MyService.decorators = [
|
|
{ type: Injectable }
|
|
];
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(getInjectableDecorators(index, analysis, 'MyService').length).toBe(1);
|
|
});
|
|
|
|
it('should not migrate provider which is already decorated with @Directive', () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {Directive, ${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
MyService.decorators = [
|
|
{ type: Directive }
|
|
];
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it('should not migrate provider which is already decorated with @Component', () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {Component, ${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
MyService.decorators = [
|
|
{ type: Component, args: [{template: ""}] }
|
|
];
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it('should not migrate provider which is already decorated with @Pipe', () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {Pipe, ${type}} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
MyService.decorators = [
|
|
{ type: Pipe, args: [{name: "pipe"}] }
|
|
];
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate multiple providers in same ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [ServiceA, ServiceB]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceB')).toBe(true);
|
|
});
|
|
|
|
it(`should migrate multiple mixed providers in same ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
export class ServiceC {}
|
|
export class ServiceD {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [
|
|
ServiceA,
|
|
{provide: ServiceB},
|
|
{provide: SomeToken, useClass: ServiceC},
|
|
]
|
|
}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceB')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceC')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceD')).toBe(false);
|
|
});
|
|
|
|
|
|
it(`should migrate multiple nested providers in same ${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
export class ServiceC {}
|
|
export class ServiceD {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [
|
|
ServiceA,
|
|
[
|
|
{provide: ServiceB},
|
|
ServiceC,
|
|
],
|
|
]}]
|
|
}
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceB')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceC')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceD')).toBe(false);
|
|
});
|
|
|
|
it('should migrate providers referenced indirectly', () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
export class ServiceC {}
|
|
|
|
const PROVIDERS = [ServiceA, ServiceB];
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: PROVIDERS}] }
|
|
];
|
|
`
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceB')).toBe(true);
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceC')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate provider once if referenced in multiple ${type} definitions`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
export class ServiceB {}
|
|
|
|
export class TestClassA {}
|
|
TestClassA.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [ServiceA]}] }
|
|
];
|
|
|
|
export class TestClassB {}
|
|
TestClassB.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [ServiceA, ServiceB]}] }
|
|
];
|
|
`
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(getInjectableDecorators(index, analysis, 'ServiceA').length).toBe(1);
|
|
expect(getInjectableDecorators(index, analysis, 'ServiceB').length).toBe(1);
|
|
});
|
|
|
|
type !== 'Component' && it(`should support @${type} without metadata argument`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
|
|
export class ServiceA {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'ServiceA')).toBe(false);
|
|
});
|
|
|
|
it(`should migrate services in a different file`, () => {
|
|
const SERVICE_FILENAME = _('/node_modules/test-package/service.js');
|
|
const {program, analysis} = setUpAndAnalyzeProgram([
|
|
{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
import {MyService} from './service';
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
},
|
|
{
|
|
name: SERVICE_FILENAME,
|
|
contents: `
|
|
export declare class MyService {}
|
|
`,
|
|
}
|
|
]);
|
|
|
|
const index = program.getSourceFile(SERVICE_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
});
|
|
|
|
it(`should not migrate services in a different package`, () => {
|
|
const SERVICE_FILENAME = _('/node_modules/external/index.d.ts');
|
|
const {program, analysis} = setUpAndAnalyzeProgram([
|
|
{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from '@angular/core';
|
|
import {MyService} from 'external';
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
},
|
|
{
|
|
name: SERVICE_FILENAME,
|
|
contents: `
|
|
export declare class MyService {}
|
|
`,
|
|
}
|
|
]);
|
|
|
|
const index = program.getSourceFile(SERVICE_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
|
|
it(`should deal with renamed imports for @${type}`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type} as Renamed} from '@angular/core';
|
|
|
|
export class MyService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: Renamed, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(true);
|
|
});
|
|
|
|
it(`should deal with decorators named @${type} not from '@angular/core'`, () => {
|
|
const {program, analysis} = setUpAndAnalyzeProgram([{
|
|
name: INDEX_FILENAME,
|
|
contents: `
|
|
import {${type}} from 'other';
|
|
|
|
export class MyService {}
|
|
|
|
export class TestClass {}
|
|
TestClass.decorators = [
|
|
{ type: ${type}, args: [{${args}${propName}: [MyService]}] }
|
|
];
|
|
`,
|
|
}]);
|
|
|
|
const index = program.getSourceFile(INDEX_FILENAME) !;
|
|
expect(hasInjectableDecorator(index, analysis, 'MyService')).toBe(false);
|
|
});
|
|
}
|
|
|
|
function setUpAndAnalyzeProgram(testFiles: TestFile[]) {
|
|
loadTestFiles(testFiles);
|
|
loadFakeCore(getFileSystem());
|
|
const errors: ts.Diagnostic[] = [];
|
|
const rootFiles = getRootFiles(testFiles);
|
|
const bundle = makeTestEntryPointBundle('test-package', 'esm2015', false, rootFiles);
|
|
const program = bundle.src.program;
|
|
|
|
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, bundle.src);
|
|
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
|
|
const analyzer = new DecorationAnalyzer(
|
|
getFileSystem(), bundle, reflectionHost, referencesRegistry, error => errors.push(error));
|
|
analyzer.migrations = [new MissingInjectableMigration()];
|
|
return {program, analysis: analyzer.analyzeProgram(), errors};
|
|
}
|
|
|
|
function getInjectableDecorators(
|
|
sourceFile: ts.SourceFile, analysis: DecorationAnalyses, className: string) {
|
|
const file = analysis.get(sourceFile);
|
|
if (file === undefined) {
|
|
return [];
|
|
}
|
|
|
|
const clazz = file.compiledClasses.find(c => c.name === className);
|
|
if (clazz === undefined || clazz.decorators === null) {
|
|
return [];
|
|
}
|
|
|
|
return clazz.decorators.filter(
|
|
decorator => getAngularCoreDecoratorName(decorator) === 'Injectable');
|
|
}
|
|
|
|
function hasInjectableDecorator(
|
|
sourceFile: ts.SourceFile, analysis: DecorationAnalyses, className: string) {
|
|
return getInjectableDecorators(sourceFile, analysis, className).length > 0;
|
|
}
|
|
});
|
|
});
|