feat(ivy): ngcc - support ngcc "migrations" (#31544)

This commit implements support for the ngcc migrations
as designed in https://hackmd.io/KhyrFV1VQHmeQsgfJq6AyQ

PR Close #31544
This commit is contained in:
Pete Bacon Darwin 2019-07-18 21:05:32 +01:00 committed by Misko Hevery
parent d39a2beae1
commit 4d93d2406f
10 changed files with 504 additions and 73 deletions

View File

@ -14,6 +14,7 @@ ts_library(
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/metadata",

View File

@ -9,17 +9,21 @@ import {ConstantPool} from '@angular/compiler';
import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {FileSystem, LogicalFileSystem, absoluteFrom, dirname, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
import {ClassSymbol} from '../../../src/ngtsc/reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Migration, MigrationHost} from '../migrations/migration';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {isDefined} from '../utils';
import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses, MatchingHandler} from './types';
import {DefaultMigrationHost} from './migration_host';
import {AnalyzedClass, AnalyzedFile, CompiledClass, CompiledFile, DecorationAnalyses} from './types';
import {analyzeDecorators, isWithinPackage} from './util';
/**
* Simple class that resolves and loads files directly from the filesystem.
@ -89,10 +93,12 @@ export class DecorationAnalyzer {
this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
];
migrations: Migration[] = [];
constructor(
private fs: FileSystem, private bundle: EntryPointBundle,
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
private diagnosticHandler: (error: ts.Diagnostic) => void = () => {}) {}
/**
* Analyze a program to find all the decorated files should be transformed.
@ -101,12 +107,15 @@ export class DecorationAnalyzer {
*/
analyzeProgram(): DecorationAnalyses {
const decorationAnalyses = new DecorationAnalyses();
const analysedFiles = this.program.getSourceFiles()
const analyzedFiles = this.program.getSourceFiles()
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
.map(sourceFile => this.analyzeFile(sourceFile))
.filter(isDefined);
analysedFiles.forEach(analysedFile => this.resolveFile(analysedFile));
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
const migrationHost = new DefaultMigrationHost(
this.reflectionHost, this.fullMetaReader, this.evaluator, this.handlers, analyzedFiles);
analyzedFiles.forEach(analyzedFile => this.migrateFile(migrationHost, analyzedFile));
analyzedFiles.forEach(analyzedFile => this.resolveFile(analyzedFile));
const compiledFiles = analyzedFiles.map(analyzedFile => this.compileFile(analyzedFile));
compiledFiles.forEach(
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
return decorationAnalyses;
@ -120,61 +129,27 @@ export class DecorationAnalyzer {
}
protected analyzeClass(symbol: ClassSymbol): AnalyzedClass|null {
const declaration = symbol.valueDeclaration;
const decorators = this.reflectionHost.getDecoratorsOfSymbol(symbol);
const matchingHandlers = this.handlers
.map(handler => {
const detected = handler.detect(declaration, decorators);
return {handler, detected};
})
.filter(isMatchingHandler);
return analyzeDecorators(symbol, decorators, this.handlers);
}
if (matchingHandlers.length === 0) {
return null;
}
const detections: {handler: DecoratorHandler<any, any>, detected: DetectResult<any>}[] = [];
let hasWeakHandler: boolean = false;
let hasNonWeakHandler: boolean = false;
let hasPrimaryHandler: boolean = false;
for (const {handler, detected} of matchingHandlers) {
if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) {
continue;
} else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) {
// Clear all the WEAK handlers from the list of matches.
detections.length = 0;
}
if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) {
throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`);
}
detections.push({handler, detected});
if (handler.precedence === HandlerPrecedence.WEAK) {
hasWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.SHARED) {
hasNonWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.PRIMARY) {
hasNonWeakHandler = true;
hasPrimaryHandler = true;
}
}
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
const allDiagnostics: ts.Diagnostic[] = [];
for (const {handler, detected} of detections) {
const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata);
if (diagnostics !== undefined) {
allDiagnostics.push(...diagnostics);
}
matches.push({handler, analysis});
}
return {
name: symbol.name,
declaration,
decorators,
matches,
diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined
};
protected migrateFile(migrationHost: MigrationHost, analyzedFile: AnalyzedFile): void {
analyzedFile.analyzedClasses.forEach(({declaration}) => {
this.migrations.forEach(migration => {
try {
const result = migration.apply(declaration, migrationHost);
if (result !== null) {
this.diagnosticHandler(result);
}
} catch (e) {
if (isFatalDiagnosticError(e)) {
this.diagnosticHandler(e.toDiagnostic());
} else {
throw e;
}
}
});
});
}
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
@ -209,8 +184,3 @@ export class DecorationAnalyzer {
});
}
}
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
handler is MatchingHandler<A, M> {
return !!handler.detected;
}

View File

@ -0,0 +1,81 @@
/**
* @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 {ErrorCode, FatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {MetadataReader} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {DecoratorHandler} from '../../../src/ngtsc/transform';
import {NgccReflectionHost} from '../host/ngcc_host';
import {MigrationHost} from '../migrations/migration';
import {AnalyzedClass, AnalyzedFile} from './types';
import {analyzeDecorators} from './util';
/**
* The standard implementation of `MigrationHost`, which is created by the
* `DecorationAnalyzer`.
*/
export class DefaultMigrationHost implements MigrationHost {
constructor(
readonly reflectionHost: NgccReflectionHost, readonly metadata: MetadataReader,
readonly evaluator: PartialEvaluator, private handlers: DecoratorHandler<any, any>[],
private analyzedFiles: AnalyzedFile[]) {}
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void {
const classSymbol = this.reflectionHost.getClassSymbol(clazz) !;
const newAnalyzedClass = analyzeDecorators(classSymbol, [decorator], this.handlers);
if (newAnalyzedClass === null) {
return;
}
const analyzedFile = getOrCreateAnalyzedFile(this.analyzedFiles, clazz.getSourceFile());
const oldAnalyzedClass = analyzedFile.analyzedClasses.find(c => c.declaration === clazz);
if (oldAnalyzedClass === undefined) {
analyzedFile.analyzedClasses.push(newAnalyzedClass);
} else {
mergeAnalyzedClasses(oldAnalyzedClass, newAnalyzedClass);
}
}
}
function getOrCreateAnalyzedFile(
analyzedFiles: AnalyzedFile[], sourceFile: ts.SourceFile): AnalyzedFile {
const analyzedFile = analyzedFiles.find(file => file.sourceFile === sourceFile);
if (analyzedFile !== undefined) {
return analyzedFile;
} else {
const newAnalyzedFile: AnalyzedFile = {sourceFile, analyzedClasses: []};
analyzedFiles.push(newAnalyzedFile);
return newAnalyzedFile;
}
}
function mergeAnalyzedClasses(oldClass: AnalyzedClass, newClass: AnalyzedClass) {
if (newClass.decorators !== null) {
if (oldClass.decorators === null) {
oldClass.decorators = newClass.decorators;
} else {
for (const newDecorator of newClass.decorators) {
if (oldClass.decorators.some(d => d.name === newDecorator.name)) {
throw new FatalDiagnosticError(
ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR, newClass.declaration,
`Attempted to inject "${newDecorator.name}" decorator over a pre-existing decorator with the same name on the "${newClass.name}" class.`);
}
}
oldClass.decorators.push(...newClass.decorators);
}
}
if (newClass.diagnostics !== undefined) {
if (oldClass.diagnostics === undefined) {
oldClass.diagnostics = newClass.diagnostics;
} else {
oldClass.diagnostics.push(...newClass.diagnostics);
}
}
}

View File

@ -7,7 +7,74 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFromSourceFile, relative} from '../../../src/ngtsc/file_system';
import {ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
import {DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {AnalyzedClass, MatchingHandler} from './types';
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
return !relative(packagePath, absoluteFromSourceFile(sourceFile)).startsWith('..');
}
export function analyzeDecorators(
symbol: ClassSymbol, decorators: Decorator[] | null,
handlers: DecoratorHandler<any, any>[]): AnalyzedClass|null {
const declaration = symbol.valueDeclaration;
const matchingHandlers = handlers
.map(handler => {
const detected = handler.detect(declaration, decorators);
return {handler, detected};
})
.filter(isMatchingHandler);
if (matchingHandlers.length === 0) {
return null;
}
const detections: {handler: DecoratorHandler<any, any>, detected: DetectResult<any>}[] = [];
let hasWeakHandler: boolean = false;
let hasNonWeakHandler: boolean = false;
let hasPrimaryHandler: boolean = false;
for (const {handler, detected} of matchingHandlers) {
if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) {
continue;
} else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) {
// Clear all the WEAK handlers from the list of matches.
detections.length = 0;
}
if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) {
throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`);
}
detections.push({handler, detected});
if (handler.precedence === HandlerPrecedence.WEAK) {
hasWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.SHARED) {
hasNonWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.PRIMARY) {
hasNonWeakHandler = true;
hasPrimaryHandler = true;
}
}
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
const allDiagnostics: ts.Diagnostic[] = [];
for (const {handler, detected} of detections) {
const {analysis, diagnostics} = handler.analyze(declaration, detected.metadata);
if (diagnostics !== undefined) {
allDiagnostics.push(...diagnostics);
}
matches.push({handler, analysis});
}
return {
name: symbol.name,
declaration,
decorators,
matches,
diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined
};
}
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
handler is MatchingHandler<A, M> {
return !!handler.detected;
}

