angular-docs-cn/packages/compiler-cli/ngcc/test/migrations/missing_injectable_migration_spec.ts
Pete Bacon Darwin 0b837e2f0d refactor(ngcc): use bundle src to create reflection hosts (#34254)
Previously individual properties of the src bundle program were
passed to the reflection host constructors. But going forward,
more properties will be required. To prevent the signature getting
continually larger and more unwieldy, this change just passes the
whole src bundle to the constructor, allowing it to extract what it
needs.

PR Close #34254
2019-12-18 11:25:01 -08:00

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;
}
});
});