angular-cn/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts

187 lines
7.8 KiB
TypeScript
Raw Normal View History

/**
* @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, 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';
import {NgccClassSymbol} from '../../src/host/ngcc_host';
describe('DefaultMigrationHost', () => {
describe('injectSyntheticDecorator()', () => {
const mockHost: any = {
fix(ngcc): consistently use outer declaration for classes (#32539) In ngcc's reflection hosts for compiled JS bundles, such as ESM2015, special care needs to be taken for classes as there may be an outer declaration (referred to as "declaration") and an inner declaration (referred to as "implementation") for a given class. Therefore, there will also be two `ts.Symbol`s bound per class, and ngcc needs to switch between those declarations and symbols depending on where certain information can be found. Prior to this commit, the `NgccReflectionHost` interface had methods `getClassSymbol` and `findClassSymbols` that would return a `ts.Symbol`. These class symbols would be used to kick off compilation of components using ngtsc, so it is important for these symbols to correspond with the publicly visible outer declaration of the class. However, the ESM2015 reflection host used to return the `ts.Symbol` for the inner declaration, if the class was declared as follows: ```javascript var MyClass = class MyClass {}; ``` For the above code, `Esm2015ReflectionHost.getClassSymbol` would return the `ts.Symbol` corresponding with the `class MyClass {}` declaration, whereas it should have corresponded with the `var MyClass` declaration. As a consequence, no `NgModule` could be resolved for the component, so no components/directives would be in scope for the component. This resulted in errors during runtime. This commit resolves the issue by introducing a `NgccClassSymbol` that contains references to both the outer and inner `ts.Symbol`, instead of just a single `ts.Symbol`. This avoids the unclarity of whether a `ts.Symbol` corresponds with the outer or inner declaration. More details can be found here: https://hackmd.io/7nkgWOFWQlSRAuIW_8KPPw Fixes #32078 Closes FW-1507 PR Close #32539
2019-09-03 15:26:58 -04:00
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 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: {}};
}
}