/** * @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[] }) { 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 { constructor(readonly name: string, protected log: string[]) {} precedence = HandlerPrecedence.PRIMARY; detect(node: ClassDeclaration, decorators: Decorator[]|null): DetectResult|undefined { this.log.push(`${this.name}:detect:${node.name.text}:${decorators!.map(d => d.name)}`); return undefined; } analyze(node: ClassDeclaration): AnalysisOutput { 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|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|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 { super.analyze(node); return {diagnostics: [makeDiagnostic(9999, node, 'test diagnostic')]}; } }