angular/packages/compiler-cli/ngcc/test/analysis/migration_host_spec.ts
Alex Rickabaugh 252e3e9487 refactor(ivy): formalize the compilation process for matched handlers (#34288)
Prior to this commit, the `IvyCompilation` tracked the state of each matched
`DecoratorHandler` on each class in the `ts.Program`, and how they
progressed through the compilation process. This tracking was originally
simple, but had grown more complicated as the compiler evolved. The state of
each specific "target" of compilation was determined by the nullability of
a number of fields on the object which tracked it.

This commit formalizes the process of compilation of each matched handler
into a new "trait" concept. A trait is some aspect of a class which gets
created when a `DecoratorHandler` matches the class. It represents an Ivy
aspect that needs to go through the compilation process.

Traits begin in a "pending" state and undergo transitions as various steps
of compilation take place. The `IvyCompilation` class is renamed to the
`TraitCompiler`, which manages the state of all of the traits in the active
program.

Making the trait concept explicit will support future work to incrementalize
the expensive analysis process of compilation.

PR Close #34288
2019-12-12 13:11:45 -08:00

331 lines
14 KiB
TypeScript

/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ErrorCode, makeDiagnostic} 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 {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';
import {createComponentDecorator} from '../../src/migrations/utils';
runInEachFileSystem(() => {
describe('DefaultMigrationHost', () => {
let _: typeof absoluteFrom;
let entryPointPath: AbsoluteFsPath;
let mockHost: any;
let mockMetadata: any = {};
let mockEvaluator: any = {};
let mockClazz: any;
let mockDecorator: any = {name: 'MockDecorator'};
let diagnosticHandler = () => {};
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, [],
diagnosticHandler);
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, [], diagnosticHandler);
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,
diagnosticHandler);
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,
diagnosticHandler);
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], entryPointPath, analyzedFiles,
diagnosticHandler);
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], entryPointPath, analyzedFiles,
diagnosticHandler);
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,
diagnosticHandler);
expect(() => host.injectSyntheticDecorator(mockClazz, mockDecorator))
.toThrow(jasmine.objectContaining(
{code: ErrorCode.NGCC_MIGRATION_DECORATOR_INJECTION_ERROR}));
});
it('should report diagnostics from handlers', () => {
const log: string[] = [];
const handler = new DiagnosticProducingHandler('handler', log);
const analyzedFiles: AnalyzedFile[] = [];
const diagnostics: ts.Diagnostic[] = [];
const host = new DefaultMigrationHost(
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
diagnostic => diagnostics.push(diagnostic));
mockClazz.getStart = () => 0;
mockClazz.getWidth = () => 0;
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
host.injectSyntheticDecorator(mockClazz, decorator);
expect(diagnostics.length).toBe(1);
expect(ts.flattenDiagnosticMessageText(diagnostics[0].messageText, '\n'))
.toEqual(
`test diagnostic\n` +
` Occurs for @Component decorator inserted by an automatic migration\n` +
` @Component({ template: "", selector: "comp" })`);
});
});
describe('getAllDecorators', () => {
it('should be null for unknown source files', () => {
const log: string[] = [];
const handler = new AlwaysDetectHandler('handler', log);
const analyzedFiles: AnalyzedFile[] = [];
const host = new DefaultMigrationHost(
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
diagnosticHandler);
const decorators = host.getAllDecorators(mockClazz);
expect(decorators).toBeNull();
});
it('should be null for unknown classes', () => {
const log: string[] = [];
const handler = new AlwaysDetectHandler('handler', log);
const analyzedFiles: AnalyzedFile[] = [];
const host = new DefaultMigrationHost(
mockHost, mockMetadata, mockEvaluator, [handler], entryPointPath, analyzedFiles,
diagnosticHandler);
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,
diagnosticHandler);
host.injectSyntheticDecorator(mockClazz, mockDecorator);
const decorators = host.getAllDecorators(mockClazz) !;
expect(decorators.length).toBe(2);
expect(decorators[0]).toBe(existingDecorator);
expect(decorators[1]).toBe(mockDecorator);
});
});
describe('isInScope', () => {
it('should be true for nodes within the entry-point', () => {
const analyzedFiles: AnalyzedFile[] = [];
const host = new DefaultMigrationHost(
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles,
diagnosticHandler);
const sourceFile: any = {
fileName: _('/node_modules/some-package/entry-point/relative.js'),
};
const clazz: any = {
getSourceFile: () => sourceFile,
};
expect(host.isInScope(clazz)).toBe(true);
});
it('should be false for nodes outside the entry-point', () => {
const analyzedFiles: AnalyzedFile[] = [];
const host = new DefaultMigrationHost(
mockHost, mockMetadata, mockEvaluator, [], entryPointPath, analyzedFiles,
diagnosticHandler);
const sourceFile: any = {
fileName: _('/node_modules/some-package/other-entry/index.js'),
};
const clazz: any = {
getSourceFile: () => sourceFile,
};
expect(host.isInScope(clazz)).toBe(false);
});
});
});
});
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
constructor(protected name: string, protected log: string[]) {}
precedence = HandlerPrecedence.PRIMARY;
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
this.log.push(`${this.name}:detect:${node.name.text}:${decorators !.map(d => d.name)}`);
return undefined;
}
analyze(node: ClassDeclaration): AnalysisOutput<unknown> {
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<unknown>|undefined {
super.detect(node, decorators);
return {trigger: node, metadata: {}};
}
}
class DiagnosticProducingHandler extends AlwaysDetectHandler {
analyze(node: ClassDeclaration): AnalysisOutput<any> {
super.analyze(node);
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
}