feat(ngcc): migrate services that are missing `@Injectable()` (#33362)
A class that is provided as Angular service is required to have an `@Injectable()` decorator so that the compiler generates its injectable definition for the runtime. Applications are automatically migrated using the "missing-injectable" schematic, however libraries built for older version of Angular may not yet satisfy this requirement. This commit ports the "missing-injectable" schematic to a migration that is ran when ngcc is processing a library. This ensures that any service that is provided from an NgModule or Directive/Component will have an `@Injectable()` decorator. PR Close #33362
This commit is contained in:
parent
0de2dbfec1
commit
31b9492951
|
@ -119,7 +119,8 @@ export class DecorationAnalyzer {
|
||||||
.map(sourceFile => this.analyzeFile(sourceFile))
|
.map(sourceFile => this.analyzeFile(sourceFile))
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
const migrationHost = new DefaultMigrationHost(
|
const migrationHost = new DefaultMigrationHost(
|
||||||
this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers, analyzedFiles);
|
this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers,
|
||||||
|
this.bundle.entryPoint.path, analyzedFiles);
|
||||||
analyzedFiles.forEach(analyzedFile => this.migrateFile(migrationHost, analyzedFile));
|
analyzedFiles.forEach(analyzedFile => this.migrateFile(migrationHost, analyzedFile));
|
||||||
analyzedFiles.forEach(analyzedFile => this.resolveFile(analyzedFile));
|
analyzedFiles.forEach(analyzedFile => this.resolveFile(analyzedFile));
|
||||||
const compiledFiles = analyzedFiles.map(analyzedFile => this.compileFile(analyzedFile));
|
const compiledFiles = analyzedFiles.map(analyzedFile => this.compileFile(analyzedFile));
|
||||||
|
|
|
@ -6,15 +6,18 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
import {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
|
||||||
|
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
|
||||||
import {MetadataReader} from '../../../src/ngtsc/metadata';
|
import {MetadataReader} from '../../../src/ngtsc/metadata';
|
||||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||||
import {DecoratorHandler} from '../../../src/ngtsc/transform';
|
import {DecoratorHandler} from '../../../src/ngtsc/transform';
|
||||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||||
import {MigrationHost} from '../migrations/migration';
|
import {MigrationHost} from '../migrations/migration';
|
||||||
|
|
||||||
import {AnalyzedClass, AnalyzedFile} from './types';
|
import {AnalyzedClass, AnalyzedFile} from './types';
|
||||||
import {analyzeDecorators} from './util';
|
import {analyzeDecorators, isWithinPackage} from './util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The standard implementation of `MigrationHost`, which is created by the
|
* The standard implementation of `MigrationHost`, which is created by the
|
||||||
|
@ -24,7 +27,7 @@ export class DefaultMigrationHost implements MigrationHost {
|
||||||
constructor(
|
constructor(
|
||||||
readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader,
|
readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader,
|
||||||
readonly evaluator: PartialEvaluator, private handlers: DecoratorHandler<any, any>[],
|
readonly evaluator: PartialEvaluator, private handlers: DecoratorHandler<any, any>[],
|
||||||
private analyzedFiles: AnalyzedFile[]) {}
|
private entryPointPath: AbsoluteFsPath, private analyzedFiles: AnalyzedFile[]) {}
|
||||||
|
|
||||||
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void {
|
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void {
|
||||||
const classSymbol = this.reflectionHost.getClassSymbol(clazz) !;
|
const classSymbol = this.reflectionHost.getClassSymbol(clazz) !;
|
||||||
|
@ -41,6 +44,25 @@ export class DefaultMigrationHost implements MigrationHost {
|
||||||
mergeAnalyzedClasses(oldAnalyzedClass, newAnalyzedClass);
|
mergeAnalyzedClasses(oldAnalyzedClass, newAnalyzedClass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllDecorators(clazz: ClassDeclaration): Decorator[]|null {
|
||||||
|
const sourceFile = clazz.getSourceFile();
|
||||||
|
const analyzedFile = this.analyzedFiles.find(file => file.sourceFile === sourceFile);
|
||||||
|
if (analyzedFile === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz);
|
||||||
|
if (analyzedClass === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return analyzedClass.decorators;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInScope(clazz: ClassDeclaration): boolean {
|
||||||
|
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrCreateAnalyzedFile(
|
function getOrCreateAnalyzedFile(
|
||||||
|
|
|
@ -42,4 +42,20 @@ export interface MigrationHost {
|
||||||
* @param decorator the decorator to inject.
|
* @param decorator the decorator to inject.
|
||||||
*/
|
*/
|
||||||
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void;
|
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all decorators that are associated with the class, including synthetic decorators
|
||||||
|
* that have been injected before.
|
||||||
|
* @param clazz the class for which all decorators are retrieved.
|
||||||
|
* @returns the list of the decorators, or null if the class was not decorated.
|
||||||
|
*/
|
||||||
|
getAllDecorators(clazz: ClassDeclaration): Decorator[]|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the provided class in within scope of the entry-point that is currently
|
||||||
|
* being compiled.
|
||||||
|
* @param clazz the class for which to determine whether it is within the current entry-point.
|
||||||
|
* @returns true if the file is part of the compiled entry-point, false otherwise.
|
||||||
|
*/
|
||||||
|
isInScope(clazz: ClassDeclaration): boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,192 @@
|
||||||
|
/**
|
||||||
|
* @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 {forwardRefResolver} from '../../../src/ngtsc/annotations';
|
||||||
|
import {Reference} from '../../../src/ngtsc/imports';
|
||||||
|
import {ResolvedValue, ResolvedValueMap} from '../../../src/ngtsc/partial_evaluator';
|
||||||
|
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||||
|
|
||||||
|
import {Migration, MigrationHost} from './migration';
|
||||||
|
import {createInjectableDecorator, isClassDeclaration} from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that classes that are provided as an Angular service in either `NgModule.providers` or
|
||||||
|
* `Directive.providers`/`Component.viewProviders` are decorated with one of the `@Injectable`,
|
||||||
|
* `@Directive`, `@Component` or `@Pipe` decorators, adding an `@Injectable()` decorator when none
|
||||||
|
* are present.
|
||||||
|
*
|
||||||
|
* At least one decorator is now mandatory, as otherwise the compiler would not compile an
|
||||||
|
* injectable definition for the service. This is unlike View Engine, where having just an unrelated
|
||||||
|
* decorator may have been sufficient for the service to become injectable.
|
||||||
|
*
|
||||||
|
* In essence, this migration operates on classes that are themselves an NgModule, Directive or
|
||||||
|
* Component. Their metadata is statically evaluated so that their "providers"/"viewProviders"
|
||||||
|
* properties can be analyzed. For any provider that refers to an undecorated class, the class will
|
||||||
|
* be migrated to have an `@Injectable()` decorator.
|
||||||
|
*
|
||||||
|
* This implementation mirrors the "missing-injectable" schematic.
|
||||||
|
*/
|
||||||
|
export class MissingInjectableMigration implements Migration {
|
||||||
|
apply(clazz: ClassDeclaration, host: MigrationHost): ts.Diagnostic|null {
|
||||||
|
const decorators = host.reflectionHost.getDecoratorsOfDeclaration(clazz);
|
||||||
|
if (decorators === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
const name = getAngularCoreDecoratorName(decorator);
|
||||||
|
if (name === 'NgModule') {
|
||||||
|
migrateNgModuleProviders(decorator, host);
|
||||||
|
} else if (name === 'Directive') {
|
||||||
|
migrateDirectiveProviders(decorator, host, /* isComponent */ false);
|
||||||
|
} else if (name === 'Component') {
|
||||||
|
migrateDirectiveProviders(decorator, host, /* isComponent */ true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates through all `NgModule.providers` and adds the `@Injectable()` decorator to any provider
|
||||||
|
* that is not otherwise decorated.
|
||||||
|
*/
|
||||||
|
function migrateNgModuleProviders(decorator: Decorator, host: MigrationHost): void {
|
||||||
|
if (decorator.args === null || decorator.args.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = host.evaluator.evaluate(decorator.args[0], forwardRefResolver);
|
||||||
|
if (!(metadata instanceof Map)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateProviders(metadata, 'providers', host);
|
||||||
|
// TODO(alxhub): we should probably also check for `ModuleWithProviders` here.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates through all `Directive.providers` and if `isComponent` is set to true also
|
||||||
|
* `Component.viewProviders` and adds the `@Injectable()` decorator to any provider that is not
|
||||||
|
* otherwise decorated.
|
||||||
|
*/
|
||||||
|
function migrateDirectiveProviders(
|
||||||
|
decorator: Decorator, host: MigrationHost, isComponent: boolean): void {
|
||||||
|
if (decorator.args === null || decorator.args.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = host.evaluator.evaluate(decorator.args[0], forwardRefResolver);
|
||||||
|
if (!(metadata instanceof Map)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
migrateProviders(metadata, 'providers', host);
|
||||||
|
if (isComponent) {
|
||||||
|
migrateProviders(metadata, 'viewProviders', host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an object with decorator metadata, iterates through the list of providers to add
|
||||||
|
* `@Injectable()` to any provider that is not otherwise decorated.
|
||||||
|
*/
|
||||||
|
function migrateProviders(metadata: ResolvedValueMap, field: string, host: MigrationHost): void {
|
||||||
|
if (!metadata.has(field)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const providers = metadata.get(field) !;
|
||||||
|
if (!Array.isArray(providers)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const provider of providers) {
|
||||||
|
migrateProvider(provider, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes a single provider entry and determines the class that is required to have an
|
||||||
|
* `@Injectable()` decorator.
|
||||||
|
*/
|
||||||
|
function migrateProvider(provider: ResolvedValue, host: MigrationHost): void {
|
||||||
|
if (provider instanceof Map) {
|
||||||
|
if (!provider.has('provide') || provider.has('useValue') || provider.has('useFactory') ||
|
||||||
|
provider.has('useExisting')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (provider.has('useClass')) {
|
||||||
|
// {provide: ..., useClass: SomeClass, deps: [...]} does not require a decorator on SomeClass,
|
||||||
|
// as the provider itself configures 'deps'. Only if 'deps' is missing will this require a
|
||||||
|
// factory to exist on SomeClass.
|
||||||
|
if (!provider.has('deps')) {
|
||||||
|
migrateProviderClass(provider.get('useClass') !, host);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
migrateProviderClass(provider.get('provide') !, host);
|
||||||
|
}
|
||||||
|
} else if (Array.isArray(provider)) {
|
||||||
|
for (const v of provider) {
|
||||||
|
migrateProvider(v, host);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
migrateProviderClass(provider, host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a provider class, adds the `@Injectable()` decorator if no other relevant Angular decorator
|
||||||
|
* is present on the class.
|
||||||
|
*/
|
||||||
|
function migrateProviderClass(provider: ResolvedValue, host: MigrationHost): void {
|
||||||
|
// Providers that do not refer to a class cannot be migrated.
|
||||||
|
if (!(provider instanceof Reference)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clazz = provider.node as ts.Declaration;
|
||||||
|
if (isClassDeclaration(clazz) && host.isInScope(clazz) && needsInjectableDecorator(clazz, host)) {
|
||||||
|
host.injectSyntheticDecorator(clazz, createInjectableDecorator(clazz));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const NO_MIGRATE_DECORATORS = new Set(['Injectable', 'Directive', 'Component', 'Pipe']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the given class needs to be decorated with `@Injectable()` based on whether it
|
||||||
|
* already has an Angular decorator applied.
|
||||||
|
*/
|
||||||
|
function needsInjectableDecorator(clazz: ClassDeclaration, host: MigrationHost): boolean {
|
||||||
|
const decorators = host.getAllDecorators(clazz);
|
||||||
|
if (decorators === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const decorator of decorators) {
|
||||||
|
const name = getAngularCoreDecoratorName(decorator);
|
||||||
|
if (name !== null && NO_MIGRATE_DECORATORS.has(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the original name of a decorator if it is from '@angular/core'. For other decorators,
|
||||||
|
* null is returned.
|
||||||
|
*/
|
||||||
|
export function getAngularCoreDecoratorName(decorator: Decorator): string|null {
|
||||||
|
if (decorator.import === null || decorator.import.from !== '@angular/core') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decorator.import.name;
|
||||||
|
}
|
|
@ -54,6 +54,27 @@ export function createDirectiveDecorator(clazz: ClassDeclaration): Decorator {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an empty `Injectable` decorator that will be associated with the `clazz`.
|
||||||
|
*/
|
||||||
|
export function createInjectableDecorator(clazz: ClassDeclaration): Decorator {
|
||||||
|
const decoratorType = ts.createIdentifier('Injectable');
|
||||||
|
const decoratorNode = ts.createObjectLiteral([
|
||||||
|
ts.createPropertyAssignment('type', decoratorType),
|
||||||
|
ts.createPropertyAssignment('args', ts.createArrayLiteral([])),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setParentPointers(clazz.getSourceFile(), decoratorNode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: 'Injectable',
|
||||||
|
identifier: decoratorType,
|
||||||
|
import: {name: 'Injectable', from: '@angular/core'},
|
||||||
|
node: decoratorNode,
|
||||||
|
args: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that a tree of AST nodes have their parents wired up.
|
* Ensure that a tree of AST nodes have their parents wired up.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -6,156 +6,256 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {ErrorCode} from '../../../src/ngtsc/diagnostics';
|
import {ErrorCode} from '../../../src/ngtsc/diagnostics';
|
||||||
|
import {AbsoluteFsPath, absoluteFrom} from '../../../src/ngtsc/file_system';
|
||||||
|
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
|
||||||
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
|
||||||
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
|
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
|
||||||
import {DefaultMigrationHost} from '../../src/analysis/migration_host';
|
import {DefaultMigrationHost} from '../../src/analysis/migration_host';
|
||||||
import {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types';
|
import {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types';
|
||||||
import {NgccClassSymbol} from '../../src/host/ngcc_host';
|
import {NgccClassSymbol} from '../../src/host/ngcc_host';
|
||||||
|
|
||||||
describe('DefaultMigrationHost', () => {
|
runInEachFileSystem(() => {
|
||||||
describe('injectSyntheticDecorator()', () => {
|
describe('DefaultMigrationHost', () => {
|
||||||
const mockHost: any = {
|
let _: typeof absoluteFrom;
|
||||||
getClassSymbol: (node: any): NgccClassSymbol | undefined => {
|
let entryPointPath: AbsoluteFsPath;
|
||||||
const symbol = { valueDeclaration: node, name: node.name.text } as any;
|
let mockHost: any;
|
||||||
return {
|
let mockMetadata: any = {};
|
||||||
name: node.name.text,
|
let mockEvaluator: any = {};
|
||||||
declaration: symbol,
|
let mockClazz: any;
|
||||||
implementation: symbol,
|
let mockDecorator: any = {name: 'MockDecorator'};
|
||||||
|
beforeEach(() => {
|
||||||
|
_ = absoluteFrom;
|
||||||
|
entryPointPath = _('/node_modules/some-package/entry-point');
|
||||||
|
mockHost = {
|
||||||
|
getClassSymbol: (node: any): NgccClassSymbol | undefined => {
|
||||||
|
const symbol = { valueDeclaration: node, name: node.name.text } as any;
|
||||||
|
return {
|
||||||
|
name: node.name.text,
|
||||||
|
declaration: symbol,
|
||||||
|
implementation: symbol,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockSourceFile: any = {
|
||||||
|
fileName: _('/node_modules/some-package/entry-point/test-file.js'),
|
||||||
|
};
|
||||||
|
mockClazz = {
|
||||||
|
name: {text: 'MockClazz'},
|
||||||
|
getSourceFile: () => mockSourceFile,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('injectSyntheticDecorator()', () => {
|
||||||
|
it('should call `detect()` on each of the provided handlers', () => {
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler1 = new TestHandler('handler1', log);
|
||||||
|
const handler2 = new TestHandler('handler2', log);
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler1, handler2], entryPointPath, []);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(log).toEqual([
|
||||||
|
`handler1:detect:MockClazz:MockDecorator`,
|
||||||
|
`handler2:detect:MockClazz:MockDecorator`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call `analyze()` on each of the provided handlers whose `detect()` call returns a result',
|
||||||
|
() => {
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler1 = new TestHandler('handler1', log);
|
||||||
|
const handler2 = new AlwaysDetectHandler('handler2', log);
|
||||||
|
const handler3 = new TestHandler('handler3', log);
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler1, handler2, handler3],
|
||||||
|
entryPointPath, []);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(log).toEqual([
|
||||||
|
`handler1:detect:MockClazz:MockDecorator`,
|
||||||
|
`handler2:detect:MockClazz:MockDecorator`,
|
||||||
|
`handler3:detect:MockClazz:MockDecorator`,
|
||||||
|
'handler2:analyze:MockClazz',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a newly `AnalyzedFile` to the `analyzedFiles` object', () => {
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
|
const analyzedFiles: AnalyzedFile[] = [];
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(analyzedFiles.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses[0].name).toEqual('MockClazz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a newly `AnalyzedClass` to an existing `AnalyzedFile` object', () => {
|
||||||
|
const DUMMY_CLASS_1: any = {};
|
||||||
|
const DUMMY_CLASS_2: any = {};
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
|
const analyzedFiles: AnalyzedFile[] = [{
|
||||||
|
sourceFile: mockClazz.getSourceFile(),
|
||||||
|
analyzedClasses: [DUMMY_CLASS_1, DUMMY_CLASS_2],
|
||||||
|
}];
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(analyzedFiles.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses.length).toEqual(3);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses[2].name).toEqual('MockClazz');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add a new decorator into an already existing `AnalyzedClass`', () => {
|
||||||
|
const analyzedClass: AnalyzedClass = {
|
||||||
|
name: 'MockClazz',
|
||||||
|
declaration: mockClazz,
|
||||||
|
matches: [],
|
||||||
|
decorators: null,
|
||||||
};
|
};
|
||||||
},
|
const log: string[] = [];
|
||||||
};
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
const mockMetadata: any = {};
|
const analyzedFiles: AnalyzedFile[] = [{
|
||||||
const mockEvaluator: any = {};
|
sourceFile: mockClazz.getSourceFile(),
|
||||||
const mockClazz: any = {
|
analyzedClasses: [analyzedClass],
|
||||||
name: {text: 'MockClazz'},
|
}];
|
||||||
getSourceFile: () => { fileName: 'test-file.js'; },
|
const host = new DefaultMigrationHost(
|
||||||
};
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
const mockDecorator: any = {name: 'MockDecorator'};
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(analyzedFiles.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
||||||
|
expect(analyzedClass.decorators !.length).toEqual(1);
|
||||||
|
expect(analyzedClass.decorators ![0].name).toEqual('MockDecorator');
|
||||||
|
});
|
||||||
|
|
||||||
it('should call `detect()` on each of the provided handlers', () => {
|
it('should merge a new decorator into pre-existing decorators an already existing `AnalyzedClass`',
|
||||||
const log: string[] = [];
|
() => {
|
||||||
const handler1 = new TestHandler('handler1', log);
|
const analyzedClass: AnalyzedClass = {
|
||||||
const handler2 = new TestHandler('handler2', log);
|
name: 'MockClazz',
|
||||||
const host =
|
declaration: mockClazz,
|
||||||
new DefaultMigrationHost(mockHost, mockMetadata, mockEvaluator, [handler1, handler2], []);
|
matches: [],
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
decorators: [{name: 'OtherDecorator'} as Decorator],
|
||||||
expect(log).toEqual([
|
};
|
||||||
`handler1:detect:MockClazz:MockDecorator`,
|
const log: string[] = [];
|
||||||
`handler2:detect:MockClazz:MockDecorator`,
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
]);
|
const analyzedFiles: AnalyzedFile[] = [{
|
||||||
|
sourceFile: mockClazz.getSourceFile(),
|
||||||
|
analyzedClasses: [analyzedClass],
|
||||||
|
}];
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
expect(analyzedFiles.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
||||||
|
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
||||||
|
expect(analyzedClass.decorators !.length).toEqual(2);
|
||||||
|
expect(analyzedClass.decorators ![1].name).toEqual('MockDecorator');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the injected decorator already exists', () => {
|
||||||
|
const analyzedClass: AnalyzedClass = {
|
||||||
|
name: 'MockClazz',
|
||||||
|
declaration: mockClazz,
|
||||||
|
matches: [],
|
||||||
|
decorators: [{name: 'MockDecorator'} as Decorator],
|
||||||
|
};
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
|
const analyzedFiles: AnalyzedFile[] = [{
|
||||||
|
sourceFile: mockClazz.getSourceFile(),
|
||||||
|
analyzedClasses: [analyzedClass],
|
||||||
|
}];
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator))
|
||||||
|
.toThrow(jasmine.objectContaining(
|
||||||
|
{code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call `analyze()` on each of the provided handlers whose `detect()` call returns a result',
|
describe('getAllDecorators', () => {
|
||||||
() => {
|
it('should be null for unknown source files', () => {
|
||||||
const log: string[] = [];
|
const log: string[] = [];
|
||||||
const handler1 = new TestHandler('handler1', log);
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
const handler2 = new AlwaysDetectHandler('handler2', log);
|
const analyzedFiles: AnalyzedFile[] = [];
|
||||||
const handler3 = new TestHandler('handler3', log);
|
const host = new DefaultMigrationHost(
|
||||||
const host = new DefaultMigrationHost(
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
mockHost, mockMetadata, mockEvaluator, [handler1, handler2, handler3], []);
|
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
|
||||||
expect(log).toEqual([
|
|
||||||
`handler1:detect:MockClazz:MockDecorator`,
|
|
||||||
`handler2:detect:MockClazz:MockDecorator`,
|
|
||||||
`handler3:detect:MockClazz:MockDecorator`,
|
|
||||||
'handler2:analyze:MockClazz',
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a newly `AnalyzedFile` to the `analyzedFiles` object', () => {
|
const decorators = host.getAllDecorators(mockClazz);
|
||||||
const log: string[] = [];
|
expect(decorators).toBeNull();
|
||||||
const handler = new AlwaysDetectHandler('handler', log);
|
});
|
||||||
const analyzedFiles: AnalyzedFile[] = [];
|
|
||||||
const host =
|
it('should be null for unknown classes', () => {
|
||||||
new DefaultMigrationHost(mockHost, mockMetadata, mockEvaluator, [handler], analyzedFiles);
|
const log: string[] = [];
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
expect(analyzedFiles.length).toEqual(1);
|
const analyzedFiles: AnalyzedFile[] = [];
|
||||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
const host = new DefaultMigrationHost(
|
||||||
expect(analyzedFiles[0].analyzedClasses[0].name).toEqual('MockClazz');
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
|
||||||
|
const sourceFile: any = {};
|
||||||
|
const unrelatedClass: any = {
|
||||||
|
getSourceFile: () => sourceFile,
|
||||||
|
};
|
||||||
|
analyzedFiles.push({sourceFile, analyzedClasses: [unrelatedClass]});
|
||||||
|
|
||||||
|
const decorators = host.getAllDecorators(mockClazz);
|
||||||
|
expect(decorators).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include injected decorators', () => {
|
||||||
|
const log: string[] = [];
|
||||||
|
const handler = new AlwaysDetectHandler('handler', log);
|
||||||
|
const existingDecorator = { name: 'ExistingDecorator' } as Decorator;
|
||||||
|
const analyzedClass: AnalyzedClass = {
|
||||||
|
name: 'MockClazz',
|
||||||
|
declaration: mockClazz,
|
||||||
|
matches: [],
|
||||||
|
decorators: [existingDecorator],
|
||||||
|
};
|
||||||
|
const analyzedFiles: AnalyzedFile[] = [{
|
||||||
|
sourceFile: mockClazz.getSourceFile(),
|
||||||
|
analyzedClasses: [analyzedClass],
|
||||||
|
}];
|
||||||
|
const host = new DefaultMigrationHost(
|
||||||
|
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles);
|
||||||
|
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
||||||
|
|
||||||
|
const decorators = host.getAllDecorators(mockClazz) !;
|
||||||
|
expect(decorators.length).toBe(2);
|
||||||
|
expect(decorators[0]).toBe(existingDecorator);
|
||||||
|
expect(decorators[1]).toBe(mockDecorator);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add a newly `AnalyzedClass` to an existing `AnalyzedFile` object', () => {
|
describe('isInScope', () => {
|
||||||
const DUMMY_CLASS_1: any = {};
|
it('should be true for nodes within the entry-point', () => {
|
||||||
const DUMMY_CLASS_2: any = {};
|
const analyzedFiles: AnalyzedFile[] = [];
|
||||||
const log: string[] = [];
|
const host = new DefaultMigrationHost(
|
||||||
const handler = new AlwaysDetectHandler('handler', log);
|
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles);
|
||||||
const analyzedFiles: AnalyzedFile[] = [{
|
|
||||||
sourceFile: mockClazz.getSourceFile(),
|
|
||||||
analyzedClasses: [DUMMY_CLASS_1, DUMMY_CLASS_2],
|
|
||||||
}];
|
|
||||||
const host =
|
|
||||||
new DefaultMigrationHost(mockHost, mockMetadata, mockEvaluator, [handler], analyzedFiles);
|
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
|
||||||
expect(analyzedFiles.length).toEqual(1);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(3);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses[2].name).toEqual('MockClazz');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a new decorator into an already existing `AnalyzedClass`', () => {
|
const sourceFile: any = {
|
||||||
const analyzedClass: AnalyzedClass = {
|
fileName: _('/node_modules/some-package/entry-point/relative.js'),
|
||||||
name: 'MockClazz',
|
};
|
||||||
declaration: mockClazz,
|
const clazz: any = {
|
||||||
matches: [],
|
getSourceFile: () => sourceFile,
|
||||||
decorators: null,
|
};
|
||||||
};
|
expect(host.isInScope(clazz)).toBe(true);
|
||||||
const log: string[] = [];
|
});
|
||||||
const handler = new AlwaysDetectHandler('handler', log);
|
|
||||||
const analyzedFiles: AnalyzedFile[] = [{
|
|
||||||
sourceFile: mockClazz.getSourceFile(),
|
|
||||||
analyzedClasses: [analyzedClass],
|
|
||||||
}];
|
|
||||||
const host =
|
|
||||||
new DefaultMigrationHost(mockHost, mockMetadata, mockEvaluator, [handler], analyzedFiles);
|
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
|
||||||
expect(analyzedFiles.length).toEqual(1);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
|
||||||
expect(analyzedClass.decorators !.length).toEqual(1);
|
|
||||||
expect(analyzedClass.decorators ![0].name).toEqual('MockDecorator');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should merge a new decorator into pre-existing decorators an already existing `AnalyzedClass`',
|
it('should be false for nodes outside the entry-point', () => {
|
||||||
() => {
|
const analyzedFiles: AnalyzedFile[] = [];
|
||||||
const analyzedClass: AnalyzedClass = {
|
const host = new DefaultMigrationHost(
|
||||||
name: 'MockClazz',
|
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles);
|
||||||
declaration: mockClazz,
|
|
||||||
matches: [],
|
|
||||||
decorators: [{name: 'OtherDecorator'} as Decorator],
|
|
||||||
};
|
|
||||||
const log: string[] = [];
|
|
||||||
const handler = new AlwaysDetectHandler('handler', log);
|
|
||||||
const analyzedFiles: AnalyzedFile[] = [{
|
|
||||||
sourceFile: mockClazz.getSourceFile(),
|
|
||||||
analyzedClasses: [analyzedClass],
|
|
||||||
}];
|
|
||||||
const host = new DefaultMigrationHost(
|
|
||||||
mockHost, mockMetadata, mockEvaluator, [handler], analyzedFiles);
|
|
||||||
host.injectSyntheticDecorator(mockClazz, mockDecorator);
|
|
||||||
expect(analyzedFiles.length).toEqual(1);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses.length).toEqual(1);
|
|
||||||
expect(analyzedFiles[0].analyzedClasses[0]).toBe(analyzedClass);
|
|
||||||
expect(analyzedClass.decorators !.length).toEqual(2);
|
|
||||||
expect(analyzedClass.decorators ![1].name).toEqual('MockDecorator');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if the injected decorator already exists', () => {
|
const sourceFile: any = {
|
||||||
const analyzedClass: AnalyzedClass = {
|
fileName: _('/node_modules/some-package/other-entry/index.js'),
|
||||||
name: 'MockClazz',
|
};
|
||||||
declaration: mockClazz,
|
const clazz: any = {
|
||||||
matches: [],
|
getSourceFile: () => sourceFile,
|
||||||
decorators: [{name: 'MockDecorator'} as Decorator],
|
};
|
||||||
};
|
expect(host.isInScope(clazz)).toBe(false);
|
||||||
const log: string[] = [];
|
});
|
||||||
const handler = new AlwaysDetectHandler('handler', log);
|
|
||||||
const analyzedFiles: AnalyzedFile[] = [{
|
|
||||||
sourceFile: mockClazz.getSourceFile(),
|
|
||||||
analyzedClasses: [analyzedClass],
|
|
||||||
}];
|
|
||||||
const host =
|
|
||||||
new DefaultMigrationHost(mockHost, mockMetadata, mockEvaluator, [handler], analyzedFiles);
|
|
||||||
expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator))
|
|
||||||
.toThrow(
|
|
||||||
jasmine.objectContaining({code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,592 @@
|
||||||
|
/**
|
||||||
|
* @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, program.getTypeChecker());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,3 +16,4 @@ export {InjectableDecoratorHandler} from './src/injectable';
|
||||||
export {NgModuleDecoratorHandler} from './src/ng_module';
|
export {NgModuleDecoratorHandler} from './src/ng_module';
|
||||||
export {PipeDecoratorHandler} from './src/pipe';
|
export {PipeDecoratorHandler} from './src/pipe';
|
||||||
export {NoopReferencesRegistry, ReferencesRegistry} from './src/references_registry';
|
export {NoopReferencesRegistry, ReferencesRegistry} from './src/references_registry';
|
||||||
|
export {forwardRefResolver} from './src/util';
|
||||||
|
|
Loading…
Reference in New Issue