diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts new file mode 100644 index 0000000000..4dca46ae67 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -0,0 +1,97 @@ +/** + * @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 fs from 'fs'; +import * as ts from 'typescript'; +import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; +import {Decorator} from '../../ngtsc/host'; +import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; +import {NgccReflectionHost} from './host/ngcc_host'; +import {ParsedClass} from './parsing/parsed_class'; +import {ParsedFile} from './parsing/parsed_file'; +import {isDefined} from './utils'; + +export interface AnalyzedClass extends ParsedClass { + handler: DecoratorHandler; + analysis: any; + diagnostics?: ts.Diagnostic[]; + compilation: CompileResult[]; +} + +export interface AnalyzedFile { + analyzedClasses: AnalyzedClass[]; + sourceFile: ts.SourceFile; +} + +export interface MatchingHandler { + handler: DecoratorHandler; + decorator: Decorator; +} + +/** + * `ResourceLoader` which directly uses the filesystem to resolve resources synchronously. + */ +export class FileResourceLoader implements ResourceLoader { + load(url: string): string { return fs.readFileSync(url, 'utf8'); } +} + +export class Analyzer { + resourceLoader = new FileResourceLoader(); + scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host); + handlers: DecoratorHandler[] = [ + new ComponentDecoratorHandler( + this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader), + new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), + new InjectableDecoratorHandler(this.host, false), + new NgModuleDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), + new PipeDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false), + ]; + + constructor(private typeChecker: ts.TypeChecker, private host: NgccReflectionHost) {} + + /** + * Analyize a parsed file to generate the information about decorated classes that + * should be converted to use ivy definitions. + * @param file The file to be analysed for decorated classes. + */ + analyzeFile(file: ParsedFile): AnalyzedFile { + const analyzedClasses = + file.decoratedClasses.map(clazz => this.analyzeClass(file.sourceFile, clazz)) + .filter(isDefined); + + return { + analyzedClasses, + sourceFile: file.sourceFile, + }; + } + + protected analyzeClass(file: ts.SourceFile, clazz: ParsedClass): AnalyzedClass|undefined { + const matchingHandlers = + this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)})) + .filter(isMatchingHandler); + + if (matchingHandlers.length > 1) { + throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.'); + } + + if (matchingHandlers.length == 0) { + return undefined; + } + + const {handler, decorator} = matchingHandlers[0]; + const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator); + let compilation = handler.compile(clazz.declaration, analysis); + if (!Array.isArray(compilation)) { + compilation = [compilation]; + } + return {...clazz, handler, analysis, diagnostics, compilation}; + } +} + +function isMatchingHandler(handler: Partial>): handler is MatchingHandler { + return !!handler.decorator; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngcc/test/BUILD.bazel b/packages/compiler-cli/src/ngcc/test/BUILD.bazel index c54936a989..5f534a5662 100644 --- a/packages/compiler-cli/src/ngcc/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/test/BUILD.bazel @@ -13,6 +13,7 @@ ts_library( "//packages/compiler-cli/src/ngcc", "//packages/compiler-cli/src/ngtsc/host", "//packages/compiler-cli/src/ngtsc/testing", + "//packages/compiler-cli/src/ngtsc/transform", ], ) diff --git a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts new file mode 100644 index 0000000000..45bb22a44d --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts @@ -0,0 +1,109 @@ +/** + * @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 {Decorator} from '../../ngtsc/host'; +import {DecoratorHandler} from '../../ngtsc/transform'; +import {AnalyzedFile, Analyzer} from '../src/analyzer'; +import {Esm2015ReflectionHost} from '../src/host/esm2015_host'; +import {ParsedClass} from '../src/parsing/parsed_class'; +import {ParsedFile} from '../src/parsing/parsed_file'; +import {getDeclaration, makeProgram} from './helpers/utils'; + +const TEST_PROGRAM = { + name: 'test.js', + contents: ` + import {Component, Injectable} from '@angular/core'; + + @Component() + export class MyComponent {} + + @Injectable() + export class MyService {} + ` +}; + +function createTestHandler() { + const handler = jasmine.createSpyObj>('TestDecoratorHandler', [ + 'detect', + 'analyze', + 'compile', + ]); + // Only detect the Component decorator + handler.detect.and.callFake( + (decorators: Decorator[]) => decorators.find(d => d.name === 'Component')); + // The "test" analysis is just the name of the decorator being analyzed + handler.analyze.and.callFake( + ((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null}))); + // The "test" compilation result is just the name of the decorator being compiled + handler.compile.and.callFake(((decl: ts.Declaration, analysis: any) => ({analysis}))); + return handler; +} + +function createParsedFile(program: ts.Program) { + const file = new ParsedFile(program.getSourceFile('test.js') !); + + const componentClass = getDeclaration(program, 'test.js', 'MyComponent', ts.isClassDeclaration); + file.decoratedClasses.push(new ParsedClass('MyComponent', {} as any, [{ + name: 'Component', + import: {from: '@angular/core', name: 'Component'}, + node: null as any, + args: null + }])); + + const serviceClass = getDeclaration(program, 'test.js', 'MyService', ts.isClassDeclaration); + file.decoratedClasses.push(new ParsedClass('MyService', {} as any, [{ + name: 'Injectable', + import: {from: '@angular/core', name: 'Injectable'}, + node: null as any, + args: null + }])); + + return file; +} + +describe('Analyzer', () => { + describe('analyzeFile()', () => { + let program: ts.Program; + let testHandler: jasmine.SpyObj>; + let result: AnalyzedFile; + + beforeEach(() => { + program = makeProgram(TEST_PROGRAM); + const file = createParsedFile(program); + const analyzer = new Analyzer( + program.getTypeChecker(), new Esm2015ReflectionHost(program.getTypeChecker())); + testHandler = createTestHandler(); + analyzer.handlers = [testHandler]; + result = analyzer.analyzeFile(file); + }); + + it('should return an object containing a reference to the original source file', + () => { expect(result.sourceFile).toBe(program.getSourceFile('test.js') !); }); + + it('should call detect on the decorator handlers with each class from the parsed file', () => { + expect(testHandler.detect).toHaveBeenCalledTimes(2); + expect(testHandler.detect.calls.allArgs()[0][0]).toEqual([jasmine.objectContaining( + {name: 'Component'})]); + expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining( + {name: 'Injectable'})]); + }); + + it('should return an object containing the classes that were analyzed', () => { + expect(result.analyzedClasses.length).toEqual(1); + expect(result.analyzedClasses[0].name).toEqual('MyComponent'); + }); + + it('should analyze and compile the classes that are detected', () => { + expect(testHandler.analyze).toHaveBeenCalledTimes(1); + expect(testHandler.analyze.calls.allArgs()[0][1].name).toEqual('Component'); + + expect(testHandler.compile).toHaveBeenCalledTimes(1); + expect(testHandler.compile.calls.allArgs()[0][1]).toEqual('Component'); + }); + }); +});