From bec4ca0c73f912069aefc8399faded1e1a5a6a8f Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Wed, 10 Oct 2018 14:17:32 +0100 Subject: [PATCH] refactor(ivy): ngcc - recombine flat and non-flat `Esm2015ReflectionHost` (#26403) Going forward we need to be able to do the same work on both flat and non-flat module formats (such as computing arity and transforming .d.ts files) PR Close #26403 --- .../src/analysis/switch_marker_analyzer.ts | 4 +- .../src/ngcc/src/host/esm2015_host.ts | 1073 ++++++++++++++- .../src/ngcc/src/host/esm5_host.ts | 4 +- .../src/ngcc/src/host/fesm2015_host.ts | 1076 --------------- .../src/ngcc/src/packages/transformer.ts | 4 +- .../test/analysis/decoration_analyzer_spec.ts | 4 +- .../analysis/switch_marker_analyzer_spec.ts | 4 +- ....ts => esm2015_host_import_helper_spec.ts} | 40 +- .../src/ngcc/test/host/esm2015_host_spec.ts | 1126 ++++++++++++++- .../src/ngcc/test/host/esm5_host_spec.ts | 6 +- .../src/ngcc/test/host/fesm2015_host_spec.ts | 1212 ----------------- .../test/rendering/esm2015_renderer_spec.ts | 4 +- .../src/ngcc/test/rendering/renderer_spec.ts | 6 +- 13 files changed, 2217 insertions(+), 2346 deletions(-) delete mode 100644 packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts rename packages/compiler-cli/src/ngcc/test/host/{fesm2015_host_import_helper_spec.ts => esm2015_host_import_helper_spec.ts} (90%) delete mode 100644 packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts diff --git a/packages/compiler-cli/src/ngcc/src/analysis/switch_marker_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/switch_marker_analyzer.ts index 22962e593d..394cab1797 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/switch_marker_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/switch_marker_analyzer.ts @@ -17,13 +17,13 @@ export type SwitchMarkerAnalyses = Map; export const SwitchMarkerAnalyses = Map; /** - * This Analyzer will analyse the files that have an NGCC switch marker in them + * This Analyzer will analyse the files that have an R3 switch marker in them * that will be replaced. */ export class SwitchMarkerAnalyzer { constructor(private host: NgccReflectionHost) {} /** - * Analyze the files in the program to identify declarations that contain NGCC + * Analyze the files in the program to identify declarations that contain R3 * switch markers. * @param program The program to analyze. * @return A map of source files to analysis objects. The map will contain only the diff --git a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts index 47b32f67ca..50707b679c 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm2015_host.ts @@ -9,12 +9,336 @@ import {readFileSync} from 'fs'; import * as ts from 'typescript'; -import {DtsMapper} from './dts_mapper'; -import {Fesm2015ReflectionHost} from './fesm2015_host'; +import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host'; +import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; +import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils'; -export class Esm2015ReflectionHost extends Fesm2015ReflectionHost { - constructor(isCore: boolean, checker: ts.TypeChecker, protected dtsMapper: DtsMapper) { - super(isCore, checker); +import {DecoratedClass} from './decorated_class'; +import {DecoratedFile} from './decorated_file'; +import {DtsMapper} from './dts_mapper'; +import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} 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(protected isCore: boolean, checker: ts.TypeChecker, protected dtsMapper?: DtsMapper) { + 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) { + return null; + } + const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); + if (decoratorsProperty) { + return this.getClassDecoratorsFromStaticProperty(decoratorsProperty); + } else { + return this.getClassDecoratorsFromHelperCall(symbol); + } + } + + /** + * 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); + } + }); + } + + // If this class was declared as a VariableDeclaration then it may have static properties + // attached to the variable rather than the class itself + // For example: + // ``` + // let MyClass = class MyClass { + // // no static properties here! + // } + // MyClass.staticProperty = ...; + // ``` + if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) { + const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name); + if (variableSymbol && variableSymbol.exports) { + variableSymbol.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): CtorParameter[]|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) { + return this.getConstructorParamInfo(classSymbol, parameterNodes); + } + return null; + } + + /** + * Find a symbol for a node that we think is a class. + * @param node the node whose symbol we are finding. + * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. + */ + getClassSymbol(declaration: ts.Node): ts.Symbol|undefined { + if (ts.isClassDeclaration(declaration)) { + return declaration.name && this.checker.getSymbolAtLocation(declaration.name); + } + if (ts.isVariableDeclaration(declaration) && declaration.initializer && + ts.isClassExpression(declaration.initializer)) { + return declaration.initializer.name && + this.checker.getSymbolAtLocation(declaration.initializer.name); + } + return undefined; + } + + /** + * Search the given module for variable declarations in which the initializer + * is an identifier marked with the `PRE_R3_MARKER`. + * @param module the module in which to search for switchable declarations. + * @returns an array of variable declarations that match. + */ + getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] { + // Don't bother to walk the AST if the marker is not found in the text + return module.getText().indexOf(PRE_R3_MARKER) >= 0 ? + findAll(module, isSwitchableVariableDeclaration) : + []; + } + + getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null { + const value = super.getVariableValue(declaration); + if (value) { + return value; + } + + // We have a variable declaration that has no initializer. For example: + // + // ``` + // var HttpClientXsrfModule_1; + // ``` + // + // So look for the special scenario where the variable is being assigned in + // a nearby statement to the return value of a call to `__decorate`. + // Then find the 2nd argument of that call, the "target", which will be the + // actual class identifier. For example: + // + // ``` + // HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([ + // NgModule({ + // providers: [], + // }) + // ], HttpClientXsrfModule); + // ``` + // + // And finally, find the declaration of the identifier in that argument. + // Note also that the assignment can occur within another assignment. + // + const block = declaration.parent.parent.parent; + const symbol = this.checker.getSymbolAtLocation(declaration.name); + if (symbol && (ts.isBlock(block) || ts.isSourceFile(block))) { + const decorateCall = this.findDecoratedVariableValue(block, symbol); + const target = decorateCall && decorateCall.arguments[1]; + if (target && ts.isIdentifier(target)) { + const targetSymbol = this.checker.getSymbolAtLocation(target); + const targetDeclaration = targetSymbol && targetSymbol.valueDeclaration; + if (targetDeclaration) { + if (ts.isClassDeclaration(targetDeclaration) || + ts.isFunctionDeclaration(targetDeclaration)) { + // The target is just a function or class declaration + // so return its identifier as the variable value. + return targetDeclaration.name || null; + } else if (ts.isVariableDeclaration(targetDeclaration)) { + // The target is a variable declaration, so find the far right expression, + // in the case of multiple assignments (e.g. `var1 = var2 = value`). + let targetValue = targetDeclaration.initializer; + while (targetValue && isAssignment(targetValue)) { + targetValue = targetValue.right; + } + if (targetValue) { + return targetValue; + } + } + } + } + } + return null; + } + + /** + * Determine if an identifier was imported from another module and return `Import` metadata + * describing its origin. + * + * @param id a TypeScript `ts.Identifer` to reflect. + * + * @returns metadata about the `Import` if the identifier was imported from another module, or + * `null` if the identifier doesn't resolve to an import but instead is locally defined. + */ + getImportOfIdentifier(id: ts.Identifier): Import|null { + return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); + } + + /* + * Find all the files accessible via an entry-point, that contain decorated classes. + * @param entryPoint The starting point file for finding files that contain decorated classes. + * @returns A collection of files objects that hold info about the decorated classes and import + * information. + */ + findDecoratedFiles(entryPoint: ts.SourceFile): Map { + const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); + const map = new Map(); + if (moduleSymbol) { + const exportedSymbols = + this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker)); + const exportedDeclarations = + exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined); + + const decoratedClasses = + exportedDeclarations + .map(declaration => { + if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { + const name = declaration.name && ts.isIdentifier(declaration.name) ? + declaration.name.text : + undefined; + const decorators = this.getDecoratorsOfDeclaration(declaration); + return decorators && isDefined(name) ? + new DecoratedClass(name, declaration, decorators) : + undefined; + } + return undefined; + }) + .filter(isDefined); + + decoratedClasses.forEach(clazz => { + const file = clazz.declaration.getSourceFile(); + if (!map.has(file)) { + map.set(file, new DecoratedFile(file)); + } + map.get(file) !.decoratedClasses.push(clazz); + }); + } + return map; } /** @@ -24,7 +348,7 @@ export class Esm2015ReflectionHost extends Fesm2015ReflectionHost { * is not a class or has an unknown number of type parameters. */ getGenericArityOfClass(clazz: ts.Declaration): number|null { - if (ts.isClassDeclaration(clazz) && clazz.name) { + if (this.dtsMapper && ts.isClassDeclaration(clazz) && clazz.name) { const sourcePath = clazz.getSourceFile(); const dtsPath = this.dtsMapper.getDtsFileNameFor(sourcePath.fileName); const dtsContents = readFileSync(dtsPath, 'utf8'); @@ -43,4 +367,741 @@ export class Esm2015ReflectionHost extends Fesm2015ReflectionHost { } return null; } + + + ///////////// Protected Helpers ///////////// + + /** + * Walk the AST looking for an assignment to the specified symbol. + * @param node The current node we are searching. + * @returns an expression that represents the value of the variable, or undefined if none can be + * found. + */ + protected findDecoratedVariableValue(node: ts.Node|undefined, symbol: ts.Symbol): + ts.CallExpression|null { + if (!node) { + return null; + } + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + const left = node.left; + const right = node.right; + if (ts.isIdentifier(left) && this.checker.getSymbolAtLocation(left) === symbol) { + return (ts.isCallExpression(right) && getCalleeName(right) === '__decorate') ? right : null; + } + return this.findDecoratedVariableValue(right, symbol); + } + return node.forEachChild(node => this.findDecoratedVariableValue(node, symbol)) || null; + } + + /** + * Try to retrieve the symbol of a static property on a class. + * @param symbol the class whose property we are interested in. + * @param propertyName the name of static property. + * @returns the symbol if it is found or `undefined` if not. + */ + protected getStaticProperty(symbol: ts.Symbol, propertyName: ts.__String): ts.Symbol|undefined { + return symbol.exports && symbol.exports.get(propertyName); + } + + /** + * Get all class decorators for the given class, where the decorators are declared + * via a static property. For example: + * + * ``` + * class SomeDirective {} + * SomeDirective.decorators = [ + * { type: Directive, args: [{ selector: '[someDirective]' },] } + * ]; + * ``` + * + * @param decoratorsSymbol the property containing the decorators we want to get. + * @returns an array of decorators or null if none where found. + */ + protected getClassDecoratorsFromStaticProperty(decoratorsSymbol: ts.Symbol): Decorator[]|null { + const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; + if (decoratorsIdentifier && decoratorsIdentifier.parent) { + if (ts.isBinaryExpression(decoratorsIdentifier.parent) && + decoratorsIdentifier.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { + // AST of the array of decorator values + const decoratorsArray = decoratorsIdentifier.parent.right; + return this.reflectDecorators(decoratorsArray) + .filter(decorator => this.isFromCore(decorator)); + } + } + return null; + } + + /** + * Get all class decorators for the given class, where the decorators are declared + * via the `__decorate` helper method. For example: + * + * ``` + * let SomeDirective = class SomeDirective {} + * SomeDirective = __decorate([ + * Directive({ selector: '[someDirective]' }), + * ], SomeDirective); + * ``` + * + * @param symbol the class whose decorators we want to get. + * @returns an array of decorators or null if none where found. + */ + protected getClassDecoratorsFromHelperCall(symbol: ts.Symbol): Decorator[]|null { + const decorators: Decorator[] = []; + const helperCalls = this.getHelperCallsForClass(symbol, '__decorate'); + helperCalls.forEach(helperCall => { + const {classDecorators} = + this.reflectDecoratorsFromHelperCall(helperCall, makeClassTargetFilter(symbol.name)); + classDecorators.filter(decorator => this.isFromCore(decorator)) + .forEach(decorator => decorators.push(decorator)); + }); + return decorators.length ? decorators : null; + } + + /** + * Get all the member decorators for the given class. + * @param classSymbol the class whose member decorators we are interested in. + * @returns a map whose keys are the name of the members and whose values are collections of + * decorators for the given member. + */ + protected getMemberDecorators(classSymbol: ts.Symbol): Map { + const decoratorsProperty = this.getStaticProperty(classSymbol, PROP_DECORATORS); + if (decoratorsProperty) { + return this.getMemberDecoratorsFromStaticProperty(decoratorsProperty); + } else { + return this.getMemberDecoratorsFromHelperCalls(classSymbol); + } + } + + /** + * Member decorators may be declared as static properties of the class: + * + * ``` + * SomeDirective.propDecorators = { + * "ngForOf": [{ type: Input },], + * "ngForTrackBy": [{ type: Input },], + * "ngForTemplate": [{ type: Input },], + * }; + * ``` + * + * @param decoratorsProperty the class whose member decorators we are interested in. + * @returns a map whose keys are the name of the members and whose values are collections of + * decorators for the given member. + */ + protected getMemberDecoratorsFromStaticProperty(decoratorsProperty: ts.Symbol): + Map { + const memberDecorators = new Map(); + // Symbol of the identifier for `SomeDirective.propDecorators`. + const propDecoratorsMap = getPropertyValueFromSymbol(decoratorsProperty); + if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { + const propertiesMap = reflectObjectLiteral(propDecoratorsMap); + propertiesMap.forEach((value, name) => { + const decorators = + this.reflectDecorators(value).filter(decorator => this.isFromCore(decorator)); + if (decorators.length) { + memberDecorators.set(name, decorators); + } + }); + } + return memberDecorators; + } + + /** + * Member decorators may be declared via helper call statements. + * + * ``` + * __decorate([ + * Input(), + * __metadata("design:type", String) + * ], SomeDirective.prototype, "input1", void 0); + * ``` + * + * @param classSymbol the class whose member decorators we are interested in. + * @returns a map whose keys are the name of the members and whose values are collections of + * decorators for the given member. + */ + protected getMemberDecoratorsFromHelperCalls(classSymbol: ts.Symbol): Map { + const memberDecoratorMap = new Map(); + const helperCalls = this.getHelperCallsForClass(classSymbol, '__decorate'); + helperCalls.forEach(helperCall => { + const {memberDecorators} = this.reflectDecoratorsFromHelperCall( + helperCall, makeMemberTargetFilter(classSymbol.name)); + memberDecorators.forEach((decorators, memberName) => { + if (memberName) { + const memberDecorators = memberDecoratorMap.get(memberName) || []; + const coreDecorators = decorators.filter(decorator => this.isFromCore(decorator)); + memberDecoratorMap.set(memberName, memberDecorators.concat(coreDecorators)); + } + }); + }); + return memberDecoratorMap; + } + + /** + * Extract decorator info from `__decorate` helper function calls. + * @param helperCall the call to a helper that may contain decorator calls + * @param targetFilter a function to filter out targets that we are not interested in. + * @returns a mapping from member name to decorators, where the key is either the name of the + * member or `undefined` if it refers to decorators on the class as a whole. + */ + protected reflectDecoratorsFromHelperCall( + helperCall: ts.CallExpression, targetFilter: TargetFilter): + {classDecorators: Decorator[], memberDecorators: Map} { + const classDecorators: Decorator[] = []; + const memberDecorators = new Map(); + + // First check that the `target` argument is correct + if (targetFilter(helperCall.arguments[1])) { + // Grab the `decorators` argument which should be an array of calls + const decoratorCalls = helperCall.arguments[0]; + if (decoratorCalls && ts.isArrayLiteralExpression(decoratorCalls)) { + decoratorCalls.elements.forEach(element => { + // We only care about those elements that are actual calls + if (ts.isCallExpression(element)) { + const decorator = this.reflectDecoratorCall(element); + if (decorator) { + const keyArg = helperCall.arguments[2]; + const keyName = keyArg && ts.isStringLiteral(keyArg) ? keyArg.text : undefined; + if (keyName === undefined) { + classDecorators.push(decorator); + } else { + const decorators = memberDecorators.get(keyName) || []; + decorators.push(decorator); + memberDecorators.set(keyName, decorators); + } + } + } + }); + } + } + return {classDecorators, memberDecorators}; + } + + /** + * Extract the decorator information from a call to a decorator as a function. + * This happens when the decorators has been used in a `__decorate` helper call. + * For example: + * + * ``` + * __decorate([ + * Directive({ selector: '[someDirective]' }), + * ], SomeDirective); + * ``` + * + * Here the `Directive` decorator is decorating `SomeDirective` and the options for + * the decorator are passed as arguments to the `Directive()` call. + * + * @param call the call to the decorator. + * @returns a decorator containing the reflected information, or null if the call + * is not a valid decorator call. + */ + protected reflectDecoratorCall(call: ts.CallExpression): Decorator|null { + // The call could be of the form `Decorator(...)` or `namespace_1.Decorator(...)` + const decoratorExpression = + ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression; + if (ts.isIdentifier(decoratorExpression)) { + // We found a decorator! + const decoratorIdentifier = decoratorExpression; + return { + name: decoratorIdentifier.text, + identifier: decoratorIdentifier, + import: this.getImportOfIdentifier(decoratorIdentifier), + node: call, + args: Array.from(call.arguments) + }; + } + return null; + } + + /** + * Check the given statement to see if it is a call to the specified helper function or null if + * not found. + * + * Matching statements will look like: `tslib_1.__decorate(...);`. + * @param statement the statement that may contain the call. + * @param helperName the name of the helper we are looking for. + * @returns the node that corresponds to the `__decorate(...)` call or null if the statement does + * not match. + */ + protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null { + if (ts.isExpressionStatement(statement)) { + const expression = + isAssignmentStatement(statement) ? statement.expression.right : statement.expression; + if (ts.isCallExpression(expression) && getCalleeName(expression) === helperName) { + return expression; + } + } + return null; + } + + + + /** + * Reflect over the given array node and extract decorator information from each element. + * + * This is used for decorators that are defined in static properties. For example: + * + * ``` + * SomeDirective.decorators = [ + * { type: Directive, args: [{ selector: '[someDirective]' },] } + * ]; + * ``` + * + * @param decoratorsArray an expression that contains decorator information. + * @returns an array of decorator info that was reflected from the array node. + */ + 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, + identifier: typeIdentifier, + import: this.getImportOfIdentifier(typeIdentifier), node, + args: getDecoratorArgs(node), + }); + } + } + }); + } + return decorators; + } + + /** + * Reflect over a symbol and extract the member information, combining it with the + * provided decorator information, and whether it is a static member. + * @param symbol the symbol for the member to reflect over. + * @param decorators an array of decorators associated with the member. + * @param isStatic true if this member is static, false if it is an instance property. + * @returns the reflected member information, or null if the symbol is not a member. + */ + 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; + } + + /** + * Get the parameter decorators of a class constructor. + * + * @param classSymbol the class whose parameter info we want to get. + * @param parameterNodes the array of TypeScript parameter nodes for this class's constructor. + * @returns an array of constructor parameter info objects. + */ + protected getConstructorParamInfo( + classSymbol: ts.Symbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] { + const paramsProperty = this.getStaticProperty(classSymbol, CONSTRUCTOR_PARAMS); + const paramInfo: ParamInfo[]|null = paramsProperty ? + this.getParamInfoFromStaticProperty(paramsProperty) : + this.getParamInfoFromHelperCall(classSymbol, parameterNodes); + + return parameterNodes.map((node, index) => { + const {decorators, type} = + paramInfo && paramInfo[index] ? paramInfo[index] : {decorators: null, type: null}; + const nameNode = node.name; + return {name: getNameText(nameNode), nameNode, type, decorators}; + }); + } + + /** + * Get the parameter type and decorators for the constructor of a class, + * where the information is stored on a static method of the class. + * + * Note that in ESM2015, the method is defined by an arrow function that returns an array of + * decorator and type information. + * + * ``` + * SomeDirective.ctorParameters = () => [ + * { type: ViewContainerRef, }, + * { type: TemplateRef, }, + * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, + * ]; + * ``` + * + * @param paramDecoratorsProperty the property that holds the parameter info we want to get. + * @returns an array of objects containing the type and decorators for each parameter. + */ + protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null { + const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty); + if (paramDecorators && ts.isArrowFunction(paramDecorators)) { + if (ts.isArrayLiteralExpression(paramDecorators.body)) { + const elements = paramDecorators.body.elements; + return elements + .map( + element => + ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null) + .map(paramInfo => { + const type = paramInfo && paramInfo.get('type') || null; + const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const decorators = decoratorInfo && + this.reflectDecorators(decoratorInfo) + .filter(decorator => this.isFromCore(decorator)); + return {type, decorators}; + }); + } + } + return null; + } + + /** + * Get the parmeter type and decorators for a class where the information is stored on + * in calls to `__decorate` helpers. + * + * Reflect over the helpers to find the decorators and types about each of + * the class's constructor parameters. + * + * @param classSymbol the class whose parameter info we want to get. + * @param parameterNodes the array of TypeScript parameter nodes for this class's constructor. + * @returns an array of objects containing the type and decorators for each parameter. + */ + protected getParamInfoFromHelperCall( + classSymbol: ts.Symbol, parameterNodes: ts.ParameterDeclaration[]): ParamInfo[] { + const parameters: ParamInfo[] = parameterNodes.map(() => ({type: null, decorators: null})); + const helperCalls = this.getHelperCallsForClass(classSymbol, '__decorate'); + helperCalls.forEach(helperCall => { + const {classDecorators} = + this.reflectDecoratorsFromHelperCall(helperCall, makeClassTargetFilter(classSymbol.name)); + classDecorators.forEach(call => { + switch (call.name) { + case '__metadata': + const metadataArg = call.args && call.args[0]; + const typesArg = call.args && call.args[1]; + const isParamTypeDecorator = metadataArg && ts.isStringLiteral(metadataArg) && + metadataArg.text === 'design:paramtypes'; + const types = typesArg && ts.isArrayLiteralExpression(typesArg) && typesArg.elements; + if (isParamTypeDecorator && types) { + types.forEach((type, index) => parameters[index].type = type); + } + break; + case '__param': + const paramIndexArg = call.args && call.args[0]; + const decoratorCallArg = call.args && call.args[1]; + const paramIndex = paramIndexArg && ts.isNumericLiteral(paramIndexArg) ? + parseInt(paramIndexArg.text, 10) : + NaN; + const decorator = decoratorCallArg && ts.isCallExpression(decoratorCallArg) ? + this.reflectDecoratorCall(decoratorCallArg) : + null; + if (!isNaN(paramIndex) && decorator) { + const decorators = parameters[paramIndex].decorators = + parameters[paramIndex].decorators || []; + decorators.push(decorator); + } + break; + } + }); + }); + return parameters; + } + + /** + * Search statements related to the given class for calls to the specified helper. + * @param classSymbol the class whose helper calls we are interested in. + * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in. + * @returns an array of CallExpression nodes for each matching helper call. + */ + protected getHelperCallsForClass(classSymbol: ts.Symbol, helperName: string): + ts.CallExpression[] { + return this.getStatementsForClass(classSymbol) + .map(statement => this.getHelperCall(statement, helperName)) + .filter(isDefined); + } + + /** + * Find statements related to the given class that may contain calls to a helper. + * + * In ESM2015 code the helper calls are in the top level module, so we have to consider + * all the statements in the module. + * + * @param classSymbol the class whose helper calls we are interested in. + * @returns an array of statements that may contain helper calls. + */ + protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] { + return Array.from(classSymbol.valueDeclaration.getSourceFile().statements); + } + + /** + * Try to get the import info for this identifier as though it is a namespaced import. + * For example, if the identifier is the `__metadata` part of a property access chain like: + * + * ``` + * tslib_1.__metadata + * ``` + * + * then it might be that `tslib_1` is a namespace import such as: + * + * ``` + * import * as tslib_1 from 'tslib'; + * ``` + * @param id the TypeScript identifier to find the import info for. + * @returns The import info if this is a namespaced import or `null`. + */ + protected getImportOfNamespacedIdentifier(id: ts.Identifier): Import|null { + if (!(ts.isPropertyAccessExpression(id.parent) && id.parent.name === id)) { + return null; + } + + const namespaceIdentifier = getFarLeftIdentifier(id.parent); + const namespaceSymbol = + namespaceIdentifier && this.checker.getSymbolAtLocation(namespaceIdentifier); + const declaration = namespaceSymbol && namespaceSymbol.declarations.length === 1 ? + namespaceSymbol.declarations[0] : + null; + const namespaceDeclaration = + declaration && ts.isNamespaceImport(declaration) ? declaration : null; + if (!namespaceDeclaration) { + return null; + } + + const importDeclaration = namespaceDeclaration.parent.parent; + if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) { + // Should not happen as this would be invalid TypesScript + return null; + } + + return { + from: importDeclaration.moduleSpecifier.text, + name: id.text, + }; + } + + /** + * Test whether a decorator was imported from `@angular/core`. + * + * Is the decorator: + * * externally imported from `@angulare/core`? + * * the current hosted program is actually `@angular/core` and + * - relatively internally imported; or + * - not imported, from the current file. + * + * @param decorator the decorator to test. + */ + isFromCore(decorator: Decorator): boolean { + if (this.isCore) { + return !decorator.import || /^\./.test(decorator.import.from); + } else { + return !!decorator.import && decorator.import.from === '@angular/core'; + } + } +} + +///////////// Exported Helpers ///////////// + +export type ParamInfo = { + decorators: Decorator[] | null, + type: ts.Expression | null +}; + +/** + * A statement node that represents an assignment. + */ +export type AssignmentStatement = + ts.ExpressionStatement & {expression: {left: ts.Identifier, right: ts.Expression}}; + +/** + * Test whether a statement node is an assignment statement. + * @param statement the statement to test. + */ +export function isAssignmentStatement(statement: ts.Statement): statement is AssignmentStatement { + return ts.isExpressionStatement(statement) && isAssignment(statement.expression) && + ts.isIdentifier(statement.expression.left); +} + +export function isAssignment(expression: ts.Expression): + expression is ts.AssignmentExpression { + return ts.isBinaryExpression(expression) && + expression.operatorToken.kind === ts.SyntaxKind.EqualsToken; +} + +/** + * The type of a function that can be used to filter out helpers based on their target. + * This is used in `reflectDecoratorsFromHelperCall()`. + */ +export type TargetFilter = (target: ts.Expression) => boolean; + +/** + * Creates a function that tests whether the given expression is a class target. + * @param className the name of the class we want to target. + */ +export function makeClassTargetFilter(className: string): TargetFilter { + return (target: ts.Expression): boolean => ts.isIdentifier(target) && target.text === className; +} + +/** + * Creates a function that tests whether the given expression is a class member target. + * @param className the name of the class we want to target. + */ +export function makeMemberTargetFilter(className: string): TargetFilter { + return (target: ts.Expression): boolean => ts.isPropertyAccessExpression(target) && + ts.isIdentifier(target.expression) && target.expression.text === className && + target.name.text === 'prototype'; +} + +/** + * 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; +} + +/** + * A callee could be one of: `__decorate(...)` or `tslib_1.__decorate`. + */ +function getCalleeName(call: ts.CallExpression): string|null { + if (ts.isIdentifier(call.expression)) { + return call.expression.text; + } + if (ts.isPropertyAccessExpression(call.expression)) { + return call.expression.name.text; + } + return null; +} + +///////////// Internal Helpers ///////////// + +function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { + // The arguments of a decorator are held in the `args` property of its declaration object. + 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) : + []; +} + +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); +} + +/** + * Compute the left most identifier in a property access chain. E.g. the `a` of `a.b.c.d`. + * @param propertyAccess The starting property access expression from which we want to compute + * the left most identifier. + * @returns the left most identifier in the chain or `null` if it is not an identifier. + */ +function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier|null { + while (ts.isPropertyAccessExpression(propertyAccess.expression)) { + propertyAccess = propertyAccess.expression; + } + return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null; } diff --git a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts index 6d074bfe0f..5ea5c0b95c 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -14,7 +14,7 @@ import {getNameText, getOriginalSymbol, isDefined} from '../utils'; import {DecoratedClass} from './decorated_class'; import {DecoratedFile} from './decorated_file'; -import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './fesm2015_host'; +import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host'; /** @@ -34,7 +34,7 @@ import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignm * a static method called `ctorParameters`. * */ -export class Esm5ReflectionHost extends Fesm2015ReflectionHost { +export class Esm5ReflectionHost extends Esm2015ReflectionHost { constructor(isCore: boolean, checker: ts.TypeChecker) { super(isCore, checker); } /** diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts deleted file mode 100644 index 1aa5cf196f..0000000000 --- a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts +++ /dev/null @@ -1,1076 +0,0 @@ -/** - * @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 {normalize} from 'canonical-path'; -import * as ts from 'typescript'; - -import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host'; -import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils'; - -import {DecoratedClass} from './decorated_class'; -import {DecoratedFile} from './decorated_file'; -import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} 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 Fesm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost { - constructor(protected isCore: boolean, 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) { - return null; - } - const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); - if (decoratorsProperty) { - return this.getClassDecoratorsFromStaticProperty(decoratorsProperty); - } else { - return this.getClassDecoratorsFromHelperCall(symbol); - } - } - - /** - * 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); - } - }); - } - - // If this class was declared as a VariableDeclaration then it may have static properties - // attached to the variable rather than the class itself - // For example: - // ``` - // let MyClass = class MyClass { - // // no static properties here! - // } - // MyClass.staticProperty = ...; - // ``` - if (ts.isVariableDeclaration(symbol.valueDeclaration.parent)) { - const variableSymbol = this.checker.getSymbolAtLocation(symbol.valueDeclaration.parent.name); - if (variableSymbol && variableSymbol.exports) { - variableSymbol.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): CtorParameter[]|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) { - return this.getConstructorParamInfo(classSymbol, parameterNodes); - } - return null; - } - - /** - * Find a symbol for a node that we think is a class. - * @param node the node whose symbol we are finding. - * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. - */ - getClassSymbol(declaration: ts.Node): ts.Symbol|undefined { - if (ts.isClassDeclaration(declaration)) { - return declaration.name && this.checker.getSymbolAtLocation(declaration.name); - } - if (ts.isVariableDeclaration(declaration) && declaration.initializer && - ts.isClassExpression(declaration.initializer)) { - return declaration.initializer.name && - this.checker.getSymbolAtLocation(declaration.initializer.name); - } - return undefined; - } - - /** - * Search the given module for variable declarations in which the initializer - * is an identifier marked with the `PRE_R3_MARKER`. - * @param module the module in which to search for switchable declarations. - * @returns an array of variable declarations that match. - */ - getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] { - // Don't bother to walk the AST if the marker is not found in the text - return module.getText().indexOf(PRE_R3_MARKER) >= 0 ? - findAll(module, isSwitchableVariableDeclaration) : - []; - } - - getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null { - const value = super.getVariableValue(declaration); - if (value) { - return value; - } - - // We have a variable declaration that has no initializer. For example: - // - // ``` - // var HttpClientXsrfModule_1; - // ``` - // - // So look for the special scenario where the variable is being assigned in - // a nearby statement to the return value of a call to `__decorate`. - // Then find the 2nd argument of that call, the "target", which will be the - // actual class identifier. For example: - // - // ``` - // HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([ - // NgModule({ - // providers: [], - // }) - // ], HttpClientXsrfModule); - // ``` - // - // And finally, find the declaration of the identifier in that argument. - // Note also that the assignment can occur within another assignment. - // - const block = declaration.parent.parent.parent; - const symbol = this.checker.getSymbolAtLocation(declaration.name); - if (symbol && (ts.isBlock(block) || ts.isSourceFile(block))) { - const decorateCall = this.findDecoratedVariableValue(block, symbol); - const target = decorateCall && decorateCall.arguments[1]; - if (target && ts.isIdentifier(target)) { - const targetSymbol = this.checker.getSymbolAtLocation(target); - const targetDeclaration = targetSymbol && targetSymbol.valueDeclaration; - if (targetDeclaration) { - if (ts.isClassDeclaration(targetDeclaration) || - ts.isFunctionDeclaration(targetDeclaration)) { - // The target is just a function or class declaration - // so return its identifier as the variable value. - return targetDeclaration.name || null; - } else if (ts.isVariableDeclaration(targetDeclaration)) { - // The target is a variable declaration, so find the far right expression, - // in the case of multiple assignments (e.g. `var1 = var2 = value`). - let targetValue = targetDeclaration.initializer; - while (targetValue && isAssignment(targetValue)) { - targetValue = targetValue.right; - } - if (targetValue) { - return targetValue; - } - } - } - } - } - return null; - } - - /** - * Determine if an identifier was imported from another module and return `Import` metadata - * describing its origin. - * - * @param id a TypeScript `ts.Identifer` to reflect. - * - * @returns metadata about the `Import` if the identifier was imported from another module, or - * `null` if the identifier doesn't resolve to an import but instead is locally defined. - */ - getImportOfIdentifier(id: ts.Identifier): Import|null { - return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); - } - - /* - * Find all the files accessible via an entry-point, that contain decorated classes. - * @param entryPoint The starting point file for finding files that contain decorated classes. - * @returns A collection of files objects that hold info about the decorated classes and import - * information. - */ - findDecoratedFiles(entryPoint: ts.SourceFile): Map { - const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint); - const map = new Map(); - if (moduleSymbol) { - const exportedSymbols = - this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker)); - const exportedDeclarations = - exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined); - - const decoratedClasses = - exportedDeclarations - .map(declaration => { - if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) { - const name = declaration.name && ts.isIdentifier(declaration.name) ? - declaration.name.text : - undefined; - const decorators = this.getDecoratorsOfDeclaration(declaration); - return decorators && isDefined(name) ? - new DecoratedClass(name, declaration, decorators) : - undefined; - } - return undefined; - }) - .filter(isDefined); - - decoratedClasses.forEach(clazz => { - const file = clazz.declaration.getSourceFile(); - if (!map.has(file)) { - map.set(file, new DecoratedFile(file)); - } - map.get(file) !.decoratedClasses.push(clazz); - }); - } - return map; - } - - ///////////// Protected Helpers ///////////// - - /** - * Walk the AST looking for an assignment to the specified symbol. - * @param node The current node we are searching. - * @returns an expression that represents the value of the variable, or undefined if none can be - * found. - */ - protected findDecoratedVariableValue(node: ts.Node|undefined, symbol: ts.Symbol): - ts.CallExpression|null { - if (!node) { - return null; - } - if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - const left = node.left; - const right = node.right; - if (ts.isIdentifier(left) && this.checker.getSymbolAtLocation(left) === symbol) { - return (ts.isCallExpression(right) && getCalleeName(right) === '__decorate') ? right : null; - } - return this.findDecoratedVariableValue(right, symbol); - } - return node.forEachChild(node => this.findDecoratedVariableValue(node, symbol)) || null; - } - - /** - * Try to retrieve the symbol of a static property on a class. - * @param symbol the class whose property we are interested in. - * @param propertyName the name of static property. - * @returns the symbol if it is found or `undefined` if not. - */ - protected getStaticProperty(symbol: ts.Symbol, propertyName: ts.__String): ts.Symbol|undefined { - return symbol.exports && symbol.exports.get(propertyName); - } - - /** - * Get all class decorators for the given class, where the decorators are declared - * via a static property. For example: - * - * ``` - * class SomeDirective {} - * SomeDirective.decorators = [ - * { type: Directive, args: [{ selector: '[someDirective]' },] } - * ]; - * ``` - * - * @param decoratorsSymbol the property containing the decorators we want to get. - * @returns an array of decorators or null if none where found. - */ - protected getClassDecoratorsFromStaticProperty(decoratorsSymbol: ts.Symbol): Decorator[]|null { - const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; - if (decoratorsIdentifier && decoratorsIdentifier.parent) { - if (ts.isBinaryExpression(decoratorsIdentifier.parent) && - decoratorsIdentifier.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) { - // AST of the array of decorator values - const decoratorsArray = decoratorsIdentifier.parent.right; - return this.reflectDecorators(decoratorsArray) - .filter(decorator => this.isFromCore(decorator)); - } - } - return null; - } - - /** - * Get all class decorators for the given class, where the decorators are declared - * via the `__decorate` helper method. For example: - * - * ``` - * let SomeDirective = class SomeDirective {} - * SomeDirective = __decorate([ - * Directive({ selector: '[someDirective]' }), - * ], SomeDirective); - * ``` - * - * @param symbol the class whose decorators we want to get. - * @returns an array of decorators or null if none where found. - */ - protected getClassDecoratorsFromHelperCall(symbol: ts.Symbol): Decorator[]|null { - const decorators: Decorator[] = []; - const helperCalls = this.getHelperCallsForClass(symbol, '__decorate'); - helperCalls.forEach(helperCall => { - const {classDecorators} = - this.reflectDecoratorsFromHelperCall(helperCall, makeClassTargetFilter(symbol.name)); - classDecorators.filter(decorator => this.isFromCore(decorator)) - .forEach(decorator => decorators.push(decorator)); - }); - return decorators.length ? decorators : null; - } - - /** - * Get all the member decorators for the given class. - * @param classSymbol the class whose member decorators we are interested in. - * @returns a map whose keys are the name of the members and whose values are collections of - * decorators for the given member. - */ - protected getMemberDecorators(classSymbol: ts.Symbol): Map { - const decoratorsProperty = this.getStaticProperty(classSymbol, PROP_DECORATORS); - if (decoratorsProperty) { - return this.getMemberDecoratorsFromStaticProperty(decoratorsProperty); - } else { - return this.getMemberDecoratorsFromHelperCalls(classSymbol); - } - } - - /** - * Member decorators may be declared as static properties of the class: - * - * ``` - * SomeDirective.propDecorators = { - * "ngForOf": [{ type: Input },], - * "ngForTrackBy": [{ type: Input },], - * "ngForTemplate": [{ type: Input },], - * }; - * ``` - * - * @param decoratorsProperty the class whose member decorators we are interested in. - * @returns a map whose keys are the name of the members and whose values are collections of - * decorators for the given member. - */ - protected getMemberDecoratorsFromStaticProperty(decoratorsProperty: ts.Symbol): - Map { - const memberDecorators = new Map(); - // Symbol of the identifier for `SomeDirective.propDecorators`. - const propDecoratorsMap = getPropertyValueFromSymbol(decoratorsProperty); - if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { - const propertiesMap = reflectObjectLiteral(propDecoratorsMap); - propertiesMap.forEach((value, name) => { - const decorators = - this.reflectDecorators(value).filter(decorator => this.isFromCore(decorator)); - if (decorators.length) { - memberDecorators.set(name, decorators); - } - }); - } - return memberDecorators; - } - - /** - * Member decorators may be declared via helper call statements. - * - * ``` - * __decorate([ - * Input(), - * __metadata("design:type", String) - * ], SomeDirective.prototype, "input1", void 0); - * ``` - * - * @param classSymbol the class whose member decorators we are interested in. - * @returns a map whose keys are the name of the members and whose values are collections of - * decorators for the given member. - */ - protected getMemberDecoratorsFromHelperCalls(classSymbol: ts.Symbol): Map { - const memberDecoratorMap = new Map(); - const helperCalls = this.getHelperCallsForClass(classSymbol, '__decorate'); - helperCalls.forEach(helperCall => { - const {memberDecorators} = this.reflectDecoratorsFromHelperCall( - helperCall, makeMemberTargetFilter(classSymbol.name)); - memberDecorators.forEach((decorators, memberName) => { - if (memberName) { - const memberDecorators = memberDecoratorMap.get(memberName) || []; - const coreDecorators = decorators.filter(decorator => this.isFromCore(decorator)); - memberDecoratorMap.set(memberName, memberDecorators.concat(coreDecorators)); - } - }); - }); - return memberDecoratorMap; - } - - /** - * Extract decorator info from `__decorate` helper function calls. - * @param helperCall the call to a helper that may contain decorator calls - * @param targetFilter a function to filter out targets that we are not interested in. - * @returns a mapping from member name to decorators, where the key is either the name of the - * member or `undefined` if it refers to decorators on the class as a whole. - */ - protected reflectDecoratorsFromHelperCall( - helperCall: ts.CallExpression, targetFilter: TargetFilter): - {classDecorators: Decorator[], memberDecorators: Map} { - const classDecorators: Decorator[] = []; - const memberDecorators = new Map(); - - // First check that the `target` argument is correct - if (targetFilter(helperCall.arguments[1])) { - // Grab the `decorators` argument which should be an array of calls - const decoratorCalls = helperCall.arguments[0]; - if (decoratorCalls && ts.isArrayLiteralExpression(decoratorCalls)) { - decoratorCalls.elements.forEach(element => { - // We only care about those elements that are actual calls - if (ts.isCallExpression(element)) { - const decorator = this.reflectDecoratorCall(element); - if (decorator) { - const keyArg = helperCall.arguments[2]; - const keyName = keyArg && ts.isStringLiteral(keyArg) ? keyArg.text : undefined; - if (keyName === undefined) { - classDecorators.push(decorator); - } else { - const decorators = memberDecorators.get(keyName) || []; - decorators.push(decorator); - memberDecorators.set(keyName, decorators); - } - } - } - }); - } - } - return {classDecorators, memberDecorators}; - } - - /** - * Extract the decorator information from a call to a decorator as a function. - * This happens when the decorators has been used in a `__decorate` helper call. - * For example: - * - * ``` - * __decorate([ - * Directive({ selector: '[someDirective]' }), - * ], SomeDirective); - * ``` - * - * Here the `Directive` decorator is decorating `SomeDirective` and the options for - * the decorator are passed as arguments to the `Directive()` call. - * - * @param call the call to the decorator. - * @returns a decorator containing the reflected information, or null if the call - * is not a valid decorator call. - */ - protected reflectDecoratorCall(call: ts.CallExpression): Decorator|null { - // The call could be of the form `Decorator(...)` or `namespace_1.Decorator(...)` - const decoratorExpression = - ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression; - if (ts.isIdentifier(decoratorExpression)) { - // We found a decorator! - const decoratorIdentifier = decoratorExpression; - return { - name: decoratorIdentifier.text, - identifier: decoratorIdentifier, - import: this.getImportOfIdentifier(decoratorIdentifier), - node: call, - args: Array.from(call.arguments) - }; - } - return null; - } - - /** - * Check the given statement to see if it is a call to the specified helper function or null if - * not found. - * - * Matching statements will look like: `tslib_1.__decorate(...);`. - * @param statement the statement that may contain the call. - * @param helperName the name of the helper we are looking for. - * @returns the node that corresponds to the `__decorate(...)` call or null if the statement does - * not match. - */ - protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null { - if (ts.isExpressionStatement(statement)) { - const expression = - isAssignmentStatement(statement) ? statement.expression.right : statement.expression; - if (ts.isCallExpression(expression) && getCalleeName(expression) === helperName) { - return expression; - } - } - return null; - } - - - - /** - * Reflect over the given array node and extract decorator information from each element. - * - * This is used for decorators that are defined in static properties. For example: - * - * ``` - * SomeDirective.decorators = [ - * { type: Directive, args: [{ selector: '[someDirective]' },] } - * ]; - * ``` - * - * @param decoratorsArray an expression that contains decorator information. - * @returns an array of decorator info that was reflected from the array node. - */ - 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, - identifier: typeIdentifier, - import: this.getImportOfIdentifier(typeIdentifier), node, - args: getDecoratorArgs(node), - }); - } - } - }); - } - return decorators; - } - - /** - * Reflect over a symbol and extract the member information, combining it with the - * provided decorator information, and whether it is a static member. - * @param symbol the symbol for the member to reflect over. - * @param decorators an array of decorators associated with the member. - * @param isStatic true if this member is static, false if it is an instance property. - * @returns the reflected member information, or null if the symbol is not a member. - */ - 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; - } - - /** - * Get the parameter decorators of a class constructor. - * - * @param classSymbol the class whose parameter info we want to get. - * @param parameterNodes the array of TypeScript parameter nodes for this class's constructor. - * @returns an array of constructor parameter info objects. - */ - protected getConstructorParamInfo( - classSymbol: ts.Symbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] { - const paramsProperty = this.getStaticProperty(classSymbol, CONSTRUCTOR_PARAMS); - const paramInfo: ParamInfo[]|null = paramsProperty ? - this.getParamInfoFromStaticProperty(paramsProperty) : - this.getParamInfoFromHelperCall(classSymbol, parameterNodes); - - return parameterNodes.map((node, index) => { - const {decorators, type} = - paramInfo && paramInfo[index] ? paramInfo[index] : {decorators: null, type: null}; - const nameNode = node.name; - return {name: getNameText(nameNode), nameNode, type, decorators}; - }); - } - - /** - * Get the parameter type and decorators for the constructor of a class, - * where the information is stored on a static method of the class. - * - * Note that in ESM2015, the method is defined by an arrow function that returns an array of - * decorator and type information. - * - * ``` - * SomeDirective.ctorParameters = () => [ - * { type: ViewContainerRef, }, - * { type: TemplateRef, }, - * { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] }, - * ]; - * ``` - * - * @param paramDecoratorsProperty the property that holds the parameter info we want to get. - * @returns an array of objects containing the type and decorators for each parameter. - */ - protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null { - const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty); - if (paramDecorators && ts.isArrowFunction(paramDecorators)) { - if (ts.isArrayLiteralExpression(paramDecorators.body)) { - const elements = paramDecorators.body.elements; - return elements - .map( - element => - ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null) - .map(paramInfo => { - const type = paramInfo && paramInfo.get('type') || null; - const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; - const decorators = decoratorInfo && - this.reflectDecorators(decoratorInfo) - .filter(decorator => this.isFromCore(decorator)); - return {type, decorators}; - }); - } - } - return null; - } - - /** - * Get the parmeter type and decorators for a class where the information is stored on - * in calls to `__decorate` helpers. - * - * Reflect over the helpers to find the decorators and types about each of - * the class's constructor parameters. - * - * @param classSymbol the class whose parameter info we want to get. - * @param parameterNodes the array of TypeScript parameter nodes for this class's constructor. - * @returns an array of objects containing the type and decorators for each parameter. - */ - protected getParamInfoFromHelperCall( - classSymbol: ts.Symbol, parameterNodes: ts.ParameterDeclaration[]): ParamInfo[] { - const parameters: ParamInfo[] = parameterNodes.map(() => ({type: null, decorators: null})); - const helperCalls = this.getHelperCallsForClass(classSymbol, '__decorate'); - helperCalls.forEach(helperCall => { - const {classDecorators} = - this.reflectDecoratorsFromHelperCall(helperCall, makeClassTargetFilter(classSymbol.name)); - classDecorators.forEach(call => { - switch (call.name) { - case '__metadata': - const metadataArg = call.args && call.args[0]; - const typesArg = call.args && call.args[1]; - const isParamTypeDecorator = metadataArg && ts.isStringLiteral(metadataArg) && - metadataArg.text === 'design:paramtypes'; - const types = typesArg && ts.isArrayLiteralExpression(typesArg) && typesArg.elements; - if (isParamTypeDecorator && types) { - types.forEach((type, index) => parameters[index].type = type); - } - break; - case '__param': - const paramIndexArg = call.args && call.args[0]; - const decoratorCallArg = call.args && call.args[1]; - const paramIndex = paramIndexArg && ts.isNumericLiteral(paramIndexArg) ? - parseInt(paramIndexArg.text, 10) : - NaN; - const decorator = decoratorCallArg && ts.isCallExpression(decoratorCallArg) ? - this.reflectDecoratorCall(decoratorCallArg) : - null; - if (!isNaN(paramIndex) && decorator) { - const decorators = parameters[paramIndex].decorators = - parameters[paramIndex].decorators || []; - decorators.push(decorator); - } - break; - } - }); - }); - return parameters; - } - - /** - * Search statements related to the given class for calls to the specified helper. - * @param classSymbol the class whose helper calls we are interested in. - * @param helperName the name of the helper (e.g. `__decorate`) whose calls we are interested in. - * @returns an array of CallExpression nodes for each matching helper call. - */ - protected getHelperCallsForClass(classSymbol: ts.Symbol, helperName: string): - ts.CallExpression[] { - return this.getStatementsForClass(classSymbol) - .map(statement => this.getHelperCall(statement, helperName)) - .filter(isDefined); - } - - /** - * Find statements related to the given class that may contain calls to a helper. - * - * In ESM2015 code the helper calls are in the top level module, so we have to consider - * all the statements in the module. - * - * @param classSymbol the class whose helper calls we are interested in. - * @returns an array of statements that may contain helper calls. - */ - protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] { - return Array.from(classSymbol.valueDeclaration.getSourceFile().statements); - } - - /** - * Try to get the import info for this identifier as though it is a namespaced import. - * For example, if the identifier is the `__metadata` part of a property access chain like: - * - * ``` - * tslib_1.__metadata - * ``` - * - * then it might be that `tslib_1` is a namespace import such as: - * - * ``` - * import * as tslib_1 from 'tslib'; - * ``` - * @param id the TypeScript identifier to find the import info for. - * @returns The import info if this is a namespaced import or `null`. - */ - protected getImportOfNamespacedIdentifier(id: ts.Identifier): Import|null { - if (!(ts.isPropertyAccessExpression(id.parent) && id.parent.name === id)) { - return null; - } - - const namespaceIdentifier = getFarLeftIdentifier(id.parent); - const namespaceSymbol = - namespaceIdentifier && this.checker.getSymbolAtLocation(namespaceIdentifier); - const declaration = namespaceSymbol && namespaceSymbol.declarations.length === 1 ? - namespaceSymbol.declarations[0] : - null; - const namespaceDeclaration = - declaration && ts.isNamespaceImport(declaration) ? declaration : null; - if (!namespaceDeclaration) { - return null; - } - - const importDeclaration = namespaceDeclaration.parent.parent; - if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) { - // Should not happen as this would be invalid TypesScript - return null; - } - - return { - from: importDeclaration.moduleSpecifier.text, - name: id.text, - }; - } - - /** - * Test whether a decorator was imported from `@angular/core`. - * - * Is the decorator: - * * externally imported from `@angulare/core`? - * * the current hosted program is actually `@angular/core` and - * - relatively internally imported; or - * - not imported, from the current file. - * - * @param decorator the decorator to test. - */ - isFromCore(decorator: Decorator): boolean { - if (this.isCore) { - return !decorator.import || /^\./.test(decorator.import.from); - } else { - return !!decorator.import && decorator.import.from === '@angular/core'; - } - } -} - -///////////// Exported Helpers ///////////// - -export type ParamInfo = { - decorators: Decorator[] | null, - type: ts.Expression | null -}; - -/** - * A statement node that represents an assignment. - */ -export type AssignmentStatement = - ts.ExpressionStatement & {expression: {left: ts.Identifier, right: ts.Expression}}; - -/** - * Test whether a statement node is an assignment statement. - * @param statement the statement to test. - */ -export function isAssignmentStatement(statement: ts.Statement): statement is AssignmentStatement { - return ts.isExpressionStatement(statement) && isAssignment(statement.expression) && - ts.isIdentifier(statement.expression.left); -} - -export function isAssignment(expression: ts.Expression): - expression is ts.AssignmentExpression { - return ts.isBinaryExpression(expression) && - expression.operatorToken.kind === ts.SyntaxKind.EqualsToken; -} - -/** - * The type of a function that can be used to filter out helpers based on their target. - * This is used in `reflectDecoratorsFromHelperCall()`. - */ -export type TargetFilter = (target: ts.Expression) => boolean; - -/** - * Creates a function that tests whether the given expression is a class target. - * @param className the name of the class we want to target. - */ -export function makeClassTargetFilter(className: string): TargetFilter { - return (target: ts.Expression): boolean => ts.isIdentifier(target) && target.text === className; -} - -/** - * Creates a function that tests whether the given expression is a class member target. - * @param className the name of the class we want to target. - */ -export function makeMemberTargetFilter(className: string): TargetFilter { - return (target: ts.Expression): boolean => ts.isPropertyAccessExpression(target) && - ts.isIdentifier(target.expression) && target.expression.text === className && - target.name.text === 'prototype'; -} - -/** - * 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; -} - -/** - * A callee could be one of: `__decorate(...)` or `tslib_1.__decorate`. - */ -function getCalleeName(call: ts.CallExpression): string|null { - if (ts.isIdentifier(call.expression)) { - return call.expression.text; - } - if (ts.isPropertyAccessExpression(call.expression)) { - return call.expression.name.text; - } - return null; -} - -///////////// Internal Helpers ///////////// - -function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { - // The arguments of a decorator are held in the `args` property of its declaration object. - 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) : - []; -} - -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); -} - -/** - * Compute the left most identifier in a property access chain. E.g. the `a` of `a.b.c.d`. - * @param propertyAccess The starting property access expression from which we want to compute - * the left most identifier. - * @returns the left most identifier in the chain or `null` if it is not an identifier. - */ -function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier|null { - while (ts.isPropertyAccessExpression(propertyAccess.expression)) { - propertyAccess = propertyAccess.expression; - } - return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null; -} diff --git a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts index bd23db28cf..ac03b344fc 100644 --- a/packages/compiler-cli/src/ngcc/src/packages/transformer.ts +++ b/packages/compiler-cli/src/ngcc/src/packages/transformer.ts @@ -15,7 +15,6 @@ import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer'; import {DtsMapper} from '../host/dts_mapper'; import {Esm2015ReflectionHost} from '../host/esm2015_host'; import {Esm5ReflectionHost} from '../host/esm5_host'; -import {Fesm2015ReflectionHost} from '../host/fesm2015_host'; import {NgccReflectionHost} from '../host/ngcc_host'; import {Esm2015Renderer} from '../rendering/esm2015_renderer'; import {Esm5Renderer} from '../rendering/esm5_renderer'; @@ -111,9 +110,8 @@ export class Transformer { NgccReflectionHost { switch (format) { case 'esm2015': - return new Esm2015ReflectionHost(isCore, program.getTypeChecker(), dtsMapper); case 'fesm2015': - return new Fesm2015ReflectionHost(isCore, program.getTypeChecker()); + return new Esm2015ReflectionHost(isCore, program.getTypeChecker(), dtsMapper); case 'esm5': case 'fesm5': return new Esm5ReflectionHost(isCore, program.getTypeChecker()); diff --git a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts index 42133d1240..c1abe7709e 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/decoration_analyzer_spec.ts @@ -10,7 +10,7 @@ import * as ts from 'typescript'; import {Decorator} from '../../../ngtsc/host'; import {DecoratorHandler} from '../../../ngtsc/transform'; import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {makeProgram} from '../helpers/utils'; @@ -57,7 +57,7 @@ describe('DecorationAnalyzer', () => { beforeEach(() => { program = makeProgram(TEST_PROGRAM); const analyzer = new DecorationAnalyzer( - program.getTypeChecker(), new Fesm2015ReflectionHost(false, program.getTypeChecker()), + program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()), [''], false); testHandler = createTestHandler(); analyzer.handlers = [testHandler]; diff --git a/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts b/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts index 485fa10d17..b97daa07ee 100644 --- a/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/analysis/switch_marker_analyzer_spec.ts @@ -8,7 +8,7 @@ import * as ts from 'typescript'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {makeProgram} from '../helpers/utils'; const TEST_PROGRAM = [ @@ -47,7 +47,7 @@ describe('SwitchMarkerAnalyzer', () => { describe('analyzeProgram()', () => { it('should check for switchable markers in all the files of the program', () => { const program = makeProgram(...TEST_PROGRAM); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const analyzer = new SwitchMarkerAnalyzer(host); const analysis = analyzer.analyzeProgram(program); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts similarity index 90% rename from packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts rename to packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts index 6f9d77f1e9..51b2120b8f 100644 --- a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_import_helper_spec.ts @@ -9,7 +9,7 @@ import * as ts from 'typescript'; import {ClassMemberKind, Import} from '../../../ngtsc/host'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {convertToDirectTsLibImport, getDeclaration, makeProgram} from '../helpers/utils'; const FILES = [ @@ -104,7 +104,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('getDecoratorsOfDeclaration()', () => { it('should find the decorators on a class', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const decorators = host.getDecoratorsOfDeclaration(classNode) !; @@ -121,14 +121,14 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { }); it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') .and.callFake( (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? {from: '@angular/core', name: 'Directive'} : {}); const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); @@ -143,7 +143,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should support decorators being used inside @angular/core', () => { const program = makeProgram(fileSystem.files[1]); - const host = new Fesm2015ReflectionHost(true, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(true, program.getTypeChecker()); const classNode = getDeclaration( program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); @@ -164,7 +164,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('getMembersOfClass()', () => { it('should find decorated members on a class', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const members = host.getMembersOfClass(classNode); @@ -182,7 +182,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should find non decorated properties on a class', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const members = host.getMembersOfClass(classNode); @@ -196,7 +196,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should find static methods on a class', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const members = host.getMembersOfClass(classNode); @@ -209,7 +209,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should find static properties on a class', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); @@ -223,10 +223,10 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should use `getImportOfIdentifier()` to retrieve import info', () => { const spy = - spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); + spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); @@ -237,7 +237,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should support decorators being used inside @angular/core', () => { const program = makeProgram(fileSystem.files[1]); - const host = new Fesm2015ReflectionHost(true, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(true, program.getTypeChecker()); const classNode = getDeclaration( program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); @@ -253,7 +253,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('getConstructorParameters', () => { it('should find the decorated constructor parameters', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const parameters = host.getConstructorParameters(classNode); @@ -270,11 +270,11 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('(returned parameters `decorators`)', () => { it('should use `getImportOfIdentifier()` to retrieve import info', () => { const mockImportInfo = {} as Import; - const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') .and.returnValue(mockImportInfo); const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const parameters = host.getConstructorParameters(classNode); @@ -292,7 +292,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('getDeclarationOfIdentifier', () => { it('should return the declaration of a locally defined identifier', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const ctrDecorators = host.getConstructorParameters(classNode) !; @@ -308,7 +308,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should return the declaration of an externally defined identifier', () => { const program = makeProgram(fileSystem.files[0]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const classNode = getDeclaration( program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; @@ -331,7 +331,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { describe('getVariableValue', () => { it('should find the "actual" declaration of an aliased variable identifier', () => { const program = makeProgram(fileSystem.files[2]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const ngModuleRef = findVariableDeclaration( program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); @@ -346,7 +346,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should return null if the variable has no assignment', () => { const program = makeProgram(fileSystem.files[2]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const missingValue = findVariableDeclaration( program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); const value = host.getVariableValue(missingValue !); @@ -355,7 +355,7 @@ describe('Fesm2015ReflectionHost [import helper style]', () => { it('should return null if the variable is not assigned from a call to __decorate', () => { const program = makeProgram(fileSystem.files[2]); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const nonDecoratedVar = findVariableDeclaration( program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); const value = host.getVariableValue(nonDecoratedVar !); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts index 8a2d9a4918..7e8252764e 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm2015_host_spec.ts @@ -8,30 +8,374 @@ import * as fs from 'fs'; import * as ts from 'typescript'; - +import {ClassMemberKind, Import} from '../../../ngtsc/host'; import {DtsMapper} from '../../src/host/dts_mapper'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {getDeclaration, makeProgram} from '../helpers/utils'; -const CLASSES = [ +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: ` + import {Directive} from '@angular/core'; + class NotArrayLiteral { + } + NotArrayLiteral.decorators = () => [ + { type: Directive, args: [{ selector: '[ignored]' },] }, + ]; + + class NotObjectLiteral { + } + NotObjectLiteral.decorators = [ + "This is not an object literal", + { type: Directive }, + ]; + + class NoTypeProperty { + } + NoTypeProperty.decorators = [ + { notType: Directive }, + { type: Directive }, + ]; + + class NotIdentifier { + } + NotIdentifier.decorators = [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: Directive }, + ]; + `, +}; + +const INVALID_DECORATOR_ARGS_FILE = { + name: '/invalid_decorator_args.js', + contents: ` + import {Directive} from '@angular/core'; + class NoArgsProperty { + } + NoArgsProperty.decorators = [ + { type: Directive }, + ]; + + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.decorators = [ + { type: Directive, args }, + ]; + + class NotArrayLiteral { + } + NotArrayLiteral.decorators = [ + { type: Directive, args: () => [{ selector: '[ignored]' },] }, + ]; + `, +}; + +const INVALID_PROP_DECORATORS_FILE = { + name: '/invalid_prop_decorators.js', + contents: ` + import {Input} from '@angular/core'; + class NotObjectLiteral { + } + NotObjectLiteral.propDecorators = () => ({ + "prop": [{ type: Input },] + }); + + class NotObjectLiteralProp { + } + NotObjectLiteralProp.propDecorators = { + "prop": [ + "This is not an object literal", + { type: Input }, + ] + }; + + class NoTypeProperty { + } + NoTypeProperty.propDecorators = { + "prop": [ + { notType: Input }, + { type: Input }, + ] + }; + + class NotIdentifier { + } + NotIdentifier.propDecorators = { + "prop": [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: Input }, + ] + }; + `, +}; + +const INVALID_PROP_DECORATOR_ARGS_FILE = { + name: '/invalid_prop_decorator_args.js', + contents: ` + import {Input} from '@angular/core'; + class NoArgsProperty { + } + NoArgsProperty.propDecorators = { + "prop": [{ type: Input },] + }; + + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + } + NoPropertyAssignment.propDecorators = { + "prop": [{ type: Input, args },] + }; + + class NotArrayLiteral { + } + NotArrayLiteral.propDecorators = { + "prop": [{ type: Input, args: () => [{ selector: '[ignored]' },] },], + }; + `, +}; + +const INVALID_CTOR_DECORATORS_FILE = { + name: '/invalid_ctor_decorators.js', + contents: ` + import {Inject} from '@angular/core'; + class NoParameters { + constructor() { + } + } + + const NotFromCoreDecorator = {}; + class NotFromCore { + constructor(arg1) { + } + } + NotFromCore.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: NotFromCoreDecorator },] }, + ] + + class NotArrowFunction { + constructor(arg1) { + } + } + NotArrowFunction.ctorParameters = function() { + return { type: 'ParamType', decorators: [{ type: Inject },] }; + }; + + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => 'StringsAreNotArrayLiterals'; + + class NotObjectLiteral { + constructor(arg1, arg2) { + } + } + NotObjectLiteral.ctorParameters = () => [ + "This is not an object literal", + { type: 'ParamType', decorators: [{ type: Inject },] }, + ]; + + class NoTypeProperty { + constructor(arg1, arg2) { + } + } + NoTypeProperty.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { notType: Inject }, + { type: Inject }, + ] + }, + ]; + + class NotIdentifier { + constructor(arg1, arg2) { + } + } + NotIdentifier.ctorParameters = () => [ + { + type: 'ParamType', + decorators: [ + { type: 'StringsLiteralsAreNotIdentifiers' }, + { type: Inject }, + ] + }, + ]; + `, +}; + +const INVALID_CTOR_DECORATOR_ARGS_FILE = { + name: '/invalid_ctor_decorator_args.js', + contents: ` + import {Inject} from '@angular/core'; + class NoArgsProperty { + constructor(arg1) { + } + } + NoArgsProperty.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: Inject },] }, + ]; + + const args = [{ selector: '[ignored]' },]; + class NoPropertyAssignment { + constructor(arg1) { + } + } + NoPropertyAssignment.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: Inject, args },] }, + ]; + + class NotArrayLiteral { + constructor(arg1) { + } + } + NotArrayLiteral.ctorParameters = () => [ + { type: 'ParamType', decorators: [{ type: Inject, args: () => [{ selector: '[ignored]' },] },] }, + ]; + `, +}; + +const IMPORTS_FILES = [ { - name: '/src/class.js', + name: '/a.js', contents: ` - export class NoTypeParam {} - export class OneTypeParam {} - export class TwoTypeParams {} + export const a = 'a'; `, }, { - name: '/typings/class.d.ts', + name: '/b.js', contents: ` - export class NoTypeParam {} - export class OneTypeParam {} - export class TwoTypeParams {} + 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 {} + `, + }, +]; + +const FUNCTION_BODY_FILE = { + name: '/function_body.js', + contents: ` + function foo(x) { + return x; + } + function bar(x, y = 42) { + return x + y; + } + function baz(x) { + let y; + if (y === void 0) { y = 42; } + return x; + } + let y; + function qux(x) { + if (x === void 0) { y = 42; } + return y; + } + function moo() { + let x; + if (x === void 0) { x = 42; } + return x; + } + let x; + function juu() { + if (x === void 0) { x = 42; } + return x; + } + ` +}; + const MARKER_FILE = { name: '/marker.js', contents: ` @@ -83,12 +427,768 @@ const DECORATED_FILES = [ } ]; -describe('Esm2015ReflectionHost', () => { +const ARITY_CLASSES = [ + { + name: '/src/class.js', + contents: ` + export class NoTypeParam {} + export class OneTypeParam {} + export class TwoTypeParams {} + `, + }, + { + name: '/typings/class.d.ts', + contents: ` + export class NoTypeParam {} + export class OneTypeParam {} + export class TwoTypeParams {} + `, + }, +]; + +describe('Fesm2015ReflectionHost', () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(false, 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(false, 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(false, 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(false, 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(false, 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: 'Directive'})); + }); + + it('should ignore decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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: 'Directive'})); + }); + + it('should ignore decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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: 'Directive'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = { from: '@angular/core' } as Import; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(false, 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(false, 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('Directive'); + 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(false, 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('Directive'); + 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(false, 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('Directive'); + 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(false, 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(false, 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(false, program.getTypeChecker()); + const classNode = + getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); + 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(false, 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(false, 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(false, 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(false, 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(false, 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: 'Input'})); + }); + + it('should ignore prop decorator elements that have no `type` property', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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: 'Input'})); + }); + + it('should ignore prop decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_PROP_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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: 'Input'})); + }); + + 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: '@angular/core'}; + }); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(false, 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 member = members.find(member => member.name === 'input1') !; + expect(member.decorators !.length).toBe(1); + expect(member.decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'}); + }); + + 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(false, 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('Input'); + 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(false, 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('Input'); + 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(false, 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('Input'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getConstructorParameters()', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(false, 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(false, 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(false, 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(false, 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(false, 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 decorators that are not imported from core', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); + const classNode = getDeclaration( + program, INVALID_CTOR_DECORATORS_FILE.name, 'NotFromCore', ts.isClassDeclaration); + const parameters = host.getConstructorParameters(classNode) !; + + expect(parameters.length).toBe(1); + expect(parameters[0]).toEqual(jasmine.objectContaining({ + name: 'arg1', + decorators: [], + })); + }); + + it('should ignore `ctorParameters` if it is not an arrow function', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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(false, 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(false, 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(false, 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: 'Inject'})); + }); + + it('should ignore param decorator elements whose `type` value is not an identifier', () => { + const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); + const host = new Esm2015ReflectionHost(false, 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: 'Inject'})); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo: Import = {name: 'mock', from: '@angular/core'}; + const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(SOME_DIRECTIVE_FILE); + const host = new Esm2015ReflectionHost(false, 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(false, 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('Inject'); + 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(false, 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('Inject'); + 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(false, 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('Inject'); + expect(decorators[0].args).toEqual([]); + }); + }); + }); + + describe('getDefinitionOfFunction()', () => { + it('should return an object describing the function declaration passed as an argument', () => { + const program = makeProgram(FUNCTION_BODY_FILE); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); + + const fooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !; + const fooDef = host.getDefinitionOfFunction(fooNode); + expect(fooDef.node).toBe(fooNode); + expect(fooDef.body !.length).toEqual(1); + expect(fooDef.body ![0].getText()).toEqual(`return x;`); + expect(fooDef.parameters.length).toEqual(1); + expect(fooDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + + const barNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !; + const barDef = host.getDefinitionOfFunction(barNode); + expect(barDef.node).toBe(barNode); + expect(barDef.body !.length).toEqual(1); + expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); + expect(barDef.body ![0].getText()).toEqual(`return x + y;`); + expect(barDef.parameters.length).toEqual(2); + expect(barDef.parameters[0].name).toEqual('x'); + expect(fooDef.parameters[0].initializer).toBe(null); + expect(barDef.parameters[1].name).toEqual('y'); + expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); + + const bazNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !; + const bazDef = host.getDefinitionOfFunction(bazNode); + expect(bazDef.node).toBe(bazNode); + expect(bazDef.body !.length).toEqual(3); + expect(bazDef.parameters.length).toEqual(1); + expect(bazDef.parameters[0].name).toEqual('x'); + expect(bazDef.parameters[0].initializer).toBe(null); + + const quxNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !; + const quxDef = host.getDefinitionOfFunction(quxNode); + expect(quxDef.node).toBe(quxNode); + expect(quxDef.body !.length).toEqual(2); + expect(quxDef.parameters.length).toEqual(1); + expect(quxDef.parameters[0].name).toEqual('x'); + expect(quxDef.parameters[0].initializer).toBe(null); + + const mooNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'moo', ts.isFunctionDeclaration) !; + const mooDef = host.getDefinitionOfFunction(mooNode); + expect(mooDef.node).toBe(mooNode); + expect(mooDef.body !.length).toEqual(3); + expect(mooDef.parameters).toEqual([]); + + const juuNode = + getDeclaration(program, FUNCTION_BODY_FILE.name, 'juu', ts.isFunctionDeclaration) !; + const juuDef = host.getDefinitionOfFunction(juuNode); + expect(juuDef.node).toBe(juuNode); + expect(juuDef.body !.length).toEqual(2); + expect(juuDef.parameters).toEqual([]); + }); + }); + + describe('getImportOfIdentifier()', () => { + it('should find the import of an identifier', () => { + const program = makeProgram(...IMPORTS_FILES); + const host = new Esm2015ReflectionHost(false, 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(false, 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(false, 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(false, 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(false, 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(false, 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(false, 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(false, program.getTypeChecker()); + const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); + expect(host.isClass(node)).toBe(false); + }); + }); + describe('getGenericArityOfClass()', () => { it('should properly count type parameters', () => { // Mock out reading the `d.ts` file from disk - const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(CLASSES[1].contents); - const program = makeProgram(CLASSES[0]); + const readFileSyncSpy = spyOn(fs, 'readFileSync').and.returnValue(ARITY_CLASSES[1].contents); + const program = makeProgram(ARITY_CLASSES[0]); const dtsMapper = new DtsMapper('/src', '/typings'); const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsMapper); diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts index 83d31bba61..b3ed33794d 100644 --- a/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_spec.ts @@ -9,8 +9,8 @@ import * as ts from 'typescript'; import {ClassMemberKind, Import} from '../../../ngtsc/host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; import {getDeclaration, makeProgram} from '../helpers/utils'; const SOME_DIRECTIVE_FILE = { @@ -1153,7 +1153,7 @@ describe('Esm5ReflectionHost', () => { let superGetClassSymbolSpy: jasmine.Spy; beforeEach(() => { - superGetClassSymbolSpy = spyOn(Fesm2015ReflectionHost.prototype, 'getClassSymbol'); + superGetClassSymbolSpy = spyOn(Esm2015ReflectionHost.prototype, 'getClassSymbol'); }); it('should return the class symbol returned by the superclass (if any)', () => { @@ -1221,7 +1221,7 @@ describe('Esm5ReflectionHost', () => { host = new Esm5ReflectionHost(false, null as any); mockNode = {} as any; - superIsClassSpy = spyOn(Fesm2015ReflectionHost.prototype, 'isClass'); + superIsClassSpy = spyOn(Esm2015ReflectionHost.prototype, 'isClass'); getClassSymbolSpy = spyOn(Esm5ReflectionHost.prototype, 'getClassSymbol'); }); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts deleted file mode 100644 index c848708489..0000000000 --- a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_spec.ts +++ /dev/null @@ -1,1212 +0,0 @@ -/** - * @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 {Fesm2015ReflectionHost} from '../../src/host/fesm2015_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: ` - import {Directive} from '@angular/core'; - class NotArrayLiteral { - } - NotArrayLiteral.decorators = () => [ - { type: Directive, args: [{ selector: '[ignored]' },] }, - ]; - - class NotObjectLiteral { - } - NotObjectLiteral.decorators = [ - "This is not an object literal", - { type: Directive }, - ]; - - class NoTypeProperty { - } - NoTypeProperty.decorators = [ - { notType: Directive }, - { type: Directive }, - ]; - - class NotIdentifier { - } - NotIdentifier.decorators = [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: Directive }, - ]; - `, -}; - -const INVALID_DECORATOR_ARGS_FILE = { - name: '/invalid_decorator_args.js', - contents: ` - import {Directive} from '@angular/core'; - class NoArgsProperty { - } - NoArgsProperty.decorators = [ - { type: Directive }, - ]; - - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - } - NoPropertyAssignment.decorators = [ - { type: Directive, args }, - ]; - - class NotArrayLiteral { - } - NotArrayLiteral.decorators = [ - { type: Directive, args: () => [{ selector: '[ignored]' },] }, - ]; - `, -}; - -const INVALID_PROP_DECORATORS_FILE = { - name: '/invalid_prop_decorators.js', - contents: ` - import {Input} from '@angular/core'; - class NotObjectLiteral { - } - NotObjectLiteral.propDecorators = () => ({ - "prop": [{ type: Input },] - }); - - class NotObjectLiteralProp { - } - NotObjectLiteralProp.propDecorators = { - "prop": [ - "This is not an object literal", - { type: Input }, - ] - }; - - class NoTypeProperty { - } - NoTypeProperty.propDecorators = { - "prop": [ - { notType: Input }, - { type: Input }, - ] - }; - - class NotIdentifier { - } - NotIdentifier.propDecorators = { - "prop": [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: Input }, - ] - }; - `, -}; - -const INVALID_PROP_DECORATOR_ARGS_FILE = { - name: '/invalid_prop_decorator_args.js', - contents: ` - import {Input} from '@angular/core'; - class NoArgsProperty { - } - NoArgsProperty.propDecorators = { - "prop": [{ type: Input },] - }; - - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - } - NoPropertyAssignment.propDecorators = { - "prop": [{ type: Input, args },] - }; - - class NotArrayLiteral { - } - NotArrayLiteral.propDecorators = { - "prop": [{ type: Input, args: () => [{ selector: '[ignored]' },] },], - }; - `, -}; - -const INVALID_CTOR_DECORATORS_FILE = { - name: '/invalid_ctor_decorators.js', - contents: ` - import {Inject} from '@angular/core'; - class NoParameters { - constructor() { - } - } - - const NotFromCoreDecorator = {}; - class NotFromCore { - constructor(arg1) { - } - } - NotFromCore.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: NotFromCoreDecorator },] }, - ] - - class NotArrowFunction { - constructor(arg1) { - } - } - NotArrowFunction.ctorParameters = function() { - return { type: 'ParamType', decorators: [{ type: Inject },] }; - }; - - class NotArrayLiteral { - constructor(arg1) { - } - } - NotArrayLiteral.ctorParameters = () => 'StringsAreNotArrayLiterals'; - - class NotObjectLiteral { - constructor(arg1, arg2) { - } - } - NotObjectLiteral.ctorParameters = () => [ - "This is not an object literal", - { type: 'ParamType', decorators: [{ type: Inject },] }, - ]; - - class NoTypeProperty { - constructor(arg1, arg2) { - } - } - NoTypeProperty.ctorParameters = () => [ - { - type: 'ParamType', - decorators: [ - { notType: Inject }, - { type: Inject }, - ] - }, - ]; - - class NotIdentifier { - constructor(arg1, arg2) { - } - } - NotIdentifier.ctorParameters = () => [ - { - type: 'ParamType', - decorators: [ - { type: 'StringsLiteralsAreNotIdentifiers' }, - { type: Inject }, - ] - }, - ]; - `, -}; - -const INVALID_CTOR_DECORATOR_ARGS_FILE = { - name: '/invalid_ctor_decorator_args.js', - contents: ` - import {Inject} from '@angular/core'; - class NoArgsProperty { - constructor(arg1) { - } - } - NoArgsProperty.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: Inject },] }, - ]; - - const args = [{ selector: '[ignored]' },]; - class NoPropertyAssignment { - constructor(arg1) { - } - } - NoPropertyAssignment.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: Inject, args },] }, - ]; - - class NotArrayLiteral { - constructor(arg1) { - } - } - NotArrayLiteral.ctorParameters = () => [ - { type: 'ParamType', decorators: [{ type: Inject, 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 {} - `, - }, -]; - -const FUNCTION_BODY_FILE = { - name: '/function_body.js', - contents: ` - function foo(x) { - return x; - } - function bar(x, y = 42) { - return x + y; - } - function baz(x) { - let y; - if (y === void 0) { y = 42; } - return x; - } - let y; - function qux(x) { - if (x === void 0) { y = 42; } - return y; - } - function moo() { - let x; - if (x === void 0) { x = 42; } - return x; - } - let x; - function juu() { - if (x === void 0) { x = 42; } - return x; - } - ` -}; - -const MARKER_FILE = { - name: '/marker.js', - contents: ` - var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__; - - function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) { - var compilerFactory = injector.get(CompilerFactory); - var compiler = compilerFactory.createCompiler([options]); - return compiler.compileModuleAsync(moduleType); - } - - function compileNgModuleFactory__POST_R3__(injector, options, moduleType) { - ngDevMode && assertNgModuleType(moduleType); - return Promise.resolve(new R3NgModuleFactory(moduleType)); - } - ` -}; - -const DECORATED_FILE = { - name: '/primary.js', - contents: ` - import {Directive} from '@angular/core'; - class A {} - A.decorators = [ - { type: Directive, args: [{ selector: '[a]' }] } - ]; - - class B {} - B.decorators = [ - { type: Directive, args: [{ selector: '[b]' }] } - ]; - - function x() {} - - function y() {} - - class C {} - - let D = class D {} - D = tslib_1.__decorate([ - Directive({ selector: '[d]' }), - OtherD() - ], D); - export {D}; - - export { A, x, C }; - ` -}; - -describe('Fesm2015ReflectionHost', () => { - - describe('getDecoratorsOfDeclaration()', () => { - it('should find the decorators on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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: 'Directive'})); - }); - - it('should ignore decorator elements that have no `type` property', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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: 'Directive'})); - }); - - it('should ignore decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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: 'Directive'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo = { from: '@angular/core' } as Import; - const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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('Directive'); - 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 Fesm2015ReflectionHost(false, 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('Directive'); - 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 Fesm2015ReflectionHost(false, 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('Directive'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getMembersOfClass()', () => { - it('should find decorated properties on a class', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, program.getTypeChecker()); - const classNode = - getDeclaration(program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', ts.isClassDeclaration); - 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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: 'Input'})); - }); - - it('should ignore prop decorator elements that have no `type` property', () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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: 'Input'})); - }); - - it('should ignore prop decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_PROP_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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: 'Input'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - let callCount = 0; - const spy = - spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => { - callCount++; - return {name: `name${callCount}`, from: '@angular/core'}; - }); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 member = members.find(member => member.name === 'input1') !; - expect(member.decorators !.length).toBe(1); - expect(member.decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'}); - }); - - 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 Fesm2015ReflectionHost(false, 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('Input'); - 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 Fesm2015ReflectionHost(false, 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('Input'); - 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 Fesm2015ReflectionHost(false, 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('Input'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getConstructorParameters()', () => { - it('should find the decorated constructor parameters', () => { - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 decorators that are not imported from core', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const classNode = getDeclaration( - program, INVALID_CTOR_DECORATORS_FILE.name, 'NotFromCore', ts.isClassDeclaration); - const parameters = host.getConstructorParameters(classNode) !; - - expect(parameters.length).toBe(1); - expect(parameters[0]).toEqual(jasmine.objectContaining({ - name: 'arg1', - decorators: [], - })); - }); - - it('should ignore `ctorParameters` if it is not an arrow function', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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: 'Inject'})); - }); - - it('should ignore param decorator elements whose `type` value is not an identifier', () => { - const program = makeProgram(INVALID_CTOR_DECORATORS_FILE); - const host = new Fesm2015ReflectionHost(false, 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: 'Inject'})); - }); - - it('should use `getImportOfIdentifier()` to retrieve import info', () => { - const mockImportInfo: Import = {name: 'mock', from: '@angular/core'}; - const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') - .and.returnValue(mockImportInfo); - - const program = makeProgram(SOME_DIRECTIVE_FILE); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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('Inject'); - 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 Fesm2015ReflectionHost(false, 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('Inject'); - 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 Fesm2015ReflectionHost(false, 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('Inject'); - expect(decorators[0].args).toEqual([]); - }); - }); - }); - - describe('getDefinitionOfFunction()', () => { - it('should return an object describing the function declaration passed as an argument', () => { - const program = makeProgram(FUNCTION_BODY_FILE); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - - const fooNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'foo', ts.isFunctionDeclaration) !; - const fooDef = host.getDefinitionOfFunction(fooNode); - expect(fooDef.node).toBe(fooNode); - expect(fooDef.body !.length).toEqual(1); - expect(fooDef.body ![0].getText()).toEqual(`return x;`); - expect(fooDef.parameters.length).toEqual(1); - expect(fooDef.parameters[0].name).toEqual('x'); - expect(fooDef.parameters[0].initializer).toBe(null); - - const barNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'bar', ts.isFunctionDeclaration) !; - const barDef = host.getDefinitionOfFunction(barNode); - expect(barDef.node).toBe(barNode); - expect(barDef.body !.length).toEqual(1); - expect(ts.isReturnStatement(barDef.body ![0])).toBeTruthy(); - expect(barDef.body ![0].getText()).toEqual(`return x + y;`); - expect(barDef.parameters.length).toEqual(2); - expect(barDef.parameters[0].name).toEqual('x'); - expect(fooDef.parameters[0].initializer).toBe(null); - expect(barDef.parameters[1].name).toEqual('y'); - expect(barDef.parameters[1].initializer !.getText()).toEqual('42'); - - const bazNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'baz', ts.isFunctionDeclaration) !; - const bazDef = host.getDefinitionOfFunction(bazNode); - expect(bazDef.node).toBe(bazNode); - expect(bazDef.body !.length).toEqual(3); - expect(bazDef.parameters.length).toEqual(1); - expect(bazDef.parameters[0].name).toEqual('x'); - expect(bazDef.parameters[0].initializer).toBe(null); - - const quxNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'qux', ts.isFunctionDeclaration) !; - const quxDef = host.getDefinitionOfFunction(quxNode); - expect(quxDef.node).toBe(quxNode); - expect(quxDef.body !.length).toEqual(2); - expect(quxDef.parameters.length).toEqual(1); - expect(quxDef.parameters[0].name).toEqual('x'); - expect(quxDef.parameters[0].initializer).toBe(null); - - const mooNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'moo', ts.isFunctionDeclaration) !; - const mooDef = host.getDefinitionOfFunction(mooNode); - expect(mooDef.node).toBe(mooNode); - expect(mooDef.body !.length).toEqual(3); - expect(mooDef.parameters).toEqual([]); - - const juuNode = - getDeclaration(program, FUNCTION_BODY_FILE.name, 'juu', ts.isFunctionDeclaration) !; - const juuDef = host.getDefinitionOfFunction(juuNode); - expect(juuDef.node).toBe(juuNode); - expect(juuDef.body !.length).toEqual(2); - expect(juuDef.parameters).toEqual([]); - }); - }); - - describe('getImportOfIdentifier()', () => { - it('should find the import of an identifier', () => { - const program = makeProgram(...IMPORTS_FILES); - const host = new Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, 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 Fesm2015ReflectionHost(false, program.getTypeChecker()); - const node = getDeclaration(program, FOO_FUNCTION_FILE.name, 'foo', ts.isFunctionDeclaration); - expect(host.isClass(node)).toBe(false); - }); - }); - - describe('getGenericArityOfClass()', () => { - it('should return 0 for a basic class', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const node = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isClassDeclaration); - expect(host.getGenericArityOfClass(node)).toBe(0); - }); - }); - - describe('getSwitchableDeclarations()', () => { - it('should return a collection of all the switchable variable declarations in the given module', - () => { - const program = makeProgram(MARKER_FILE); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const file = program.getSourceFile(MARKER_FILE.name) !; - const declarations = host.getSwitchableDeclarations(file); - expect(declarations.map(d => [d.name.getText(), d.initializer !.getText()])).toEqual([ - ['compileNgModuleFactory', 'compileNgModuleFactory__PRE_R3__'] - ]); - }); - }); - - describe('findDecoratedFiles()', () => { - it('should return an array of objects for each file that has exported and decorated classes', - () => { - const program = makeProgram(DECORATED_FILE); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); - const primaryFile = program.getSourceFile(DECORATED_FILE.name) !; - const decoratedFiles = host.findDecoratedFiles(primaryFile); - - expect(decoratedFiles.size).toEqual(1); - const decoratedClasses = decoratedFiles.get(primaryFile) !.decoratedClasses; - expect(decoratedClasses.length).toEqual(2); - - const decoratedClassA = decoratedClasses.find(c => c.name === 'A') !; - expect(decoratedClassA.decorators.map(decorator => decorator.name)).toEqual(['Directive']); - expect(decoratedClassA.decorators.map( - decorator => decorator.args && decorator.args.map(arg => arg.getText()))) - .toEqual([[`{ selector: '[a]' }`]]); - - const decoratedClassD = decoratedClasses.find(c => c.name === 'D') !; - expect(decoratedClassD.decorators.map(decorator => decorator.name)).toEqual(['Directive']); - expect(decoratedClassD.decorators.map( - decorator => decorator.args && decorator.args.map(arg => arg.getText()))) - .toEqual([[`{ selector: '[d]' }`]]); - }); - }); -}); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts index c57b4f43b5..a7c533a969 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/esm2015_renderer_spec.ts @@ -13,7 +13,7 @@ import {makeProgram} from '../helpers/utils'; import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; import {DtsMapper} from '../../src/host/dts_mapper'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015Renderer} from '../../src/rendering/esm2015_renderer'; function setup(file: {name: string, contents: string}, transformDts: boolean = false) { @@ -21,7 +21,7 @@ function setup(file: {name: string, contents: string}, transformDts: boolean = f const dtsMapper = new DtsMapper(dir, dir); const program = makeProgram(file); const sourceFile = program.getSourceFile(file.name) !; - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const decorationAnalyses = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program); diff --git a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts index 6603aa6ad0..283e46eedb 100644 --- a/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts +++ b/packages/compiler-cli/src/ngcc/test/rendering/renderer_spec.ts @@ -13,11 +13,11 @@ import {fromObject, generateMapFileComment} from 'convert-source-map'; import {makeProgram} from '../helpers/utils'; import {AnalyzedClass, DecorationAnalyzer, DecorationAnalyses} from '../../src/analysis/decoration_analyzer'; import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer'; -import {Fesm2015ReflectionHost} from '../../src/host/fesm2015_host'; +import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Renderer} from '../../src/rendering/renderer'; class TestRenderer extends Renderer { - constructor(host: Fesm2015ReflectionHost) { super(host, false, null, '/src', '/dist'); } + constructor(host: Esm2015ReflectionHost) { super(host, false, null, '/src', '/dist'); } addImports(output: MagicString, imports: {name: string, as: string}[]) { output.prepend('\n// ADD IMPORTS\n'); } @@ -37,7 +37,7 @@ class TestRenderer extends Renderer { function createTestRenderer(file: {name: string, contents: string}) { const program = makeProgram(file); - const host = new Fesm2015ReflectionHost(false, program.getTypeChecker()); + const host = new Esm2015ReflectionHost(false, program.getTypeChecker()); const decorationAnalyses = new DecorationAnalyzer(program.getTypeChecker(), host, [''], false).analyzeProgram(program); const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);