View File

@ -0,0 +1,18 @@
# ngcc migrations
There are some cases where source code needs to be migrated before ngtsc can compile it correctly.
For example, there are cases where ngtsc expects directives need to be explicitly attached to
classes, whereas previously they were not required.
There are two ways this can happen:
1) in a project being developed, the code can be migrated via a CLI schematic.
2) in a package already published to npm, the code can be migrated as part of the ngcc compilation.
To create one of these migrations for ngcc, you should implement the `Migration` interface and add
an instance of the class to the `DecorationAnalyzer.migrations` collection.
This folder is where we keep the `Migration` interface and the implemented migrations.
Each migration should have a unit test stored in the `../../test/migrations` directory.

View File

@ -0,0 +1,45 @@
/**
* @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 {MetadataReader} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
/**
* Implement this interface and add it to the `DecorationAnalyzer.migrations` collection to get ngcc
* to modify the analysis of the decorators in the program in order to migrate older code to work
* with Ivy.
*
* `Migration.apply()` is called for every class in the program being compiled by ngcc.
*
* Note that the underlying program could be in a variety of different formats, e.g. ES2015, ES5,
* UMD, CommonJS etc. This means that an author of a `Migration` should not attempt to navigate and
* manipulate the AST nodes directly. Instead, the `MigrationHost` interface, passed to the
* `Migration`, provides access to a `MetadataReader`, `ReflectionHost` and `PartialEvaluator`
* interfaces, which should be used.
*/
export interface Migration {
apply(clazz: ClassDeclaration, host: MigrationHost): ts.Diagnostic|null;
}
export interface MigrationHost {
/** Provides access to the decorator information associated with classes. */
readonly metadata: MetadataReader;
/** Provides access to navigate the AST in a format-agnostic manner. */
readonly reflectionHost: NgccReflectionHost;
/** Enables expressions to be statically evaluated in the context of the program. */
readonly evaluator: PartialEvaluator;
/**
* Associate a new synthesized decorator, which did not appear in the original source, with a
* given class.
* @param clazz the class to receive the new decorator.
* @param decorator the decorator to inject.
*/
injectSyntheticDecorator(clazz: ClassDeclaration, decorator: Decorator): void;
}

