352 lines
15 KiB
TypeScript
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')]};
|
||
|
}
|
||
|
}
|