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
This commit is contained in:
Pete Bacon Darwin 2018-09-30 20:53:25 +01:00 committed by Jason Aden
parent 13cdd13511
commit 7d08722e80
6 changed files with 1367 additions and 107 deletions

View File

@ -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<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
@ -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<string, ts.Expression>|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);
}

View File

@ -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<string, Decorator[]> {
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<string, Decorator[]> {
protected getMemberDecoratorsFromStaticProperty(decoratorsProperty: ts.Symbol):
Map<string, Decorator[]> {
const memberDecorators = new Map<string, Decorator[]>();
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<string, Decorator[]> {
const memberDecoratorMap = new Map<string, Decorator[]>();
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<string, Decorator[]>} {
const classDecorators: Decorator[] = [];
const memberDecorators = new Map<string, Decorator[]>();
// 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<string, ts.Expression>|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<ts.EqualsToken> {
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<T>(map: Map<string, T>, key: ts.__String): T|undefined {
const mapKey = key as string;
const value = map.get(mapKey);

View File

@ -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};
});
}

View File

@ -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));
}
});

View File

@ -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);

View File

@ -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));
}
});