View File

@ -12,6 +12,7 @@ ts_library(
deps = [
"//packages/compiler-cli/ngcc",
"//packages/compiler-cli/ngcc/test/helpers",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",

View File

@ -7,15 +7,17 @@
*/
import * as ts from 'typescript';
import {FatalDiagnosticError, makeDiagnostic} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../../src/ngtsc/file_system';
import {TestFile, runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {Decorator} from '../../../src/ngtsc/reflection';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
import {DecoratorHandler, DetectResult} from '../../../src/ngtsc/transform';
import {loadFakeCore, loadTestFiles} from '../../../test/helpers';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {CompiledClass, DecorationAnalyses} from '../../src/analysis/types';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {Migration, MigrationHost} from '../../src/migrations/migration';
import {MockLogger} from '../helpers/mock_logger';
import {getRootFiles, makeTestEntryPointBundle} from '../helpers/utils';
@ -31,6 +33,8 @@ runInEachFileSystem(() => {
describe('analyzeProgram()', () => {
let logs: string[];
let migrationLogs: string[];
let diagnosticLogs: ts.Diagnostic[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
@ -87,7 +91,7 @@ runInEachFileSystem(() => {
return handler;
};
function setUpAndAnalyzeProgram(testFiles: TestFile[]) {
function setUpAnalyzer(testFiles: TestFile[]) {
logs = [];
loadTestFiles(testFiles);
loadFakeCore(getFileSystem());
@ -99,11 +103,17 @@ runInEachFileSystem(() => {
const reflectionHost =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer =
new DecorationAnalyzer(getFileSystem(), bundle, reflectionHost, referencesRegistry);
diagnosticLogs = [];
const analyzer = new DecorationAnalyzer(
getFileSystem(), bundle, reflectionHost, referencesRegistry,
(error) => diagnosticLogs.push(error));
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
migrationLogs = [];
const migration1 = new MockMigration('migration1', migrationLogs);
const migration2 = new MockMigration('migration2', migrationLogs);
analyzer.migrations = [migration1, migration2];
return analyzer;
}
describe('basic usage', () => {
@ -144,7 +154,8 @@ runInEachFileSystem(() => {
`,
},
];
setUpAndAnalyzeProgram(TEST_PROGRAM);
const analyzer = setUpAnalyzer(TEST_PROGRAM);
result = analyzer.analyzeProgram();
});
it('should return an object containing a reference to the original source file', () => {
@ -185,6 +196,18 @@ runInEachFileSystem(() => {
} as unknown as CompiledClass));
});
it('should call `apply()` on each migration for each class', () => {
expect(migrationLogs).toEqual([
'migration1:MyComponent',
'migration2:MyComponent',
'migration1:MyDirective',
'migration2:MyDirective',
'migration1:MyOtherComponent',
'migration2:MyOtherComponent',
]);
});
it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([
// Classes without decorators should also be detected.
@ -238,7 +261,8 @@ runInEachFileSystem(() => {
isRoot: false,
}
];
setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM);
const analyzer = setUpAnalyzer(INTERNAL_COMPONENT_PROGRAM);
result = analyzer.analyzeProgram();
});
// The problem of exposing the type of these internal components in the .d.ts typing
@ -299,7 +323,8 @@ runInEachFileSystem(() => {
},
];
setUpAndAnalyzeProgram(EXTERNAL_COMPONENT_PROGRAM);
const analyzer = setUpAnalyzer(EXTERNAL_COMPONENT_PROGRAM);
result = analyzer.analyzeProgram();
});
it('should ignore classes from an externally imported file', () => {
@ -307,6 +332,45 @@ runInEachFileSystem(() => {
expect(result.has(file)).toBe(false);
});
});
describe('diagnostic handling', () => {
it('should report migration diagnostics to the `diagnosticHandler` callback', () => {
const analyzer = setUpAnalyzer([
{
name: _('/node_modules/test-package/index.js'),
contents: `
import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {}
MyComponent.decorators = [{type: Component}];
`,
},
]);
analyzer.migrations = [
{
apply(clazz: ClassDeclaration) {
return makeDiagnostic(9999, clazz, 'normal diagnostic');
}
},
{
apply(clazz: ClassDeclaration) {
throw new FatalDiagnosticError(6666, clazz, 'fatal diagnostic');
}
}
];
analyzer.analyzeProgram();
expect(diagnosticLogs.length).toEqual(2);
expect(diagnosticLogs[0]).toEqual(jasmine.objectContaining({code: -999999}));
expect(diagnosticLogs[1]).toEqual(jasmine.objectContaining({code: -996666}));
});
});
});
});
});
});
class MockMigration implements Migration {
constructor(private name: string, private log: string[]) {}
apply(clazz: ClassDeclaration, host: MigrationHost): ts.Diagnostic|null {
this.log.push(`${this.name}:${clazz.name.text}`);
return null;
}
}

View File

@ -0,0 +1,179 @@
/**
* @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 {ErrorCode} from '../../../src/ngtsc/diagnostics';
import {ClassDeclaration, ClassSymbol, Decorator} from '../../../src/ngtsc/reflection';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {DefaultMigrationHost} from '../../src/analysis/migration_host';
import {AnalyzedClass, AnalyzedFile} from '../../src/analysis/types';
describe('DefaultMigrationHost', () => {
describe('injectSyntheticDecorator()', () => {
const mockHost: any = {
getClassSymbol: (node: any): ClassSymbol | undefined =>
({ valueDeclaration: node, name: node.name.text } as any),
};
const mockMetadata: any = {};
const mockEvaluator: any = {};
const mockClazz: any = {
name: {text: 'MockClazz'},
getSourceFile: () => { fileName: 'test-file.js'; },
};
const mockDecorator: any = {name: 'MockDecorator'};
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], []);
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], []);
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], 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], 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 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`',
() => {
const analyzedClass: AnalyzedClass = {
name: 'MockClazz',
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 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], analyzedFiles);
expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator))
.toThrow(
jasmine.objectContaining({code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR}));
});
});
});
class TestHandler implements DecoratorHandler<any, any> {
constructor(protected name: string, protected log: string[]) {}
precedence = HandlerPrecedence.PRIMARY;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<any>|undefined {
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`);
return undefined;
}
analyze(node: ClassDeclaration): AnalysisOutput<any> {
this.log.push(this.name + ':analyze:' + node.name.text);
return {};
}
compile(node: ClassDeclaration): CompileResult|CompileResult[] {
this.log.push(this.name + ':compile:' + node.name.text);
return [];
}
}
class AlwaysDetectHandler extends TestHandler {
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<any>|undefined {
super.detect(node, decorators);
return {trigger: node, metadata: {}};
}
}

View File

@ -56,6 +56,11 @@ export enum ErrorCode {
* otherwise imported.
*/
NGMODULE_INVALID_REEXPORT = 6004,
/**
* Raised when ngcc tries to inject a synthetic decorator over one that already exists.
*/
NGCC_MIGRATION_DECORATOR_INJECTION_ERROR = 7001,
}
export function ngErrorCode(code: ErrorCode): number {