diff --git a/packages/compiler-cli/src/ngcc/src/analyzer.ts b/packages/compiler-cli/src/ngcc/src/analyzer.ts index b28b8e325d..6cc6aae722 100644 --- a/packages/compiler-cli/src/ngcc/src/analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analyzer.ts @@ -12,9 +12,9 @@ import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; -import {NgccReflectionHost} from './host/ngcc_host'; import {DecoratedClass} from './host/decorated_class'; import {DecoratedFile} from './host/decorated_file'; +import {NgccReflectionHost} from './host/ngcc_host'; import {isDefined} from './utils'; export interface AnalyzedClass extends DecoratedClass { diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index a63e26bae4..f70a042e7f 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -7,9 +7,13 @@ */ import * as ts from 'typescript'; + import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host'; import {reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {getNameText} from '../utils'; +import {getNameText, getOriginalSymbol, isDefined} from '../utils'; + +import {DecoratedClass} from './decorated_class'; +import {DecoratedFile} from './decorated_file'; import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './fesm2015_host'; @@ -121,6 +125,42 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { return {node, body: statements || null, parameters}; } + /** + * Find all the files accessible via an entry-point, that contain decorated classes. + * @param entryPoint The starting point file for finding files that contain decorated classes. + * @returns A collection of files objects that hold info about the decorated classes and import + * information. + */ + findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[] { + const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); + const map = new Map(); + const getParsedClass = (declaration: ts.VariableDeclaration) => { + const decorators = this.getDecoratorsOfDeclaration(declaration); + if (decorators) { + return new DecoratedClass(getNameText(declaration.name), declaration, decorators); + } + }; + + if (moduleSymbol) { + const classDeclarations = this.checker.getExportsOfModule(moduleSymbol) + .map(getOriginalSymbol(this.checker)) + .map(exportSymbol => exportSymbol.valueDeclaration) + .filter(isDefined) + .filter(ts.isVariableDeclaration); + + const decoratedClasses = classDeclarations.map(getParsedClass).filter(isDefined); + + decoratedClasses.forEach(clazz => { + const file = clazz.declaration.getSourceFile(); + if (!map.has(file)) { + map.set(file, new DecoratedFile(file)); + } + map.get(file) !.decoratedClasses.push(clazz); + }); + } + return Array.from(map.values()); + } + ///////////// Protected Helpers ///////////// /** diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts index f772a29a14..ede69a267e 100644 --- a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts @@ -11,8 +11,10 @@ import * as ts from 'typescript'; import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host'; import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {findAll, getNameText, isDefined} from '../utils'; +import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils'; +import {DecoratedClass} from './decorated_class'; +import {DecoratedFile} from './decorated_file'; import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; export const DECORATORS = 'decorators' as ts.__String; @@ -294,6 +296,48 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); } + /* + * Find all the files accessible via an entry-point, that contain decorated classes. + * @param entryPoint The starting point file for finding files that contain decorated classes. + * @returns A collection of files objects that hold info about the decorated classes and import + * information. + */ + findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[] { + const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); + const map = new Map(); + if (moduleSymbol) { + const exportedSymbols = + this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker)); + const exportedDeclarations = + exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined); + + const decoratedClasses = + exportedDeclarations + .map(declaration => { + if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { + const name = declaration.name && ts.isIdentifier(declaration.name) ? + declaration.name.text : + undefined; + const decorators = this.getDecoratorsOfDeclaration(declaration); + return decorators && isDefined(name) ? + new DecoratedClass(name, declaration, decorators) : + undefined; + } + return undefined; + }) + .filter(isDefined); + + decoratedClasses.forEach(clazz => { + const file = clazz.declaration.getSourceFile(); + if (!map.has(file)) { + map.set(file, new DecoratedFile(file)); + } + map.get(file) !.decoratedClasses.push(clazz); + }); + } + return Array.from(map.values()); + } + ///////////// Protected Helpers ///////////// /** diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index efe7b09383..f0e250e07e 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -7,6 +7,7 @@ */ import * as ts from 'typescript'; import {ReflectionHost} from '../../../ngtsc/host'; +import {DecoratedFile} from './decorated_file'; export const PRE_NGCC_MARKER = '__PRE_NGCC__'; export const POST_NGCC_MARKER = '__POST_NGCC__'; @@ -37,4 +38,12 @@ export interface NgccReflectionHost extends ReflectionHost { * @returns An array of variable declarations that match. */ getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[]; + + /** + * Find all the files accessible via an entry-point, that contain decorated classes. + * @param entryPoint The starting point file for finding files that contain decorated classes. + * @returns A collection of files objects that hold info about the decorated classes and import + * information. + */ + findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[]; } diff --git a/packages/compiler-cli/src/ngcc/src/parsing/esm2015_parser.ts b/packages/compiler-cli/src/ngcc/src/parsing/esm2015_parser.ts index 499f8103a1..ad79d13d1c 100644 --- a/packages/compiler-cli/src/ngcc/src/parsing/esm2015_parser.ts +++ b/packages/compiler-cli/src/ngcc/src/parsing/esm2015_parser.ts @@ -8,9 +8,9 @@ import * as ts from 'typescript'; -import {NgccReflectionHost} from '../host/ngcc_host'; import {DecoratedClass} from '../host/decorated_class'; import {DecoratedFile} from '../host/decorated_file'; +import {NgccReflectionHost} from '../host/ngcc_host'; import {getOriginalSymbol, isDefined} from '../utils'; import {FileParser} from './file_parser'; diff --git a/packages/compiler-cli/src/ngcc/src/parsing/esm5_parser.ts b/packages/compiler-cli/src/ngcc/src/parsing/esm5_parser.ts index 599c93b14a..cf050b6504 100644 --- a/packages/compiler-cli/src/ngcc/src/parsing/esm5_parser.ts +++ b/packages/compiler-cli/src/ngcc/src/parsing/esm5_parser.ts @@ -8,9 +8,9 @@ import * as ts from 'typescript'; -import {NgccReflectionHost} from '../host/ngcc_host'; import {DecoratedClass} from '../host/decorated_class'; import {DecoratedFile} from '../host/decorated_file'; +import {NgccReflectionHost} from '../host/ngcc_host'; import {getNameText, getOriginalSymbol, isDefined} from '../utils'; import {FileParser} from './file_parser'; diff --git a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts index 35d054763d..486cba8ee9 100644 --- a/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analyzer_spec.ts @@ -6,12 +6,14 @@ * 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 {Fesm2015ReflectionHost} from '../src/host/fesm2015_host'; import {DecoratedClass} from '../src/host/decorated_class'; import {DecoratedFile} from '../src/host/decorated_file'; +import {Fesm2015ReflectionHost} from '../src/host/fesm2015_host'; + import {getDeclaration, makeProgram} from './helpers/utils'; const TEST_PROGRAM = { diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index c9e933b3b8..cdd8986296 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -50,6 +50,39 @@ const MARKER_FILE = { ` }; +const DECORATED_FILES = [ + { + name: '/primary.js', + contents: ` + import {Directive} from '@angular/core'; + class A {} + A.decorators = [ + { type: Directive, args: [{ selector: '[a]' }] } + ]; + function x() {} + function y() {} + class B {} + B.decorators = [ + { type: Directive, args: [{ selector: '[b]' }] } + ]; + class C {} + export { A, x, C }; + export { D } from '/secondary'; + ` + }, + { + name: '/secondary.js', + contents: ` + import {Directive} from '@angular/core'; + class D {} + D.decorators = [ + { type: Directive, args: [{ selector: '[d]' }] } + ]; + export { D }; + ` + } +]; + describe('Esm2015ReflectionHost', () => { describe('getGenericArityOfClass()', () => { it('should properly count type parameters', () => { @@ -84,4 +117,28 @@ describe('Esm2015ReflectionHost', () => { ]); }); }); + + describe('findDecoratedFiles()', () => { + it('should return an array of objects for each file that has exported and decorated classes', + () => { + const program = makeProgram(...DECORATED_FILES); + const dtsMapper = new DtsMapper('/src', '/typings'); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsMapper); + const decoratedFiles = + host.findDecoratedFiles(program.getSourceFile(DECORATED_FILES[0].name) !); + expect(decoratedFiles.length).toEqual(2); + const primary = decoratedFiles[0]; + expect(primary.decoratedClasses.length).toEqual(1); + const classA = primary.decoratedClasses.find(c => c.name === 'A') !; + expect(classA.name).toEqual('A'); + expect(ts.isClassDeclaration(classA.declaration)).toBeTruthy(); + expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + const secondary = decoratedFiles[1]; + expect(secondary.decoratedClasses.length).toEqual(1); + const classD = secondary.decoratedClasses.find(c => c.name === 'D') !; + expect(classD.name).toEqual('D'); + expect(ts.isClassDeclaration(classD.declaration)).toBeTruthy(); + expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + }); + }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 5c1340f423..5a83ef8f80 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -427,6 +427,51 @@ const FUNCTION_BODY_FILE = { ` }; +const DECORATED_FILES = [ + { + name: '/primary.js', + contents: ` + import {Directive} from '@angular/core'; + var A = (function() { + function A() {} + A.decorators = [ + { type: Directive, args: [{ selector: '[a]' }] } + ]; + return A; + }()); + var B = (function() { + function B() {} + B.decorators = [ + { type: Directive, args: [{ selector: '[b]' }] } + ]; + return B; + }()); + function x() {} + function y() {} + var C = (function() { + function C() {} + return C; + }); + export { A, x, C }; + export { D } from '/secondary'; + ` + }, + { + name: '/secondary.js', + contents: ` + import {Directive} from '@angular/core'; + var D = (function() { + function D() {} + D.decorators = [ + { type: Directive, args: [{ selector: '[d]' }] } + ]; + return D; + }()); + export { D }; + ` + } +]; + describe('Esm5ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1206,4 +1251,25 @@ describe('Esm5ReflectionHost', () => { expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); }); }); + + describe('fileDecoratedFiles()', () => { + it('should return an array of objects for each file that has exported and decorated classes', + () => { + const program = makeProgram(...DECORATED_FILES); + const host = new Esm5ReflectionHost(false, program.getTypeChecker()); + const decoratedFiles = + host.findDecoratedFiles(program.getSourceFile(DECORATED_FILES[0].name) !); + expect(decoratedFiles.length).toEqual(2); + const primary = decoratedFiles[0]; + expect(primary.decoratedClasses.length).toEqual(1); + const classA = primary.decoratedClasses[0]; + expect(classA.name).toEqual('A'); + expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + const secondary = decoratedFiles[1]; + expect(secondary.decoratedClasses.length).toEqual(1); + const classD = secondary.decoratedClasses[0]; + expect(classD.name).toEqual('D'); + expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + }); + }); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts index 2c7d35e3e6..ebf673c3ca 100644 --- a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts @@ -392,6 +392,37 @@ const MARKER_FILE = { ` }; +const DECORATED_FILE = { + name: '/primary.js', + contents: ` + import {Directive} from '@angular/core'; + class A {} + A.decorators = [ + { type: Directive, args: [{ selector: '[a]' }] } + ]; + + class B {} + B.decorators = [ + { type: Directive, args: [{ selector: '[b]' }] } + ]; + + function x() {} + + function y() {} + + class C {} + + let D = class D {} + D = tslib_1.__decorate([ + Directive({ selector: '[d]' }), + OtherD() + ], D); + export {D}; + + export { A, x, C }; + ` +}; + describe('Fesm2015ReflectionHost', () => { describe('getDecoratorsOfDeclaration()', () => { @@ -1152,4 +1183,29 @@ describe('Fesm2015ReflectionHost', () => { ]); }); }); + + describe('findDecoratedFiles()', () => { + it('should return an array of objects for each file that has exported and decorated classes', + () => { + const program = makeProgram(DECORATED_FILE); + const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const decoratedFiles = + host.findDecoratedFiles(program.getSourceFile(DECORATED_FILE.name) !); + expect(decoratedFiles.length).toEqual(1); + const decoratedClasses = decoratedFiles[0].decoratedClasses; + expect(decoratedClasses.length).toEqual(2); + + const decoratedClassA = decoratedClasses.find(c => c.name === 'A') !; + expect(decoratedClassA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + expect(decoratedClassA.decorators.map( + decorator => decorator.args && decorator.args.map(arg => arg.getText()))) + .toEqual([[`{ selector: '[a]' }`]]); + + const decoratedClassD = decoratedClasses.find(c => c.name === 'D') !; + expect(decoratedClassD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); + expect(decoratedClassD.decorators.map( + decorator => decorator.args && decorator.args.map(arg => arg.getText()))) + .toEqual([[`{ selector: '[d]' }`]]); + }); + }); });