From 7d08722e804f7eb5ee04184898c177cf36e057e2 Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Sun, 30 Sep 2018 20:53:25 +0100 Subject: [PATCH] fix(ivy): support `tsutils.__decorate` decorator declarations in ngcc (#26236) The most recent Angular distributions have begun to use __decorate instead of Class.decorators. This prevents `ngcc` from recognizing the classes and then fails to perform the transform to ivy format. Example: ``` var ApplicationModule = /** @class */ (function () { // Inject ApplicationRef to make it eager... function ApplicationModule(appRef) { } ApplicationModule = __decorate([ NgModule({ providers: APPLICATION_MODULE_PROVIDERS }), __metadata("design:paramtypes", [ApplicationRef]) ], ApplicationModule); return ApplicationModule; }()); ``` Now `ngcc` recognizes `__decorate([...])` declarations and performs its transform. See FW-379 PR Close #26236 --- .../src/ngcc/src/host/esm5_host.ts | 87 ++- .../src/ngcc/src/host/fesm2015_host.ts | 572 +++++++++++++++--- .../src/ngcc/test/helpers/utils.ts | 16 +- .../test/host/esm5_host_import_helper_spec.ts | 396 ++++++++++++ .../src/ngcc/test/host/esm5_host_spec.ts | 24 +- .../host/fesm2015_host_import_helper_spec.ts | 379 ++++++++++++ 6 files changed, 1367 insertions(+), 107 deletions(-) create mode 100644 packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts create mode 100644 packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts 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 35e86b7895..ccee938dcb 100644 --- a/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/esm5_host.ts @@ -10,7 +10,8 @@ import * as ts from 'typescript'; import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host'; import {reflectObjectLiteral} from '../../../ngtsc/metadata'; import {getNameText} from '../utils'; -import {CONSTRUCTOR_PARAMS, Fesm2015ReflectionHost, getPropertyValueFromSymbol} from './fesm2015_host'; +import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './fesm2015_host'; + /** * ESM5 packages contain ECMAScript IIFE functions that act like classes. For example: @@ -49,9 +50,9 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { * - The function declaration inside the IIFE, which is eventually returned and assigned to the * outer variable. * - * @param node The top level declaration that represents an exported class or the function + * @param node the top level declaration that represents an exported class or the function * expression inside the IIFE. - * @returns The symbol for the node or `undefined` if it is not a "class" or has no symbol. + * @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol. */ getClassSymbol(node: ts.Node): ts.Symbol|undefined { const symbol = super.getClassSymbol(node); @@ -96,10 +97,13 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { /** * Parse a function declaration to find the relevant metadata about it. + * * In ESM5 we need to do special work with optional arguments to the function, since they get * their own initializer statement that needs to be parsed and then not included in the "body" * statements of the function. + * * @param node the function declaration to parse. + * @returns an object containing the node, statements and parameters of the function. */ getDefinitionOfFunction(node: T): FunctionDefinition { @@ -117,10 +121,17 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { return {node, body: statements || null, parameters}; } + ///////////// Protected Helpers ///////////// + /** * Find the declarations of the constructor parameters of a class identified by its symbol. + * * In ESM5 there is no "class" so the constructor that we want is actually the declaration * function itself. + * + * @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[] { const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration; @@ -131,8 +142,13 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { } /** - * Constructors parameter decorators are declared in the body of static method of the constructor - * function in ES5. Note that unlike ESM2105 this is a function expression rather than an arrow + * Get the parameter type and decorators for the constructor of a class, + * where the information is stored on a static method of the class. + * + * In this case the decorators are stored in the body of a method + * (`ctorParatemers`) attached to the constructor function. + * + * Note that unlike ESM2015 this is a function expression rather than an arrow * function: * * ``` @@ -143,17 +159,34 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { * { 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 getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { - const declaration = classSymbol.exports && classSymbol.exports.get(CONSTRUCTOR_PARAMS); - const paramDecoratorsProperty = declaration && getPropertyValueFromSymbol(declaration); - const returnStatement = getReturnStatement(paramDecoratorsProperty); + protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null { + const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty); + const returnStatement = getReturnStatement(paramDecorators); const expression = returnStatement && returnStatement.expression; - return expression && ts.isArrayLiteralExpression(expression) ? - expression.elements.map(reflectArrayElement) : - []; + if (expression && ts.isArrayLiteralExpression(expression)) { + const elements = expression.elements; + return elements.map(reflectArrayElement).map(paramInfo => { + const type = paramInfo && paramInfo.get('type') || null; + const decoratorInfo = paramInfo && paramInfo.get('decorators') || null; + const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo); + return {type, decorators}; + }); + } + return null; } + /** + * 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 { const member = super.reflectMember(symbol, decorators, isStatic); @@ -168,8 +201,25 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost { } return member; } + + /** + * Find statements related to the given class that may contain calls to a helper. + * + * In ESM5 code the helper calls are hidden inside the class's IIFE. + * + * @param classSymbol the class whose helper calls we are interested in. We expect this symbol + * to reference the inner identifier inside the IIFE. + * @returns an array of statements that may contain helper calls. + */ + protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] { + const classDeclaration = classSymbol.valueDeclaration; + return ts.isBlock(classDeclaration.parent) ? Array.from(classDeclaration.parent.statements) : + []; + } } +///////////// Internal Helpers ///////////// + function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined { if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) { return undefined; @@ -209,8 +259,8 @@ function reflectArrayElement(element: ts.Expression) { * if (arg === void 0) { arg = initializer; } * ``` * - * @param statement A statement that may be initializing an optional parameter - * @param parameters The collection of parameters that were found in the function definition + * @param statement a statement that may be initializing an optional parameter + * @param parameters the collection of parameters that were found in the function definition * @returns true if the statement was a parameter initializer */ function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) { @@ -218,7 +268,7 @@ function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[ ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) { const ifStatementComparison = statement.expression; // (arg === void 0) const thenStatement = statement.thenStatement.statements[0]; // arg = initializer; - if (isAssignment(thenStatement)) { + if (isAssignmentStatement(thenStatement)) { const comparisonName = ifStatementComparison.left.text; const assignmentName = thenStatement.expression.left.text; if (comparisonName === assignmentName) { @@ -239,10 +289,3 @@ function isUndefinedComparison(expression: ts.Expression): expression is ts.Expr expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken && ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left); } - -function isAssignment(statement: ts.Statement): statement is ts.ExpressionStatement& - {expression: {left: ts.Identifier, right: ts.Expression}} { - return ts.isExpressionStatement(statement) && ts.isBinaryExpression(statement.expression) && - statement.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken && - ts.isIdentifier(statement.expression.left); -} diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts index f9ba0cec65..e69b22f5d3 100644 --- a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts @@ -6,11 +6,12 @@ * 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} from '../../../ngtsc/host'; import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; -import {findAll, getNameText} from '../utils'; +import {findAll, getNameText, isDefined} from '../utils'; import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; @@ -64,22 +65,15 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements */ getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null { const symbol = this.getClassSymbol(declaration); - if (symbol) { - if (symbol.exports && symbol.exports.has(DECORATORS)) { - // Symbol of the identifier for `SomeDirective.decorators`. - const decoratorsSymbol = symbol.exports.get(DECORATORS) !; - const decoratorsIdentifier = decoratorsSymbol.valueDeclaration; - - if (decoratorsIdentifier && decoratorsIdentifier.parent) { - if (ts.isBinaryExpression(decoratorsIdentifier.parent)) { - // AST of the array of decorator values - const decoratorsArray = decoratorsIdentifier.parent.right; - return this.reflectDecorators(decoratorsArray); - } - } - } + if (!symbol) { + return null; + } + const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); + if (decoratorsProperty) { + return this.getClassDecoratorsFromStaticProperty(decoratorsProperty); + } else { + return this.getClassDecoratorsFromHelperCall(symbol); } - return null; } /** @@ -128,6 +122,28 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements }); } + // 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({ @@ -171,38 +187,33 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements } const parameterNodes = this.getConstructorParameterDeclarations(classSymbol); if (parameterNodes) { - const parameters: CtorParameter[] = []; - const decoratorInfo = this.getConstructorDecorators(classSymbol); - parameterNodes.forEach((node, index) => { - const info = decoratorInfo[index]; - const decorators = - info && info.has('decorators') && this.reflectDecorators(info.get('decorators') !) || - null; - const type = info && info.get('type') || null; - const nameNode = node.name; - parameters.push({name: getNameText(nameNode), nameNode, type, decorators}); - }); - return parameters; + return 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. + * @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 { - return ts.isClassDeclaration(declaration) ? - declaration.name && this.checker.getSymbolAtLocation(declaration.name) : - 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_NGCC_MARKER`. - * @param module The module in which to search for switchable declarations. - * @returns An array of variable declarations that match. + * @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 @@ -295,7 +306,84 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements } /** - * Member decorators are declared as static properties of the class in ES2015: + * 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); + } + } + 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(isImportedFromCore).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 = { @@ -304,25 +392,166 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements * "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 getMemberDecorators(classSymbol: ts.Symbol): Map { + protected getMemberDecoratorsFromStaticProperty(decoratorsProperty: ts.Symbol): + Map { const memberDecorators = new Map(); - if (classSymbol.exports && classSymbol.exports.has(PROP_DECORATORS)) { - // Symbol of the identifier for `SomeDirective.propDecorators`. - const propDecoratorsMap = - getPropertyValueFromSymbol(classSymbol.exports.get(PROP_DECORATORS) !); - if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { - const propertiesMap = reflectObjectLiteral(propDecoratorsMap); - propertiesMap.forEach( - (value, name) => { memberDecorators.set(name, this.reflectDecorators(value)); }); - } + // Symbol of the identifier for `SomeDirective.propDecorators`. + const propDecoratorsMap = getPropertyValueFromSymbol(decoratorsProperty); + if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) { + const propertiesMap = reflectObjectLiteral(propDecoratorsMap); + propertiesMap.forEach( + (value, name) => { memberDecorators.set(name, this.reflectDecorators(value)); }); } return memberDecorators; } /** - * Reflect over the given expression and extract decorator information. - * @param decoratorsArray An expression that contains decorator information. + * 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(isImportedFromCore); + 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, + 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[] = []; @@ -351,6 +580,14 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements 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; @@ -421,8 +658,7 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements * 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. + * the class's constructor or null if there is no constructor. */ protected getConstructorParameterDeclarations(classSymbol: ts.Symbol): ts.ParameterDeclaration[]|null { @@ -440,44 +676,211 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements } /** - * Constructors parameter decorators are declared in the body of static method of the class in - * ES2015: + * 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: IterableDiffers, }, * { 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 getConstructorDecorators(classSymbol: ts.Symbol): (Map|null)[] { - if (classSymbol.exports && classSymbol.exports.has(CONSTRUCTOR_PARAMS)) { - const paramDecoratorsProperty = - getPropertyValueFromSymbol(classSymbol.exports.get(CONSTRUCTOR_PARAMS) !); - if (paramDecoratorsProperty && ts.isArrowFunction(paramDecoratorsProperty)) { - if (ts.isArrayLiteralExpression(paramDecoratorsProperty.body)) { - return paramDecoratorsProperty.body.elements.map( - element => - ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null); - } + 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); + return {type, decorators}; + }); } } - return []; + 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); } } +///////////// Exported Helpers ///////////// + +export type ParamInfo = { + decorators: Decorator[] | null, + type: ts.Expression | null +}; + /** - * The arguments of a decorator are held in the `args` property of its declaration object. + * A statement node that represents an assignment. */ -function getDecoratorArgs(node: ts.ObjectLiteralExpression): ts.Expression[] { - const argsProperty = node.properties.filter(ts.isPropertyAssignment) - .find(property => getNameText(property.name) === 'args'); - const argsExpression = argsProperty && argsProperty.initializer; - return argsExpression && ts.isArrayLiteralExpression(argsExpression) ? - Array.from(argsExpression.elements) : - []; +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; +} + +/** + * Test whether a decorator was imported from `@angular/core`. + * + * Is the decorator: + * * extermally mported from `@angulare/core`? + * * relatively internally imported where the decoratee is already in `@angular/core`? + * + * Note we do not support decorators that are not imported at all. + * + * @param decorator the decorator to test. + */ +export function isImportedFromCore(decorator: Decorator): boolean { + const importFrom = decorator.import && decorator.import.from || ''; + return importFrom === '@angular/core' || + (/^\./.test(importFrom) && + /node_modules[\\\/]@angular[\\\/]core/.test(decorator.node.getSourceFile().fileName)); +} + +/** + * 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'; } /** @@ -490,6 +893,31 @@ export function getPropertyValueFromSymbol(propSymbol: ts.Symbol): ts.Expression 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); diff --git a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts index fbf0f4febc..705f2e2669 100644 --- a/packages/compiler-cli/src/ngcc/test/helpers/utils.ts +++ b/packages/compiler-cli/src/ngcc/test/helpers/utils.ts @@ -11,7 +11,8 @@ import {makeProgram as _makeProgram} from '../../../ngtsc/testing/in_memory_type export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript'; export function makeProgram(...files: {name: string, contents: string}[]): ts.Program { - return _makeProgram([getFakeCore(), ...files], {allowJs: true, checkJs: false}).program; + return _makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false}) + .program; } // TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package @@ -51,6 +52,17 @@ export function getFakeCore() { }; } +export function getFakeTslib() { + return { + name: 'node_modules/tslib/index.ts', + contents: ` + export function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any) {} + export function __param(paramIndex: number, decorator: any) {} + export function __metadata(metadataKey: any, metadataValue: any) {} + ` + }; +} + export function convertToDirectTsLibImport(filesystem: {name: string, contents: string}[]) { return filesystem.map(file => { const contents = @@ -58,7 +70,7 @@ export function convertToDirectTsLibImport(filesystem: {name: string, contents: .replace( `import * as tslib_1 from 'tslib';`, `import { __decorate, __metadata, __read, __values, __param, __extends, __assign } from 'tslib';`) - .replace('tslib_1.', ''); + .replace(/tslib_1\./g, ''); return {...file, contents}; }); } diff --git a/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts b/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts new file mode 100644 index 0000000000..474e7c070a --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/esm5_host_import_helper_spec.ts @@ -0,0 +1,396 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import * as ts from 'typescript'; + +import {ClassMemberKind, Import} from '../../../ngtsc/host'; +import {Esm5ReflectionHost} from '../../src/host/esm5_host'; +import {convertToDirectTsLibImport, getDeclaration, makeProgram} from '../helpers/utils'; + +const FILES = [ + { + name: '/some_directive.js', + contents: ` + import * as tslib_1 from "tslib"; + import { Directive, Inject, InjectionToken, Input } from '@angular/core'; + var INJECTED_TOKEN = new InjectionToken('injected'); + var ViewContainerRef = /** @class */ (function () { + function ViewContainerRef() { + } + return ViewContainerRef; + }()); + var TemplateRef = /** @class */ (function () { + function TemplateRef() { + } + return TemplateRef; + }()); + var SomeDirective = /** @class */ (function () { + function SomeDirective(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + this.input1 = ''; + this.input2 = 0; + } + SomeDirective.prototype.instanceMethod = function () { }; + SomeDirective.staticMethod = function () { }; + SomeDirective.staticProperty = 'static'; + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", String) + ], SomeDirective.prototype, "input1", void 0); + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", Number) + ], SomeDirective.prototype, "input2", void 0); + SomeDirective = tslib_1.__decorate([ + Directive({ selector: '[someDirective]' }), + tslib_1.__param(2, Inject(INJECTED_TOKEN)), + tslib_1.__metadata("design:paramtypes", [ViewContainerRef, + TemplateRef, String]) + ], SomeDirective); + return SomeDirective; + }()); + export { SomeDirective }; + `, + }, + { + name: '/node_modules/@angular/core/some_directive.js', + contents: ` + import * as tslib_1 from "tslib"; + import { Directive, Input } from './directives'; + var SomeDirective = /** @class */ (function () { + function SomeDirective() { + this.input1 = ''; + } + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", String) + ], SomeDirective.prototype, "input1", void 0); + SomeDirective = tslib_1.__decorate([ + Directive({ selector: '[someDirective]' }), + ], SomeDirective); + return SomeDirective; +}()); +export { SomeDirective }; +`, + }, + { + name: '/ngmodule.js', + contents: ` + import * as tslib_1 from "tslib"; + import { NgModule } from '@angular/core'; + var HttpClientXsrfModule = /** @class */ (function () { + function HttpClientXsrfModule() { + } + HttpClientXsrfModule_1 = HttpClientXsrfModule; + HttpClientXsrfModule.withOptions = function (options) { + if (options === void 0) { options = {}; } + return { + ngModule: HttpClientXsrfModule_1, + providers: [], + }; + }; + var HttpClientXsrfModule_1; + HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([ + NgModule({ + providers: [], + }) + ], HttpClientXsrfModule); + return HttpClientXsrfModule; + }()); + var missingValue; + var nonDecoratedVar; + nonDecoratedVar = 43; + export { HttpClientXsrfModule }; + ` + }, +]; + +describe('Esm5ReflectionHost [import helper style]', () => { + [{files: FILES, label: 'namespaced'}, + {files: convertToDirectTsLibImport(FILES), label: 'direct import'}, + ].forEach(fileSystem => { + describe(`[${fileSystem.label}]`, () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + 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 use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.callFake( + (identifier: ts.Identifier) => identifier.getText() === 'Directive' ? + {from: '@angular/core', name: 'Directive'} : + {}); + + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); + + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const program = makeProgram(fileSystem.files[1]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', + ts.isVariableDeclaration); + 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: './directives'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticMethod = members.find(member => member.name === 'staticMethod') !; + expect(staticMethod.kind).toEqual(ClassMemberKind.Method); + expect(staticMethod.isStatic).toEqual(true); + expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true); + }); + + it('should find static properties on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); + + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + + host.getMembersOfClass(classNode); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const program = makeProgram(fileSystem.files[1]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ + 'ViewContainerRef', 'TemplateRef', 'String' + ]); + }); + + describe('(returned parameters `decorators`)', () => { + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, '/some_directive.js', '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(fileSystem.files[0]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const decoratorNode = classDecorators[0].node; + + const identifierOfDirective = + ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? + decoratorNode.expression : + null; + + 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('getVariableValue', () => { + it('should find the "actual" declaration of an aliased variable identifier', () => { + const program = makeProgram(fileSystem.files[2]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const ngModuleRef = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); + + const value = host.getVariableValue(ngModuleRef !); + expect(value).not.toBe(null); + if (!value || !ts.isFunctionDeclaration(value.parent)) { + throw new Error( + `Expected result to be a function declaration: ${value && value.getText()}.`); + } + expect(value.getText()).toBe('HttpClientXsrfModule'); + }); + + it('should return undefined if the variable has no assignment', () => { + const program = makeProgram(fileSystem.files[2]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const missingValue = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); + const value = host.getVariableValue(missingValue !); + expect(value).toBe(null); + }); + + it('should return null if the variable is not assigned from a call to __decorate', () => { + const program = makeProgram(fileSystem.files[2]); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const nonDecoratedVar = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); + const value = host.getVariableValue(nonDecoratedVar !); + expect(value).toBe(null); + }); + }); + }); + + function findVariableDeclaration( + node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { + if (!node) { + return; + } + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && + node.name.text === variableName) { + return node; + } + return node.forEachChild(node => findVariableDeclaration(node, variableName)); + } +}); 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 01978321f9..25fe54083e 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 @@ -1164,18 +1164,20 @@ describe('Esm5ReflectionHost', () => { expect(host.getClassSymbol(innerNode)).toBeDefined(); }); - it('should return the same class symbol for outer and inner declarations', () => { - const program = makeProgram(SIMPLE_CLASS_FILE); - const host = new Esm5ReflectionHost(program.getTypeChecker()); - const outerNode = - getDeclaration(program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); - const innerNode = - (((outerNode.initializer as ts.ParenthesizedExpression).expression as ts.CallExpression) - .expression as ts.FunctionExpression) - .body.statements.find(ts.isFunctionDeclaration) !; + it('should return the same class symbol (of the inner declaration) for outer and inner declarations', + () => { + const program = makeProgram(SIMPLE_CLASS_FILE); + const host = new Esm5ReflectionHost(program.getTypeChecker()); + const outerNode = getDeclaration( + program, SIMPLE_CLASS_FILE.name, 'EmptyClass', ts.isVariableDeclaration); + const innerNode = (((outerNode.initializer as ts.ParenthesizedExpression) + .expression as ts.CallExpression) + .expression as ts.FunctionExpression) + .body.statements.find(ts.isFunctionDeclaration) !; - expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); - }); + expect(host.getClassSymbol(innerNode)).toBe(host.getClassSymbol(outerNode)); + expect(host.getClassSymbol(innerNode) !.valueDeclaration).toBe(innerNode); + }); it('should return undefined if node is not an ES5 class', () => { const program = makeProgram(FOO_FUNCTION_FILE); diff --git a/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts new file mode 100644 index 0000000000..96a5475016 --- /dev/null +++ b/packages/compiler-cli/src/ngcc/test/host/fesm2015_host_import_helper_spec.ts @@ -0,0 +1,379 @@ +/** + * @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 {convertToDirectTsLibImport, getDeclaration, makeProgram} from '../helpers/utils'; + +const FILES = [ + { + name: '/some_directive.js', + contents: ` + import * as tslib_1 from 'tslib'; + import { Directive, Inject, InjectionToken, Input } from '@angular/core'; + const INJECTED_TOKEN = new InjectionToken('injected'); + class ViewContainerRef { + } + class TemplateRef { + } + let SomeDirective = class SomeDirective { + constructor(_viewContainer, _template, injected) { + this.instanceProperty = 'instance'; + this.input1 = ''; + this.input2 = 0; + } + instanceMethod() { } + static staticMethod() { } + }; + SomeDirective.staticProperty = 'static'; + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", String) + ], SomeDirective.prototype, "input1", void 0); + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", Number) + ], SomeDirective.prototype, "input2", void 0); + SomeDirective = tslib_1.__decorate([ + Directive({ selector: '[someDirective]' }), + tslib_1.__param(2, Inject(INJECTED_TOKEN)), + tslib_1.__metadata("design:paramtypes", [ViewContainerRef, + TemplateRef, String]) + ], SomeDirective); + export { SomeDirective }; + `, + }, + { + name: '/node_modules/@angular/core/some_directive.js', + contents: ` + import * as tslib_1 from 'tslib'; + import { Directive, Input } from './directives'; + let SomeDirective = class SomeDirective { + constructor() { this.input1 = ''; } + }; + tslib_1.__decorate([ + Input(), + tslib_1.__metadata("design:type", String) + ], SomeDirective.prototype, "input1", void 0); + SomeDirective = tslib_1.__decorate([ + Directive({ selector: '[someDirective]' }), + ], SomeDirective); + export { SomeDirective }; + `, + }, + { + name: 'ngmodule.js', + contents: ` + import * as tslib_1 from 'tslib'; + import { NgModule } from './directives'; + var HttpClientXsrfModule_1; + let HttpClientXsrfModule = HttpClientXsrfModule_1 = class HttpClientXsrfModule { + static withOptions(options = {}) { + return { + ngModule: HttpClientXsrfModule_1, + providers: [], + }; + } + }; + HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([ + NgModule({ + providers: [], + }) + ], HttpClientXsrfModule); + let missingValue; + let nonDecoratedVar; + nonDecoratedVar = 43; + export { HttpClientXsrfModule }; + ` + }, +]; + +describe('Fesm2015ReflectionHost [import helper style]', () => { + [{files: FILES, label: 'namespaced'}, + {files: convertToDirectTsLibImport(FILES), label: 'direct import'}, + ].forEach(fileSystem => { + describe(`[${fileSystem.label}]`, () => { + + describe('getDecoratorsOfDeclaration()', () => { + it('should find the decorators on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + 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 use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = spyOn(Fesm2015ReflectionHost.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(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + + const decorators = host.getDecoratorsOfDeclaration(classNode) !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'}); + + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const program = makeProgram(fileSystem.files[1]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', + ts.isVariableDeclaration); + 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: './directives'}); + expect(decorator.args !.map(arg => arg.getText())).toEqual([ + '{ selector: \'[someDirective]\' }', + ]); + }); + }); + + describe('getMembersOfClass()', () => { + it('should find decorated members on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + + const input2 = members.find(member => member.name === 'input2') !; + expect(input2.kind).toEqual(ClassMemberKind.Property); + expect(input2.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + + it('should find non decorated properties on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const instanceProperty = members.find(member => member.name === 'instanceProperty') !; + expect(instanceProperty.kind).toEqual(ClassMemberKind.Property); + expect(instanceProperty.isStatic).toEqual(false); + expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true); + expect(instanceProperty.value !.getText()).toEqual(`'instance'`); + }); + + it('should find static methods on a class', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + 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(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + + const members = host.getMembersOfClass(classNode); + const staticProperty = members.find(member => member.name === 'staticProperty') !; + expect(staticProperty.kind).toEqual(ClassMemberKind.Property); + expect(staticProperty.isStatic).toEqual(true); + expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true); + expect(staticProperty.value !.getText()).toEqual(`'static'`); + }); + + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const spy = + spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({}); + + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + + host.getMembersOfClass(classNode); + const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text); + expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy(); + }); + + it('should support decorators being used inside @angular/core', () => { + const program = makeProgram(fileSystem.files[1]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective', + ts.isVariableDeclaration); + const members = host.getMembersOfClass(classNode); + + const input1 = members.find(member => member.name === 'input1') !; + expect(input1.kind).toEqual(ClassMemberKind.Property); + expect(input1.isStatic).toEqual(false); + expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); + }); + }); + + describe('getConstructorParameters', () => { + it('should find the decorated constructor parameters', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + + expect(parameters).toBeDefined(); + expect(parameters !.map(parameter => parameter.name)).toEqual([ + '_viewContainer', '_template', 'injected' + ]); + expect(parameters !.map(parameter => parameter.type !.getText())).toEqual([ + 'ViewContainerRef', 'TemplateRef', 'String' + ]); + }); + + describe('(returned parameters `decorators`)', () => { + it('should use `getImportOfIdentifier()` to retrieve import info', () => { + const mockImportInfo = {} as Import; + const spy = spyOn(Fesm2015ReflectionHost.prototype, 'getImportOfIdentifier') + .and.returnValue(mockImportInfo); + + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const parameters = host.getConstructorParameters(classNode); + const decorators = parameters ![2].decorators !; + + expect(decorators.length).toEqual(1); + expect(decorators[0].import).toBe(mockImportInfo); + + const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier; + expect(typeIdentifier.text).toBe('Inject'); + }); + }); + }); + + describe('getDeclarationOfIdentifier', () => { + it('should return the declaration of a locally defined identifier', () => { + const program = makeProgram(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const ctrDecorators = host.getConstructorParameters(classNode) !; + const identifierOfViewContainerRef = ctrDecorators[0].type !as ts.Identifier; + + const expectedDeclarationNode = getDeclaration( + program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration); + 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(fileSystem.files[0]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const classNode = getDeclaration( + program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration); + const classDecorators = host.getDecoratorsOfDeclaration(classNode) !; + const decoratorNode = classDecorators[0].node; + const identifierOfDirective = + ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ? + decoratorNode.expression : + null; + + 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('getVariableValue', () => { + it('should find the "actual" declaration of an aliased variable identifier', () => { + const program = makeProgram(fileSystem.files[2]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const ngModuleRef = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1'); + + const value = host.getVariableValue(ngModuleRef !); + expect(value).not.toBe(null); + if (!value || !ts.isClassExpression(value)) { + throw new Error( + `Expected value to be a class expression: ${value && value.getText()}.`); + } + expect(value.name !.text).toBe('HttpClientXsrfModule'); + }); + + it('should return null if the variable has no assignment', () => { + const program = makeProgram(fileSystem.files[2]); + const host = new Fesm2015ReflectionHost(program.getTypeChecker()); + const missingValue = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'missingValue'); + const value = host.getVariableValue(missingValue !); + expect(value).toBe(null); + }); + + 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(program.getTypeChecker()); + const nonDecoratedVar = findVariableDeclaration( + program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar'); + const value = host.getVariableValue(nonDecoratedVar !); + expect(value).toBe(null); + }); + }); + }); + }); + + function findVariableDeclaration( + node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined { + if (!node) { + return; + } + if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && + node.name.text === variableName) { + return node; + } + return node.forEachChild(node => findVariableDeclaration(node, variableName)); + } +});