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:
parent
13cdd13511
commit
7d08722e80
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue