diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts new file mode 100644 index 0000000000..a2dff5d7df --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -0,0 +1,425 @@ +/** + * @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 {ClassMember, ClassMemberKind, Decorator, Parameter} from '../../../ngtsc/host'; +import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; +import {getNameText} from '../utils'; +import {NgccReflectionHost} from './ngcc_host'; + +export const DECORATORS = 'decorators' as ts.__String; +export const PROP_DECORATORS = 'propDecorators' as ts.__String; +export const CONSTRUCTOR = '__constructor' as ts.__String; +export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String; + +/** + * Esm2015 packages contain ECMAScript 2015 classes, etc. + * Decorators are defined via static properties on the class. For example: + * + * ``` + * class SomeDirective { + * } + * SomeDirective.decorators = [ + * { type: Directive, args: [{ selector: '[someDirective]' },] } + * ]; + * SomeDirective.ctorParameters = () => [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; + * SomeDirective.propDecorators = { + * "input1": [{ type: Input },], + * "input2": [{ type: Input },], + * }; + * ``` + * + * * Classes are decorated if they have a static property called `decorators`. + * * Members are decorated if there is a matching key on a static property + * called `propDecorators`. + * * Constructor parameters decorators are found on an object returned from + * a static method called `ctorParameters`. + */ +export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { + constructor(checker: ts.TypeChecker) { super(checker); } + + /** + * Examine a declaration (for example, of a class or function) and return metadata about any + * decorators present on the declaration. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class or function over + * which to reflect. For example, if the intent is to reflect the decorators of a class and the + * source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5 + * format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the + * result of an IIFE execution. + * + * @returns an array of `Decorator` metadata if decorators are present on the declaration, or + * `null` if either no decorators were present or if the declaration is not of a decoratable type. + */ + getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null { + const symbol = this.getClassSymbol(declaration); + if (symbol) { + if (symbol.exports && symbol.exports.has(DECORATORS)) { + // Symbol of the identifier for `SomeDirective.decorators`. + const decoratorsSymbol = symbol.exports.get(DECORATORS) !; + const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; + + if (decoratorsIdentifier && decoratorsIdentifier.parent) { + if (ts.isBinaryExpression(decoratorsIdentifier.parent)) { + // AST of the array of decorator values + const decoratorsArray = decoratorsIdentifier.parent.right; + return this.reflectDecorators(decoratorsArray); + } + } + } + } + return null; + } + + /** + * Examine a declaration which should be of a class, and return metadata about the members of the + * class. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `ClassMember` metadata representing the members of the class. + * + * @throws if `declaration` does not resolve to a class declaration. + */ + getMembersOfClass(clazz: ts.Declaration): ClassMember[] { + const members: ClassMember[] = []; + const symbol = this.getClassSymbol(clazz); + if (!symbol) { + throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`); + } + + // The decorators map contains all the properties that are decorated + const decoratorsMap = this.getMemberDecorators(symbol); + + // The member map contains all the method (instance and static); and any instance properties + // that are initialized in the class. + if (symbol.members) { + symbol.members.forEach((value, key) => { + const decorators = removeFromMap(decoratorsMap, key); + const member = this.reflectMember(value, decorators); + if (member) { + members.push(member); + } + }); + } + + // The static property map contains all the static properties + if (symbol.exports) { + symbol.exports.forEach((value, key) => { + const decorators = removeFromMap(decoratorsMap, key); + const member = this.reflectMember(value, decorators, true); + if (member) { + members.push(member); + } + }); + } + + // Deal with any decorated properties that were not initialized in the class + decoratorsMap.forEach((value, key) => { + members.push({ + implementation: null, + decorators: value, + isStatic: false, + kind: ClassMemberKind.Property, + name: key, + nameNode: null, + node: null, + type: null, + value: null + }); + }); + + return members; + } + + /** + * Reflect over the constructor of a class and return metadata about its parameters. + * + * This method only looks at the constructor of a class directly and not at any inherited + * constructors. + * + * @param declaration a TypeScript `ts.Declaration` node representing the class over which to + * reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the + * source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are + * represented as the result of an IIFE execution. + * + * @returns an array of `Parameter` metadata representing the parameters of the constructor, if + * a constructor exists. If the constructor exists and has 0 parameters, this array will be empty. + * If the class has no constructor, this method returns `null`. + * + * @throws if `declaration` does not resolve to a class declaration. + */ + getConstructorParameters(clazz: ts.Declaration): Parameter[]|null { + const classSymbol = this.getClassSymbol(clazz); + if (!classSymbol) { + throw new Error( + `Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`); + } + const parameterNodes = this.getConstructorParameterDeclarations(classSymbol); + if (parameterNodes) { + const parameters: Parameter[] = []; + const decoratorInfo = this.getConstructorDecorators(classSymbol); + parameterNodes.forEach((node, index) => { + const info = decoratorInfo[index]; + const decorators = + info && info.has('decorators') && this.reflectDecorators(info.get('decorators') !) || + null; + const type = info && info.get('type') || null; + const nameNode = node.name; + parameters.push({name: getNameText(nameNode), nameNode, type, decorators}); + }); + return parameters; + } + return null; + } + + /** + * Find a symbol for a declaration that we think is a class. + * @param declaration The declaration whose symbol we are finding + * @returns the symbol for the declaration or `undefined` if it is not + * a "class" or has no symbol. + */ + getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined { + return ts.isClassDeclaration(declaration) ? + declaration.name && this.checker.getSymbolAtLocation(declaration.name) : + undefined; + } + + /** + * Member decorators are declared as static properties of the class in ES2015: + * + * ``` + * SomeDirective.propDecorators = { + * "ngForOf": [{ type: Input },], + * "ngForTrackBy": [{ type: Input },], + * "ngForTemplate": [{ type: Input },], + * }; + * ``` + */ + protected getMemberDecorators(classSymbol: ts.Symbol): Map { + const memberDecorators = new Map(); + if (classSymbol.exports && classSymbol.exports.has(PROP_DECORATORS)) { + // Symbol of the identifier for `SomeDirective.propDecorators`. + const propDecoratorsMap = + getPropertyValueFromSymbol(classSymbol.exports.get(PROP_DECORATORS) !); + if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { + const propertiesMap = reflectObjectLiteral(propDecoratorsMap); + propertiesMap.forEach( + (value, name) => { memberDecorators.set(name, this.reflectDecorators(value)); }); + } + } + return memberDecorators; + } + + /** + * Reflect over the given expression and extract decorator information. + * @param decoratorsArray An expression that contains decorator information. + */ + protected reflectDecorators(decoratorsArray: ts.Expression): Decorator[] { + const decorators: Decorator[] = []; + + if (ts.isArrayLiteralExpression(decoratorsArray)) { + // Add each decorator that is imported from `@angular/core` into the `decorators` array + decoratorsArray.elements.forEach(node => { + + // If the decorator is not an object literal expression then we are not interested + if (ts.isObjectLiteralExpression(node)) { + // We are only interested in objects of the form: `{ type: DecoratorType, args: [...] }` + const decorator = reflectObjectLiteral(node); + + // Is the value of the `type` property an identifier? + const typeIdentifier = decorator.get('type'); + if (typeIdentifier && ts.isIdentifier(typeIdentifier)) { + decorators.push({ + name: typeIdentifier.text, + import: this.getImportOfIdentifier(typeIdentifier), node, + args: getDecoratorArgs(node), + }); + } + } + }); + } + return decorators; + } + + protected reflectMember(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean): + ClassMember|null { + let kind: ClassMemberKind|null = null; + let value: ts.Expression|null = null; + let name: string|null = null; + let nameNode: ts.Identifier|null = null; + let type = null; + + + const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0]; + if (!node || !isClassMemberType(node)) { + return null; + } + + if (symbol.flags & ts.SymbolFlags.Method) { + kind = ClassMemberKind.Method; + } else if (symbol.flags & ts.SymbolFlags.Property) { + kind = ClassMemberKind.Property; + } else if (symbol.flags & ts.SymbolFlags.GetAccessor) { + kind = ClassMemberKind.Getter; + } else if (symbol.flags & ts.SymbolFlags.SetAccessor) { + kind = ClassMemberKind.Setter; + } + + if (isStatic && isPropertyAccess(node)) { + name = node.name.text; + value = symbol.flags & ts.SymbolFlags.Property ? node.parent.right : null; + } else if (isThisAssignment(node)) { + kind = ClassMemberKind.Property; + name = node.left.name.text; + value = node.right; + isStatic = false; + } else if (ts.isConstructorDeclaration(node)) { + kind = ClassMemberKind.Constructor; + name = 'constructor'; + isStatic = false; + } + + if (kind === null) { + console.warn(`Unknown member type: "${node.getText()}`); + return null; + } + + if (!name) { + if (isNamedDeclaration(node) && node.name && ts.isIdentifier(node.name)) { + name = node.name.text; + nameNode = node.name; + } else { + return null; + } + } + + // If we have still not determined if this is a static or instance member then + // look for the `static` keyword on the declaration + if (isStatic === undefined) { + isStatic = node.modifiers !== undefined && + node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword); + } + + return { + node, + implementation: node, kind, type, name, nameNode, value, isStatic, + decorators: decorators || [] + }; + } + + /** + * Find the declarations of the constructor parameters of a class identified by its symbol. + * @param classSymbol the class whose parameters we want to find. + * @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in + * the + * class's constructor or null if there is no constructor. + */ + protected getConstructorParameterDeclarations(classSymbol: ts.Symbol): + ts.ParameterDeclaration[]|null { + const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR); + if (constructorSymbol) { + // For some reason the constructor does not have a `valueDeclaration` ?!? + const constructor = constructorSymbol.declarations && + constructorSymbol.declarations[0] as ts.ConstructorDeclaration; + if (constructor && constructor.parameters) { + return Array.from(constructor.parameters); + } + return []; + } + return null; + } + + /** + * Constructors parameter decorators are declared in the body of static method of the class in + * ES2015: + * + * ``` + * SomeDirective.ctorParameters = () => [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: IterableDiffers, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; + * ``` + */ + protected getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { + if (classSymbol.exports && classSymbol.exports.has(CONSTRUCTOR_PARAMS)) { + const paramDecoratorsProperty = + getPropertyValueFromSymbol(classSymbol.exports.get(CONSTRUCTOR_PARAMS) !); + if (paramDecoratorsProperty && ts.isArrowFunction(paramDecoratorsProperty)) { + if (ts.isArrayLiteralExpression(paramDecoratorsProperty.body)) { + return paramDecoratorsProperty.body.elements.map( + element => + ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null); + } + } + } + return []; + } +} + +/** + * The arguments of a decorator are held in the `args` property of its declaration object. + */ +function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { + const argsProperty = node.properties.filter(ts.isPropertyAssignment) + .find(property => getNameText(property.name) === 'args'); + const argsExpression = argsProperty && argsProperty.initializer; + return argsExpression && ts.isArrayLiteralExpression(argsExpression) ? + Array.from(argsExpression.elements) : + []; +} + +/** + * Helper method to extract the value of a property given the property's "symbol", + * which is actually the symbol of the identifier of the property. + */ +export function getPropertyValueFromSymbol(propSymbol: ts.Symbol): ts.Expression|undefined { + const propIdentifier = propSymbol.valueDeclaration; + const parent = propIdentifier && propIdentifier.parent; + return parent && ts.isBinaryExpression(parent) ? parent.right : undefined; +} + +function removeFromMap(map: Map, key: ts.__String): T|undefined { + const mapKey = key as string; + const value = map.get(mapKey); + if (value !== undefined) { + map.delete(mapKey); + } + return value; +} + +function isPropertyAccess(node: ts.Node): node is ts.PropertyAccessExpression& + {parent: ts.BinaryExpression} { + return !!node.parent && ts.isBinaryExpression(node.parent) && ts.isPropertyAccessExpression(node); +} + +function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression& + {left: ts.PropertyAccessExpression} { + return ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) && + node.left.expression.kind === ts.SyntaxKind.ThisKeyword; +} + +function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration { + return !!(node as any).name; +} + + +function isClassMemberType(node: ts.Declaration): node is ts.ClassElement| + ts.PropertyAccessExpression|ts.BinaryExpression { + return ts.isClassElement(node) || isPropertyAccess(node) || ts.isBinaryExpression(node); +} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts new file mode 100644 index 0000000000..a84bc471a7 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -0,0 +1,138 @@ +/** + * @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 {ClassMember, ClassMemberKind} from '../../../ngtsc/host/src/reflection'; +import {reflectObjectLiteral} from '../../../ngtsc/metadata/src/reflector'; +import {CONSTRUCTOR_PARAMS, Esm2015ReflectionHost, getPropertyValueFromSymbol} from './esm2015_host'; + +/** + * ESM5 packages contain ECMAScript IIFE functions that act like classes. For example: + * + * ``` + * var CommonModule = (function () { + * function CommonModule() { + * } + * CommonModule.decorators = [ ... ]; + * ``` + * + * * "Classes" are decorated if they have a static property called `decorators`. + * * Members are decorated if there is a matching key on a static property + * called `propDecorators`. + * * Constructor parameters decorators are found on an object returned from + * a static method called `ctorParameters`. + * + */ +export class Esm5ReflectionHost extends Esm2015ReflectionHost { + constructor(checker: ts.TypeChecker) { super(checker); } + + /** + * Check whether the given declaration node actually represents a class. + */ + isClass(node: ts.Declaration): boolean { return !!this.getClassSymbol(node); } + + /** + * In ESM5 the implementation of a class is a function expression that is hidden inside an IIFE. + * So we need to dig around inside to get hold of the "class" symbol. + * @param declaration the top level declaration that represents an exported class. + */ + getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined { + if (ts.isVariableDeclaration(declaration)) { + const iifeBody = getIifeBody(declaration); + if (iifeBody) { + const innerClassIdentifier = getReturnIdentifier(iifeBody); + if (innerClassIdentifier) { + return this.checker.getSymbolAtLocation(innerClassIdentifier); + } + } + } + return undefined; + } + + /** + * Find the declarations of the constructor parameters of a class identified by its symbol. + * In ESM5 there is no "class" so the constructor that we want is actually the declaration + * function itself. + */ + protected getConstructorParameterDeclarations(classSymbol: ts.Symbol): ts.ParameterDeclaration[] { + const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration; + if (constructor && constructor.parameters) { + return Array.from(constructor.parameters); + } + return []; + } + + /** + * Constructors parameter decorators are declared in the body of static method of the constructor + * function in ES5. Note that unlike ESM2105 this is a function expression rather than an arrow + * function: + * + * ``` + * SomeDirective.ctorParameters = function() { return [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: IterableDiffers, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; }; + * ``` + */ + protected getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { + const declaration = classSymbol.exports && classSymbol.exports.get(CONSTRUCTOR_PARAMS); + const paramDecoratorsProperty = declaration && getPropertyValueFromSymbol(declaration); + const returnStatement = getReturnStatement(paramDecoratorsProperty); + const expression = returnStatement && returnStatement.expression; + return expression && ts.isArrayLiteralExpression(expression) ? + expression.elements.map(reflectArrayElement) : + []; + } + + protected reflectMember(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean): + ClassMember|null { + const member = super.reflectMember(symbol, decorators, isStatic); + if (member && member.kind === ClassMemberKind.Method && member.isStatic && member.node && + ts.isPropertyAccessExpression(member.node) && member.node.parent && + ts.isBinaryExpression(member.node.parent) && + ts.isFunctionExpression(member.node.parent.right)) { + // Recompute the implementation for this member: + // ES5 static methods are variable declarations so the declaration is actually the + // initializer of the variable assignment + member.implementation = member.node.parent.right; + } + return member; + } +} + +function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined { + if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) { + return undefined; + } + const call = declaration.initializer; + return ts.isCallExpression(call.expression) && + ts.isFunctionExpression(call.expression.expression) ? + call.expression.expression.body : + undefined; +} + +function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined { + const returnStatement = body.statements.find(ts.isReturnStatement); + return returnStatement && returnStatement.expression && + ts.isIdentifier(returnStatement.expression) ? + returnStatement.expression : + undefined; +} + +function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnStatement|undefined { + return declaration && ts.isFunctionExpression(declaration) ? + declaration.body.statements.find(ts.isReturnStatement) : + undefined; +} + +function reflectArrayElement(element: ts.Expression) { + return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null; +} \ No newline at end of file diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts new file mode 100644 index 0000000000..ef32c719ed --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -0,0 +1,16 @@ +/** + * @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 {ReflectionHost} from '../../../ngtsc/host'; + +/** + * A reflection host that has extra methods for looking at non-Typescript package formats + */ +export interface NgccReflectionHost extends ReflectionHost { + getClassSymbol(declaration: ts.Declaration): ts.Symbol|undefined; +} diff --git a/packages/compiler-cli/src/ngcc/src/utils.ts b/packages/compiler-cli/src/ngcc/src/utils.ts new file mode 100644 index 0000000000..425e6c9da7 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/src/utils.ts @@ -0,0 +1,22 @@ +/** + * @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'; + +export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) => ts.Symbol { + return function(symbol: ts.Symbol) { + return ts.SymbolFlags.Alias & symbol.flags ? checker.getAliasedSymbol(symbol) : symbol; + }; +} + +export function isDefined(value: T | undefined | null): value is T { + return !!value; +} + +export function getNameText(name: ts.PropertyName | ts.BindingName): string { + return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText(); +} \ No newline at end of file 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 new file mode 100644 index 0000000000..b30866ade3 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -0,0 +1,1020 @@ +/** + * @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 {ClassMemberKind, Import} from '../../../ngtsc/host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; +import {getDeclaration, makeProgram} from '../helpers/utils'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.js', + contents: ` + import { Directive, Inject, InjectionToken, Input, HostListener, HostBinding } from '@angular/core'; + + const INJECTED_TOKEN = new InjectionToken('injected'); + const ViewContainerRef = {}; + const TemplateRef = {}; + + class SomeDirective { + constructor(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + instanceMethod() {} + + onClick() {} + + @HostBinding('class.foo') + get isClassFoo() { return false; } + + static staticMethod() {} + } + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = () => [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + ]; + SomeDirective.propDecorators = { + "input1": [{ type: Input },], + "input2": [{ type: Input },], + "target": [{ type: HostBinding, args: ['attr.target',] }, { type: Input },], + "onClick": [{ type: HostListener, args: ['click',] },], + }; + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` + class EmptyClass {} + class NoDecoratorConstructorClass { + constructor(foo) {} + } + `, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` + import { Directive } from '@angular/core'; + + function foo() {} + foo.decorators = [ + { type: Directive, args: [{ selector: '[ignored]' },] } + ]; + `, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.decorators = () => [ + { type: NotArrayLiteralDecorator, args: [{ selector: '[ignored]' },] }, + ]; + + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: NotObjectLiteralDecorator }, + ]; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + } + NoTypeProperty.decorators = [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ]; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ]; + `, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + } + NoArgsProperty.decorators = [ + { type: NoArgsPropertyDecorator }, + ]; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.decorators = [ + { type: NoPropertyAssignmentDecorator, args }, + ]; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.decorators = [ + { type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] }, + ]; + `, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: NotObjectLiteralDecorator },] + }); + + const NotObjectLiteralPropDecorator = {}; + class NotObjectLiteralProp { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: NotObjectLiteralPropDecorator }, + ] + }; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }; + `, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: NoArgsPropertyDecorator },] + }; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: NoPropertyAssignmentDecorator, args },] + }; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },], + }; + `, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + const NoParametersDecorator = {}; + class NoParameters { + constructor() { + } + } + + const NotArrowFunctionDecorator = {}; + class NotArrowFunction { + constructor(arg1) { + } + } + NotArrowFunction.ctorParameters = function() { + return { type: 'ParamType', decorators: [{ type: NotArrowFunctionDecorator },] }; + }; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => 'StringsAreNotArrayLiterals'; + + const NotObjectLiteralDecorator = {}; + class NotObjectLiteral { + constructor(arg1, arg2) { + } + } + NotObjectLiteral.ctorParameters = () => [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: NotObjectLiteralDecorator },] }, + ]; + + const NoTypePropertyDecorator1 = {}; + const NoTypePropertyDecorator2 = {}; + class NoTypeProperty { + constructor(arg1, arg2) { + } + } + NoTypeProperty.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }, + ]; + + const NotIdentifierDecorator = {}; + class NotIdentifier { + constructor(arg1, arg2) { + } + } + NotIdentifier.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }, + ]; + `, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + const NoArgsPropertyDecorator = {}; + class NoArgsProperty { + constructor(arg1) { + } + } + NoArgsProperty.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NoArgsPropertyDecorator },] }, + ]; + + const NoPropertyAssignmentDecorator = {}; + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + constructor(arg1) { + } + } + NoPropertyAssignment.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NoPropertyAssignmentDecorator, args },] }, + ]; + + const NotArrayLiteralDecorator = {}; + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },] }, + ]; + `, +}; + +const IMPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {a} from './a.js'; + import {a as foo} from './a.js'; + + const b = a; + const c = foo; + const d = b; + `, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {Directive} from '@angular/core'; + import {a} from './a'; + import {a as foo} from './a'; + export {Directive} from '@angular/core'; + export {a} from './a'; + export const b = a; + export const c = foo; + export const d = b; + export const e = 'e'; + export const DirectiveX = Directive; + export class SomeClass {} + `, + }, +]; + +describe('Esm2015ReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + + it('should return null if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotObjectLiteralDecorator'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + debugger; + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({ + name: 'NotObjectLiteralPropDecorator' + })); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + let callCount = 0; + const spy = + spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { + callCount++; + return {name: `name${callCount}`, from: `from${callCount}`}; + }); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(spy).toHaveBeenCalled(); + expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([ + 'Input', + 'Input', + 'HostBinding', + 'Input', + 'HostListener', + ]); + + const index = members.findIndex(member => member.name === 'input1'); + expect(members[index].decorators !.length).toBe(1); + expect(members[index].decorators ![0].import).toEqual({name: 'name1', from: 'from1'}); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isClassDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ + 'ViewContainerRef', 'TemplateRef', 'undefined' + ]); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + it('should return `null` if there is no constructor', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters).toBe(null); + }); + + it('should return an array even if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + it('should ignore `ctorParameters` if it is not an arrow function', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrowFunction', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getImportOfIdentifier', () => { + it('should find the import of an identifier', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'b', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should find the name by which the identifier was exported, not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'c', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should return null if the identifier was not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'd', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the declaration of an externally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, 'node_modules/@angular/core/index.ts', 'Directive', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const program = makeProgram(...EXPORTS_FILES); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const file = program.getSourceFile(EXPORTS_FILES[1].name) !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.keys())).toEqual([ + 'Directive', + 'a', + 'b', + 'c', + 'd', + 'e', + 'DirectiveX', + 'SomeClass', + ]); + + const values = Array.from(exportDeclarations !.values()) + .map(declaration => [declaration.node.getText(), declaration.viaModule]); + expect(values).toEqual([ + // TODO clarify what is expected here... + // [`Directive = callableClassDecorator()`, '@angular/core'], + [`Directive = callableClassDecorator()`, null], + [`a = 'a'`, null], + [`b = a`, null], + [`c = foo`, null], + [`d = b`, null], + [`e = 'e'`, null], + [`DirectiveX = Directive`, null], + ['export class SomeClass {}', null], + ]); + }); + }); + + describe('isClass()', () => { + it('should return true if a given node is a TS class declaration', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); + expect(host.isClass(node)).toBe(true); + }); + + it('should return false if a given node is a TS function declaration', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm2015ReflectionHost(program.getTypeChecker()); + const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(host.isClass(node)).toBe(false); + }); + }); +}); 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 new file mode 100644 index 0000000000..92b9ab0719 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -0,0 +1,1058 @@ +/** + * @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 {ClassMemberKind, Import} from '../../../ngtsc/host'; +import {Esm5ReflectionHost} from '../../src/host/esm5_host'; +import {getDeclaration, makeProgram} from '../helpers/utils'; + +const SOME_DIRECTIVE_FILE = { + name: '/some_directive.js', + contents: ` + import { Directive, Inject, InjectionToken, Input } from '@angular/core'; + + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = {}; + var TemplateRef = {}; + + var SomeDirective = (function() { + function SomeDirective(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + } + SomeDirective.prototype = { + instanceMethod: function() {}, + }; + SomeDirective.staticMethod = function() {}; + SomeDirective.staticProperty = 'static'; + SomeDirective.decorators = [ + { type: Directive, args: [{ selector: '[someDirective]' },] } + ]; + SomeDirective.ctorParameters = function() { return [ + { type: ViewContainerRef, }, + { type: TemplateRef, }, + { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + ]; }; + SomeDirective.propDecorators = { + "input1": [{ type: Input },], + "input2": [{ type: Input },], + }; + return SomeDirective; + }()); + `, +}; + +const SIMPLE_CLASS_FILE = { + name: '/simple_class.js', + contents: ` + var EmptyClass = (function() { + function EmptyClass() {} + return EmptyClass; + }()); + var NoDecoratorConstructorClass = (function() { + function NoDecoratorConstructorClass(foo) { + } + return NoDecoratorConstructorClass; + }()); + `, +}; + +const FOO_FUNCTION_FILE = { + name: '/foo_function.js', + contents: ` + import { Directive } from '@angular/core'; + + function foo() {} + foo.decorators = [ + { type: Directive, args: [{ selector: '[ignored]' },] } + ]; + `, +}; + +const INVALID_DECORATORS_FILE = { + name: '/invalid_decorators.js', + contents: ` + var NotArrayLiteralDecorator = {}; + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = () => [ + { type: NotArrayLiteralDecorator, args: [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); + + var NotObjectLiteralDecorator = {}; + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: NotObjectLiteralDecorator }, + ]; + return NotObjectLiteral; + }()); + + var NoTypePropertyDecorator1 = {}; + var NoTypePropertyDecorator2 = {}; + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.decorators = [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ]; + return NoTypeProperty; + }()); + + var NotIdentifierDecorator = {}; + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ]; + return NotIdentifier; + }()); + `, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` + var NoArgsPropertyDecorator = {}; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.decorators = [ + { type: NoArgsPropertyDecorator }, + ]; + return NoArgsProperty; + }()); + + var NoPropertyAssignmentDecorator = {}; + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.decorators = [ + { type: NoPropertyAssignmentDecorator, args }, + ]; + return NoPropertyAssignment; + }()); + + var NotArrayLiteralDecorator = {}; + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.decorators = [ + { type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] }, + ]; + return NotArrayLiteral; + }()); + `, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` + var NotObjectLiteralDecorator = {}; + var NotObjectLiteral = (function() { + function NotObjectLiteral() { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: NotObjectLiteralDecorator },] + }); + return NotObjectLiteral; + }()); + + var NotObjectLiteralPropDecorator = {}; + var NotObjectLiteralProp = (function() { + function NotObjectLiteralProp() { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: NotObjectLiteralPropDecorator }, + ] + }; + return NotObjectLiteralProp; + }()); + + var NoTypePropertyDecorator1 = {}; + var NoTypePropertyDecorator2 = {}; + var NoTypeProperty = (function() { + function NoTypeProperty() { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }; + return NoTypeProperty; + }()); + + var NotIdentifierDecorator = {}; + var NotIdentifier = (function() { + function NotIdentifier() { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }; + return NotIdentifier; + }()); + `, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + var NoArgsPropertyDecorator = {}; + var NoArgsProperty = (function() { + function NoArgsProperty() { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: NoArgsPropertyDecorator },] + }; + return NoArgsProperty; + }()); + + var NoPropertyAssignmentDecorator = {}; + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment() { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: NoPropertyAssignmentDecorator, args },] + }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteralDecorator = {}; + var NotArrayLiteral = (function() { + function NotArrayLiteral() { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },], + }; + return NotArrayLiteral; + }()); + `, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + var NoParametersDecorator = {}; + var NoParameters = (function() { + function NoParameters() {} + return NoParameters; + }()); + + var ArrowFunctionDecorator = {}; + var ArrowFunction = (function() { + function ArrowFunction(arg1) { + } + ArrowFunction.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: ArrowFunctionDecorator },] } + ]; + return ArrowFunction; + }()); + + var NotArrayLiteralDecorator = {}; + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return 'StringsAreNotArrayLiterals'; }; + return NotArrayLiteral; + }()); + + var NotObjectLiteralDecorator = {}; + var NotObjectLiteral = (function() { + function NotObjectLiteral(arg1, arg2) { + } + NotObjectLiteral.ctorParameters = function() { return [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: NotObjectLiteralDecorator },] }, + ]; }; + return NotObjectLiteral; + }()); + + var NoTypePropertyDecorator1 = {}; + var NoTypePropertyDecorator2 = {}; + var NoTypeProperty = (function() { + function NoTypeProperty(arg1, arg2) { + } + NoTypeProperty.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { notType: NoTypePropertyDecorator1 }, + { type: NoTypePropertyDecorator2 }, + ] + }, + ]; }; + return NoTypeProperty; + }()); + + var NotIdentifierDecorator = {}; + var NotIdentifier = (function() { + function NotIdentifier(arg1, arg2) { + } + NotIdentifier.ctorParameters = function() { return [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: NotIdentifierDecorator }, + ] + }, + ]; }; + return NotIdentifier; + }()); + `, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + var NoArgsPropertyDecorator = {}; + var NoArgsProperty = (function() { + function NoArgsProperty(arg1) { + } + NoArgsProperty.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: NoArgsPropertyDecorator },] }, + ]; }; + return NoArgsProperty; + }()); + + var NoPropertyAssignmentDecorator = {}; + var args = [{ selector: '[ignored]' },]; + var NoPropertyAssignment = (function() { + function NoPropertyAssignment(arg1) { + } + NoPropertyAssignment.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: NoPropertyAssignmentDecorator, args },] }, + ]; }; + return NoPropertyAssignment; + }()); + + var NotArrayLiteralDecorator = {}; + var NotArrayLiteral = (function() { + function NotArrayLiteral(arg1) { + } + NotArrayLiteral.ctorParameters = function() { return [ + { type: 'ParamType', decorators: [{ type: NotArrayLiteralDecorator, args: () => [{ selector: '[ignored]' },] },] }, + ]; }; + return NotArrayLiteral; + }()); + `, +}; + +const IMPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {a} from './a.js'; + import {a as foo} from './a.js'; + + var b = a; + var c = foo; + var d = b; + `, + }, +]; + +const EXPORTS_FILES = [ + { + name: '/a.js', + contents: ` + export const a = 'a'; + `, + }, + { + name: '/b.js', + contents: ` + import {Directive} from '@angular/core'; + import {a} from './a'; + import {a as foo} from './a'; + export {Directive} from '@angular/core'; + export {a} from './a'; + export var b = a; + export var c = foo; + export var d = b; + export var e = 'e'; + export var DirectiveX = Directive; + export var SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }()); + `, + }, +]; + +describe('Esm5ReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + debugger; + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators).toBeDefined(); + expect(decorators.length).toEqual(1); + + const decorator = decorators[0]; + expect(decorator.name).toEqual('Directive'); + expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + + it('should return null if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + const decorators = host.getDecoratorsOfDeclaration(functionNode); + expect(decorators).toBe(null); + }); + + it('should return null if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toBe(null); + }); + + it('should ignore `decorators` if it is not an array literal', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode); + expect(decorators).toEqual([]); + }); + + it('should ignore decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotObjectLiteral', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotObjectLiteralDecorator'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NoTypeProperty', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATORS_FILE.name, 'NotIdentifier', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Directive'); + }); + + describe('(returned decorators `args`)', () => { + it('should be an empty array if decorator has no `args` property', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', ts.isVariableDeclaration); + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + debugger; + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { + host.getMembersOfClass(functionNode); + }).toThrowError(`Attempted to get members of a non-class: "function foo() {}"`); + }); + + it('should return an empty array if there are no prop decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members).toEqual([]); + }); + + it('should not process decorated properties in `propDecorators` if it is not an object literal', + () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteral', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(members.map(member => member.name)).not.toContain('prop'); + }); + + it('should ignore prop decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotObjectLiteralProp', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({ + name: 'NotObjectLiteralPropDecorator' + })); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NoTypeProperty', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATORS_FILE.name, 'NotIdentifier', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + let callCount = 0; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { + callCount++; + return {name: `name${callCount}`, from: `from${callCount}`}; + }); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + expect(spy).toHaveBeenCalled(); + spy.calls.allArgs().forEach(arg => expect(arg[0].getText()).toEqual('Input')); + + const index = members.findIndex(member => member.name === 'input1'); + expect(members[index].decorators !.length).toBe(1); + expect(members[index].decorators ![0].import).toEqual({name: 'name1', from: 'from1'}); + }); + + describe('(returned prop decorators `args`)', () => { + it('should be an empty array if prop decorator has no `args` property', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if prop decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_PROP_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_PROP_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + const prop = members.find(m => m.name === 'prop') !; + const decorators = prop.decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ + 'ViewContainerRef', 'TemplateRef', 'undefined' + ]); + }); + + it('should throw if the symbol is not a class', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const functionNode = + getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(() => { host.getConstructorParameters(functionNode); }) + .toThrowError( + 'Attempted to get constructor parameters of a non-class: "function foo() {}"'); + }); + + // In ES5 there is no such thing as a constructor-less class + // it('should return `null` if there is no constructor', () => { }); + + it('should return an array even if there are no decorators', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'NoDecoratorConstructorClass', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual(jasmine.any(Array)); + expect(parameters !.length).toEqual(1); + expect(parameters ![0].name).toEqual('foo'); + expect(parameters ![0].decorators).toBe(null); + }); + + it('should return an empty array if there are no constructor parameters', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoParameters', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toEqual([]); + }); + + // In ES5 there are no arrow functions + // it('should ignore `ctorParameters` if it is an arrow function', () => { }); + + it('should ignore `ctorParameters` if it does not return an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotArrayLiteral', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(1); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + }); + + describe('(returned parameters `decorators`)', () => { + it('should ignore param decorator elements that are not object literals', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotObjectLiteral', + ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters !.length).toBe(2); + expect(parameters ![0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: null, + })); + expect(parameters ![1]).toEqual(jasmine.objectContaining({ + name: 'arg2', + decorators: jasmine.any(Array) as any + })); + }); + + it('should ignore param decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NoTypeProperty', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NoTypePropertyDecorator2'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotIdentifier', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'NotIdentifierDecorator'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + + describe('(returned parameters `decorators.args`)', () => { + it('should be an empty array if param decorator has no `args` property', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoArgsProperty', + ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + expect(parameters !.length).toBe(1); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoArgsPropertyDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if param decorator\'s `args` has no property assignment', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NoPropertyAssignment', + ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NoPropertyAssignmentDecorator'); + expect(decorators[0].args).toEqual([]); + }); + + it('should be an empty array if `args` property value is not an array literal', () => { + const program = makeProgram(INVALID_CTOR_DECORATOR_ARGS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATOR_ARGS_FILE.name, 'NotArrayLiteral', + ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![0].decorators !; + + expect(decorators.length).toBe(1); + expect(decorators[0].name).toBe('NotArrayLiteralDecorator'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getImportOfIdentifier', () => { + it('should find the import of an identifier', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'b', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should find the name by which the identifier was exported, not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'c', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toEqual({name: 'a', from: './a.js'}); + }); + + it('should return null if the identifier was not imported', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const variableNode = + getDeclaration(program, IMPORTS_FILES[1].name, 'd', ts.isVariableDeclaration); + const importOfIdent = host.getImportOfIdentifier(variableNode.initializer as ts.Identifier); + + expect(importOfIdent).toBeNull(); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'ViewContainerRef', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe(null); + }); + + it('should return the declaration of an externally defined identifier', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const identifierOfDirective = ((classDecorators[0].node as ts.ObjectLiteralExpression) + .properties[0] as ts.PropertyAssignment) + .initializer as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, 'node_modules/@angular/core/index.ts', 'Directive', ts.isVariableDeclaration); + const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective); + expect(actualDeclaration).not.toBe(null); + expect(actualDeclaration !.node).toBe(expectedDeclarationNode); + expect(actualDeclaration !.viaModule).toBe('@angular/core'); + }); + }); + + describe('getExportsOfModule()', () => { + it('should return a map of all the exports from a given module', () => { + const program = makeProgram(...EXPORTS_FILES); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const file = program.getSourceFile(EXPORTS_FILES[1].name) !; + const exportDeclarations = host.getExportsOfModule(file); + expect(exportDeclarations).not.toBe(null); + expect(Array.from(exportDeclarations !.keys())).toEqual([ + 'Directive', + 'a', + 'b', + 'c', + 'd', + 'e', + 'DirectiveX', + 'SomeClass', + ]); + + const values = Array.from(exportDeclarations !.values()) + .map(declaration => [declaration.node.getText(), declaration.viaModule]); + expect(values).toEqual([ + // TODO: clarify what is expected here... + //[`Directive = callableClassDecorator()`, '@angular/core'], + [`Directive = callableClassDecorator()`, null], + [`a = 'a'`, null], + [`b = a`, null], + [`c = foo`, null], + [`d = b`, null], + [`e = 'e'`, null], + [`DirectiveX = Directive`, null], + [ + `SomeClass = (function() { + function SomeClass() {} + return SomeClass; + }())`, + null + ], + ]); + }); + }); + + describe('isClass()', () => { + it('should return true if a given node is an ES5 class declaration', () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const node = + getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + expect(host.isClass(node)).toBe(true); + }); + + it('should return false if a given node is not an ES5 class declaration', () => { + const program = makeProgram(FOO_FUNCTION_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(host.isClass(node)).toBe(false); + }); + }); +});