angular-docs-cn/packages/compiler-cli/ngcc/test/analysis/ngcc_trait_compiler_spec.ts
JoostK 7659f2e24b fix(ngcc): do not attempt compilation when analysis fails (#34889)
In #34288, ngtsc was refactored to separate the result of the analysis
and resolve phase for more granular incremental rebuilds. In this model,
any errors in one phase transition the trait into an error state, which
prevents it from being ran through subsequent phases. The ngcc compiler
on the other hand did not adopt this strict error model, which would
cause incomplete metadata—due to errors in earlier phases—to be offered
for compilation that could result in a hard crash.

This commit updates ngcc to take advantage of ngtsc's `TraitCompiler`,
that internally manages all Ivy classes that are part of the
compilation. This effectively replaces ngcc's own `AnalyzedFile` and
`AnalyzedClass` types, together with all of the logic to drive the
`DecoratorHandler`s. All of this is now handled in the `TraitCompiler`,
benefiting from its explicit state transitions of `Trait`s so that the
ngcc crash is a thing of the past.

Fixes #34500
Resolves FW-1788

PR Close #34889
2020-01-23 14:47:03 -08:00

352 lines
15 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 {ErrorCode, makeDiagnostic, ngErrorCode} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {ClassDeclaration, Decorator, isNamedClassDeclaration} from '../../../src/ngtsc/reflection';
import {getDeclaration} from '../../../src/ngtsc/testing';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, TraitState} from '../../../src/ngtsc/transform';
import {loadTestFiles} from '../../../test/helpers';
import {NgccTraitCompiler} from '../../src/analysis/ngcc_trait_compiler';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {createComponentDecorator} from '../../src/migrations/utils';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {MockLogger} from '../helpers/mock_logger';
import {makeTestEntryPointBundle} from '../helpers/utils';
runInEachFileSystem(() => {
describe('NgccTraitCompiler', () => {
let _: typeof absoluteFrom;
let mockClazz: any;
let injectedDecorator: any = {name: 'InjectedDecorator'};
beforeEach(() => {
_ = absoluteFrom;
const mockSourceFile: any = {
fileName: _('/node_modules/some-package/entry-point/test-file.js'),
};
mockClazz = {
name: {text: 'MockClazz'},
getSourceFile: () => mockSourceFile,
getStart: () => 0,
getWidth: () => 0,
};
});
function createCompiler({entryPoint, handlers}: {
entryPoint: EntryPointBundle; handlers: DecoratorHandler<unknown, unknown, unknown>[]
}) {
const reflectionHost = new Esm2015ReflectionHost(new MockLogger(), false, entryPoint.src);
return new NgccTraitCompiler(handlers, reflectionHost);
}
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);
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler = createCompiler({entryPoint, handlers: [handler1, handler2]});
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
expect(log).toEqual([
`handler1:detect:MockClazz:InjectedDecorator`,
`handler2:detect:MockClazz:InjectedDecorator`,
]);
});
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);
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint = makeTestEntryPointBundle(
'test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler = createCompiler({entryPoint, handlers: [handler1, handler2, handler3]});
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
expect(log).toEqual([
`handler1:detect:MockClazz:InjectedDecorator`,
`handler2:detect:MockClazz:InjectedDecorator`,
`handler3:detect:MockClazz:InjectedDecorator`,
'handler2:analyze:MockClazz',
]);
});
it('should inject a new class record into the compilation', () => {
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler = createCompiler({entryPoint, handlers: [injectedHandler]});
compiler.injectSyntheticDecorator(mockClazz, injectedDecorator);
const record = compiler.recordFor(mockClazz);
expect(record).toBeDefined();
expect(record !.traits.length).toBe(1);
});
it('should add a new trait to an existing class record', () => {
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler =
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
const record = compiler.recordFor(myClass) !;
expect(record).toBeDefined();
expect(record.traits.length).toBe(2);
expect(record.traits[0].detected.decorator !.name).toBe('Directive');
expect(record.traits[1].detected.decorator !.name).toBe('InjectedDecorator');
});
it('should not add a weak handler when a primary handler already exists', () => {
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler =
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
const record = compiler.recordFor(myClass) !;
expect(record).toBeDefined();
expect(record.traits.length).toBe(1);
expect(record.traits[0].detected.decorator !.name).toBe('Directive');
});
it('should replace an existing weak handler when injecting a primary handler', () => {
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler =
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
const record = compiler.recordFor(myClass) !;
expect(record).toBeDefined();
expect(record.traits.length).toBe(1);
expect(record.traits[0].detected.decorator !.name).toBe('InjectedDecorator');
});
it('should produce an error when a primary handler is added when a primary handler is already present',
() => {
const directiveHandler =
new DetectDecoratorHandler('Directive', HandlerPrecedence.PRIMARY);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.PRIMARY);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint = makeTestEntryPointBundle(
'test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler =
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
const record = compiler.recordFor(myClass) !;
expect(record).toBeDefined();
expect(record.metaDiagnostics).toBeDefined();
expect(record.metaDiagnostics !.length).toBe(1);
expect(record.metaDiagnostics ![0].code)
.toBe(ngErrorCode(ErrorCode.DECORATOR_COLLISION));
expect(record.traits.length).toBe(0);
});
it('should report diagnostics from handlers', () => {
const log: string[] = [];
const handler = new DiagnosticProducingHandler('handler', log);
loadTestFiles([{name: _('/node_modules/test/index.js'), contents: ``}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler = createCompiler({entryPoint, handlers: [handler]});
const decorator = createComponentDecorator(mockClazz, {selector: 'comp', exportAs: null});
compiler.injectSyntheticDecorator(mockClazz, decorator);
const record = compiler.recordFor(mockClazz) !;
const migratedTrait = record.traits[0];
if (migratedTrait.state !== TraitState.ERRORED) {
return fail('Expected migrated class trait to be in an error state');
}
expect(migratedTrait.diagnostics.length).toBe(1);
expect(migratedTrait.diagnostics[0].messageText).toEqual(`test diagnostic`);
});
});
describe('getAllDecorators', () => {
it('should be null for classes without decorators', () => {
loadTestFiles(
[{name: _('/node_modules/test/index.js'), contents: `export class MyClass {};`}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler = createCompiler({entryPoint, handlers: []});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
const decorators = compiler.getAllDecorators(myClass);
expect(decorators).toBeNull();
});
it('should include injected decorators', () => {
const directiveHandler = new DetectDecoratorHandler('Directive', HandlerPrecedence.WEAK);
const injectedHandler =
new DetectDecoratorHandler('InjectedDecorator', HandlerPrecedence.WEAK);
loadTestFiles([{
name: _('/node_modules/test/index.js'),
contents: `
import {Directive} from '@angular/core';
export class MyClass {};
MyClass.decorators = [{ type: Directive }];
`
}]);
const entryPoint =
makeTestEntryPointBundle('test', 'esm2015', false, [_('/node_modules/test/index.js')]);
const compiler =
createCompiler({entryPoint, handlers: [directiveHandler, injectedHandler]});
const myClass = getDeclaration(
entryPoint.src.program, _('/node_modules/test/index.js'), 'MyClass',
isNamedClassDeclaration);
compiler.analyzeFile(entryPoint.src.file);
compiler.injectSyntheticDecorator(myClass, injectedDecorator);
const decorators = compiler.getAllDecorators(myClass) !;
expect(decorators.length).toBe(2);
expect(decorators[0].name).toBe('Directive');
expect(decorators[1].name).toBe('InjectedDecorator');
});
});
});
});
class TestHandler implements DecoratorHandler<unknown, unknown, unknown> {
constructor(readonly 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);
const decorator = decorators !== null ? decorators[0] : null;
return {trigger: node, decorator, metadata: {}};
}
}
class DetectDecoratorHandler extends TestHandler {
constructor(private decorator: string, readonly precedence: HandlerPrecedence) {
super(decorator, []);
}
detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult<unknown>|undefined {
super.detect(node, decorators);
if (decorators === null) {
return undefined;
}
const decorator = decorators.find(decorator => decorator.name === this.decorator);
if (decorator === undefined) {
return undefined;
}
return {trigger: node, decorator, metadata: {}};
}
}
class DiagnosticProducingHandler extends AlwaysDetectHandler {
analyze(node: ClassDeclaration): AnalysisOutput<any> {
super.analyze(node);
return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]};
}
}