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')]}; | ||
|  |   } | ||
|  | } |