refactor(ivy): ngcc - categorize the various decorate calls upfront (#31614)

Any decorator information present in TypeScript is emitted into the
generated JavaScript sources by means of `__decorate` call. This call
contains both the decorators as they existed in the original source
code, together with calls to `tslib` helpers that convey additional
information on e.g. type information and parameter decorators. These
different kinds of decorator calls were not previously distinguished on
their own, but instead all treated as `Decorator` by themselves. The
"decorators" that were actually `tslib` helper calls were conveniently
filtered out because they were not imported from `@angular/core`, a
characteristic that ngcc uses to drop certain decorators.

Note that this posed an inconsistency in ngcc when it processes
`@angular/core`'s UMD bundle, as the `tslib` helper functions have been
inlined in said bundle. Because of the inlining, the `tslib` helpers
appear to be from `@angular/core`, so ngcc would fail to drop those
apparent "decorators". This inconsistency does not currently cause any
issues, as ngtsc is specifically looking for decorators based on  their
name and any remaining decorators are simply ignored.

This commit rewrites the decorator analysis of a class to occur all in a
single phase, instead of all throughout the `ReflectionHost`. This
allows to categorize the various decorate calls in a single sweep,
instead of constantly needing to filter out undesired decorate calls on
the go. As an added benefit, the computed decorator information is now
cached per class, such that subsequent reflection queries that need
decorator information can reuse the cached info.

PR Close #31614
This commit is contained in:
JoostK 2019-07-19 23:17:22 +02:00 committed by Andrew Kushnir
parent 0386c964b5
commit 5e5be43acd
8 changed files with 424 additions and 422 deletions

View File

@ -9,7 +9,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system'; import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, isDecoratorIdentifier, reflectObjectLiteral} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, TypeScriptReflectionHost, isDecoratorIdentifier, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger'; import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program'; import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils'; import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
@ -73,6 +73,14 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*/ */
protected aliasedClassDeclarations = new Map<ts.Declaration, ts.Identifier>(); protected aliasedClassDeclarations = new Map<ts.Declaration, ts.Identifier>();
/**
* Caches the information of the decorators on a class, as the work involved with extracting
* decorators is complex and frequently used.
*
* This map is lazily populated during the first call to `acquireDecoratorInfo` for a given class.
*/
protected decoratorCache = new Map<ClassDeclaration, DecoratorInfo>();
constructor( constructor(
protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker, protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker,
dts?: BundleProgram|null) { dts?: BundleProgram|null) {
@ -247,12 +255,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/** Gets all decorators of the given class symbol. */ /** Gets all decorators of the given class symbol. */
getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null { getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS); const {classDecorators} = this.acquireDecoratorInfo(symbol);
if (decoratorsProperty) { return classDecorators;
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
} else {
return this.getClassDecoratorsFromHelperCall(symbol);
}
} }
/** /**
@ -542,6 +546,72 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return symbol.exports && symbol.exports.get(propertyName); return symbol.exports && symbol.exports.get(propertyName);
} }
/**
* This is the main entry-point for obtaining information on the decorators of a given class. This
* information is computed either from static properties if present, or using `tslib.__decorate`
* helper calls otherwise. The computed result is cached per class.
*
* @param classSymbol the class for which decorators should be acquired.
* @returns all information of the decorators on the class.
*/
protected acquireDecoratorInfo(classSymbol: ClassSymbol): DecoratorInfo {
if (this.decoratorCache.has(classSymbol.valueDeclaration)) {
return this.decoratorCache.get(classSymbol.valueDeclaration) !;
}
// First attempt extracting decorators from static properties.
let decoratorInfo = this.computeDecoratorInfoFromStaticProperties(classSymbol);
if (decoratorInfo === null) {
// If none were present, use the `__decorate` helper calls instead.
decoratorInfo = this.computeDecoratorInfoFromHelperCalls(classSymbol);
}
this.decoratorCache.set(classSymbol.valueDeclaration, decoratorInfo);
return decoratorInfo;
}
/**
* Attempts to compute decorator information from static properties "decorators", "propDecorators"
* and "ctorParameters" on the class. If neither of these static properties is present the
* library is likely not compiled using tsickle for usage with Closure compiler, in which case
* `null` is returned.
*
* @param classSymbol The class symbol to compute the decorators information for.
* @returns All information on the decorators as extracted from static properties, or `null` if
* none of the static properties exist.
*/
protected computeDecoratorInfoFromStaticProperties(classSymbol: ClassSymbol): DecoratorInfo|null {
let classDecorators: Decorator[]|null = null;
let memberDecorators: Map<string, Decorator[]>|null = null;
let constructorParamInfo: ParamInfo[]|null = null;
const decoratorsProperty = this.getStaticProperty(classSymbol, DECORATORS);
if (decoratorsProperty !== undefined) {
classDecorators = this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
}
const propDecoratorsProperty = this.getStaticProperty(classSymbol, PROP_DECORATORS);
if (propDecoratorsProperty !== undefined) {
memberDecorators = this.getMemberDecoratorsFromStaticProperty(propDecoratorsProperty);
}
const constructorParamsProperty = this.getStaticProperty(classSymbol, CONSTRUCTOR_PARAMS);
if (constructorParamsProperty !== undefined) {
constructorParamInfo = this.getParamInfoFromStaticProperty(constructorParamsProperty);
}
// If none of the static properties were present, no decorator info could be computed.
if (classDecorators === null && memberDecorators === null && constructorParamInfo === null) {
return null;
}
return {
classDecorators,
memberDecorators: memberDecorators || new Map<string, Decorator[]>(),
constructorParamInfo: constructorParamInfo || [],
};
}
/** /**
* Get all class decorators for the given class, where the decorators are declared * Get all class decorators for the given class, where the decorators are declared
* via a static property. For example: * via a static property. For example:
@ -570,32 +640,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return null; 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: ClassSymbol): Decorator[]|null {
const decorators: Decorator[] = [];
const helperCalls = this.getHelperCallsForClass(symbol, '__decorate');
helperCalls.forEach(helperCall => {
const {classDecorators} =
this.reflectDecoratorsFromHelperCall(helperCall, makeClassTargetFilter(symbol.name));
classDecorators.filter(decorator => this.isFromCore(decorator))
.forEach(decorator => decorators.push(decorator));
});
return decorators.length ? decorators : null;
}
/** /**
* Examine a symbol which should be of a class, and return metadata about its members. * Examine a symbol which should be of a class, and return metadata about its members.
* *
@ -606,7 +650,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
const members: ClassMember[] = []; const members: ClassMember[] = [];
// The decorators map contains all the properties that are decorated // The decorators map contains all the properties that are decorated
const decoratorsMap = this.getMemberDecorators(symbol); const {memberDecorators} = this.acquireDecoratorInfo(symbol);
// Make a copy of the decorators as successfully reflected members delete themselves from the
// map, so that any leftovers can be easily dealt with.
const decoratorsMap = new Map(memberDecorators);
// The member map contains all the method (instance and static); and any instance properties // The member map contains all the method (instance and static); and any instance properties
// that are initialized in the class. // that are initialized in the class.
@ -675,21 +723,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return members; return members;
} }
/**
* 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: ClassSymbol): 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: * Member decorators may be declared as static properties of the class:
* *
@ -724,7 +757,21 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
} }
/** /**
* Member decorators may be declared via helper call statements. * For a given class symbol, collects all decorator information from tslib helper methods, as
* generated by TypeScript into emitted JavaScript files.
*
* Class decorators are extracted from calls to `tslib.__decorate` that look as follows:
*
* ```
* let SomeDirective = class SomeDirective {}
* SomeDirective = __decorate([
* Directive({ selector: '[someDirective]' }),
* ], SomeDirective);
* ```
*
* The extraction of member decorators is similar, with the distinction that its 2nd and 3rd
* argument correspond with a "prototype" target and the name of the member to which the
* decorators apply.
* *
* ``` * ```
* __decorate([ * __decorate([
@ -733,103 +780,195 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
* ], SomeDirective.prototype, "input1", void 0); * ], SomeDirective.prototype, "input1", void 0);
* ``` * ```
* *
* @param classSymbol the class whose member decorators we are interested in. * @param classSymbol The class symbol for which decorators should be extracted.
* @returns a map whose keys are the name of the members and whose values are collections of * @returns All information on the decorators of the class.
* decorators for the given member.
*/ */
protected getMemberDecoratorsFromHelperCalls(classSymbol: ClassSymbol): Map<string, Decorator[]> { protected computeDecoratorInfoFromHelperCalls(classSymbol: ClassSymbol): DecoratorInfo {
const memberDecoratorMap = new Map<string, Decorator[]>(); let classDecorators: Decorator[]|null = null;
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.has(memberName) ? memberDecoratorMap.get(memberName) ! : [];
const coreDecorators = decorators.filter(decorator => this.isFromCore(decorator));
memberDecoratorMap.set(memberName, memberDecorators.concat(coreDecorators));
}
});
});
return memberDecoratorMap;
}
/**
* Extract decorator info from `__decorate` helper function calls.
* @param helperCall the call to a helper that may contain decorator calls
* @param targetFilter a function to filter out targets that we are not interested in.
* @returns a mapping from member name to decorators, where the key is either the name of the
* member or `undefined` if it refers to decorators on the class as a whole.
*/
protected reflectDecoratorsFromHelperCall(
helperCall: ts.CallExpression, targetFilter: TargetFilter):
{classDecorators: Decorator[], memberDecorators: Map<string, Decorator[]>} {
const classDecorators: Decorator[] = [];
const memberDecorators = new Map<string, Decorator[]>(); const memberDecorators = new Map<string, Decorator[]>();
const constructorParamInfo: ParamInfo[] = [];
// First check that the `target` argument is correct const getConstructorParamInfo = (index: number) => {
if (targetFilter(helperCall.arguments[1])) { let param = constructorParamInfo[index];
// Grab the `decorators` argument which should be an array of calls if (param === undefined) {
const decoratorCalls = helperCall.arguments[0]; param = constructorParamInfo[index] = {decorators: null, typeExpression: null};
if (decoratorCalls && ts.isArrayLiteralExpression(decoratorCalls)) { }
decoratorCalls.elements.forEach(element => { return param;
// We only care about those elements that are actual calls };
if (ts.isCallExpression(element)) {
const decorator = this.reflectDecoratorCall(element); // All relevant information can be extracted from calls to `__decorate`, obtain these first.
if (decorator) { // Note that although the helper calls are retrieved using the class symbol, the result may
const keyArg = helperCall.arguments[2]; // contain helper calls corresponding with unrelated classes. Therefore, each helper call still
const keyName = keyArg && ts.isStringLiteral(keyArg) ? keyArg.text : undefined; // has to be checked to actually correspond with the class symbol.
if (keyName === undefined) { const helperCalls = this.getHelperCallsForClass(classSymbol, '__decorate');
classDecorators.push(decorator);
} else { for (const helperCall of helperCalls) {
if (isClassDecorateCall(helperCall, classSymbol.name)) {
// This `__decorate` call is targeting the class itself.
const helperArgs = helperCall.arguments[0];
for (const element of helperArgs.elements) {
const entry = this.reflectDecorateHelperEntry(element);
if (entry === null) {
continue;
}
if (entry.type === 'decorator') {
// The helper arg was reflected to represent an actual decorator
if (this.isFromCore(entry.decorator)) {
(classDecorators || (classDecorators = [])).push(entry.decorator);
}
} else if (entry.type === 'param:decorators') {
// The helper arg represents a decorator for a parameter. Since it's applied to the
// class, it corresponds with a constructor parameter of the class.
const param = getConstructorParamInfo(entry.index);
(param.decorators || (param.decorators = [])).push(entry.decorator);
} else if (entry.type === 'params') {
// The helper arg represents the types of the parameters. Since it's applied to the
// class, it corresponds with the constructor parameters of the class.
entry.types.forEach(
(type, index) => getConstructorParamInfo(index).typeExpression = type);
}
}
} else if (isMemberDecorateCall(helperCall, classSymbol.name)) {
// The `__decorate` call is targeting a member of the class
const helperArgs = helperCall.arguments[0];
const memberName = helperCall.arguments[2].text;
for (const element of helperArgs.elements) {
const entry = this.reflectDecorateHelperEntry(element);
if (entry === null) {
continue;
}
if (entry.type === 'decorator') {
// The helper arg was reflected to represent an actual decorator.
if (this.isFromCore(entry.decorator)) {
const decorators = const decorators =
memberDecorators.has(keyName) ? memberDecorators.get(keyName) ! : []; memberDecorators.has(memberName) ? memberDecorators.get(memberName) ! : [];
decorators.push(decorator); decorators.push(entry.decorator);
memberDecorators.set(keyName, decorators); memberDecorators.set(memberName, decorators);
}
} else {
// Information on decorated parameters is not interesting for ngcc, so it's ignored.
} }
} }
} }
});
} }
}
return {classDecorators, memberDecorators}; return {classDecorators, memberDecorators, constructorParamInfo};
} }
/** /**
* Extract the decorator information from a call to a decorator as a function. * Extract the details of an entry within a `__decorate` helper call. For example, given the
* This happens when the decorators has been used in a `__decorate` helper call. * following code:
* For example:
* *
* ``` * ```
* __decorate([ * __decorate([
* Directive({ selector: '[someDirective]' }), * Directive({ selector: '[someDirective]' }),
* tslib_1.__param(2, Inject(INJECTED_TOKEN)),
* tslib_1.__metadata("design:paramtypes", [ViewContainerRef, TemplateRef, String])
* ], SomeDirective); * ], SomeDirective);
* ``` * ```
* *
* Here the `Directive` decorator is decorating `SomeDirective` and the options for * it can be seen that there are calls to regular decorators (the `Directive`) and calls into
* the decorator are passed as arguments to the `Directive()` call. * `tslib` functions which have been inserted by TypeScript. Therefore, this function classifies
* a call to correspond with
* 1. a real decorator like `Directive` above, or
* 2. a decorated parameter, corresponding with `__param` calls from `tslib`, or
* 3. the type information of parameters, corresponding with `__metadata` call from `tslib`
* *
* @param call the call to the decorator. * @param expression the expression that needs to be reflected into a `DecorateHelperEntry`
* @returns a decorator containing the reflected information, or null if the call * @returns an object that indicates which of the three categories the call represents, together
* is not a valid decorator call. * with the reflected information of the call, or null if the call is not a valid decorate call.
*/ */
protected reflectDecorateHelperEntry(expression: ts.Expression): DecorateHelperEntry|null {
// We only care about those elements that are actual calls
if (!ts.isCallExpression(expression)) {
return null;
}
const call = expression;
const helperCallFn =
ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression;
if (!ts.isIdentifier(helperCallFn)) {
return null;
}
const name = helperCallFn.text;
if (name === '__metadata') {
// This is a `tslib.__metadata` call, reflect to arguments into a `ParameterTypes` object
// if the metadata key is "design:paramtypes".
const key = call.arguments[0];
if (key === undefined || !ts.isStringLiteral(key) || key.text !== 'design:paramtypes') {
return null;
}
const value = call.arguments[1];
if (value === undefined || !ts.isArrayLiteralExpression(value)) {
return null;
}
return {
type: 'params',
types: Array.from(value.elements),
};
}
if (name === '__param') {
// This is a `tslib.__param` call that is reflected into a `ParameterDecorators` object.
const indexArg = call.arguments[0];
const index = indexArg && ts.isNumericLiteral(indexArg) ? parseInt(indexArg.text, 10) : NaN;
if (isNaN(index)) {
return null;
}
const decoratorCall = call.arguments[1];
if (decoratorCall === undefined || !ts.isCallExpression(decoratorCall)) {
return null;
}
const decorator = this.reflectDecoratorCall(decoratorCall);
if (decorator === null) {
return null;
}
return {
type: 'param:decorators',
index,
decorator,
};
}
// Otherwise attempt to reflect it as a regular decorator.
const decorator = this.reflectDecoratorCall(call);
if (decorator === null) {
return null;
}
return {
type: 'decorator',
decorator,
};
}
protected reflectDecoratorCall(call: ts.CallExpression): Decorator|null { protected reflectDecoratorCall(call: ts.CallExpression): Decorator|null {
const decoratorExpression = call.expression; const decoratorExpression = call.expression;
if (isDecoratorIdentifier(decoratorExpression)) { if (!isDecoratorIdentifier(decoratorExpression)) {
return null;
}
// We found a decorator! // We found a decorator!
const decoratorIdentifier = const decoratorIdentifier =
ts.isIdentifier(decoratorExpression) ? decoratorExpression : decoratorExpression.name; ts.isIdentifier(decoratorExpression) ? decoratorExpression : decoratorExpression.name;
return { return {
name: decoratorIdentifier.text, name: decoratorIdentifier.text,
identifier: decoratorIdentifier, identifier: decoratorIdentifier,
import: this.getImportOfIdentifier(decoratorIdentifier), import: this.getImportOfIdentifier(decoratorIdentifier),
node: call, node: call,
args: Array.from(call.arguments) 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 * Check the given statement to see if it is a call to the specified helper function or null if
@ -1070,14 +1209,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
*/ */
protected getConstructorParamInfo( protected getConstructorParamInfo(
classSymbol: ClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] { classSymbol: ClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] {
const paramsProperty = this.getStaticProperty(classSymbol, CONSTRUCTOR_PARAMS); const {constructorParamInfo} = this.acquireDecoratorInfo(classSymbol);
const paramInfo: ParamInfo[]|null = paramsProperty ?
this.getParamInfoFromStaticProperty(paramsProperty) :
this.getParamInfoFromHelperCall(classSymbol, parameterNodes);
return parameterNodes.map((node, index) => { return parameterNodes.map((node, index) => {
const {decorators, typeExpression} = paramInfo && paramInfo[index] ? const {decorators, typeExpression} = constructorParamInfo[index] ?
paramInfo[index] : constructorParamInfo[index] :
{decorators: null, typeExpression: null}; {decorators: null, typeExpression: null};
const nameNode = node.name; const nameNode = node.name;
return { return {
@ -1153,58 +1289,6 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
return null; return null;
} }
/**
* Get the parameter type and decorators for a class where the information is stored via
* 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: ClassSymbol, parameterNodes: ts.ParameterDeclaration[]): ParamInfo[] {
const parameters: ParamInfo[] =
parameterNodes.map(() => ({typeExpression: 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].typeExpression = 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. * 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 classSymbol the class whose helper calls we are interested in.
@ -1377,6 +1461,72 @@ export type ParamInfo = {
typeExpression: ts.Expression | null typeExpression: ts.Expression | null
}; };
/**
* Represents a call to `tslib.__metadata` as present in `tslib.__decorate` calls. This is a
* synthetic decorator inserted by TypeScript that contains reflection information about the
* target of the decorator, i.e. the class or property.
*/
export interface ParameterTypes {
type: 'params';
types: ts.Expression[];
}
/**
* Represents a call to `tslib.__param` as present in `tslib.__decorate` calls. This contains
* information on any decorators were applied to a certain parameter.
*/
export interface ParameterDecorators {
type: 'param:decorators';
index: number;
decorator: Decorator;
}
/**
* Represents a call to a decorator as it was present in the original source code, as present in
* `tslib.__decorate` calls.
*/
export interface DecoratorCall {
type: 'decorator';
decorator: Decorator;
}
/**
* Represents the different kinds of decorate helpers that may be present as first argument to
* `tslib.__decorate`, as follows:
*
* ```
* __decorate([
* Directive({ selector: '[someDirective]' }),
* tslib_1.__param(2, Inject(INJECTED_TOKEN)),
* tslib_1.__metadata("design:paramtypes", [ViewContainerRef, TemplateRef, String])
* ], SomeDirective);
* ```
*/
export type DecorateHelperEntry = ParameterTypes | ParameterDecorators | DecoratorCall;
/**
* The recorded decorator information of a single class. This information is cached in the host.
*/
interface DecoratorInfo {
/**
* All decorators that were present on the class. If no decorators were present, this is `null`
*/
classDecorators: Decorator[]|null;
/**
* All decorators per member of the class they were present on.
*/
memberDecorators: Map<string, Decorator[]>;
/**
* Represents the constructor parameter information, such as the type of a parameter and all
* decorators for a certain parameter. Indices in this array correspond with the parameter's index
* in the constructor. Note that this array may be sparse, i.e. certain constructor parameters may
* not have any info recorded.
*/
constructorParamInfo: ParamInfo[];
}
/** /**
* A statement node that represents an assignment. * A statement node that represents an assignment.
*/ */
@ -1397,27 +1547,55 @@ export function isAssignment(node: ts.Node): node is ts.AssignmentExpression<ts.
} }
/** /**
* The type of a function that can be used to filter out helpers based on their target. * Tests whether the provided call expression targets a class, by verifying its arguments are
* This is used in `reflectDecoratorsFromHelperCall()`. * according to the following form:
*
* ```
* __decorate([], SomeDirective);
* ```
*
* @param call the call expression that is tested to represent a class decorator call.
* @param className the name of the class that the call needs to correspond with.
*/ */
export type TargetFilter = (target: ts.Expression) => boolean; export function isClassDecorateCall(call: ts.CallExpression, className: string):
call is ts.CallExpression&{arguments: [ts.ArrayLiteralExpression, ts.Expression]} {
const helperArgs = call.arguments[0];
if (helperArgs === undefined || !ts.isArrayLiteralExpression(helperArgs)) {
return false;
}
/** const target = call.arguments[1];
* Creates a function that tests whether the given expression is a class target. return target !== undefined && ts.isIdentifier(target) && target.text === className;
* @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. * Tests whether the provided call expression targets a member of the class, by verifying its
* @param className the name of the class we want to target. * arguments are according to the following form:
*
* ```
* __decorate([], SomeDirective.prototype, "member", void 0);
* ```
*
* @param call the call expression that is tested to represent a member decorator call.
* @param className the name of the class that the call needs to correspond with.
*/ */
export function makeMemberTargetFilter(className: string): TargetFilter { export function isMemberDecorateCall(call: ts.CallExpression, className: string):
return (target: ts.Expression): boolean => ts.isPropertyAccessExpression(target) && call is ts.CallExpression&
ts.isIdentifier(target.expression) && target.expression.text === className && {arguments: [ts.ArrayLiteralExpression, ts.StringLiteral, ts.StringLiteral]} {
target.name.text === 'prototype'; const helperArgs = call.arguments[0];
if (helperArgs === undefined || !ts.isArrayLiteralExpression(helperArgs)) {
return false;
}
const target = call.arguments[1];
if (target === undefined || !ts.isPropertyAccessExpression(target) ||
!ts.isIdentifier(target.expression) || target.expression.text !== className ||
target.name.text !== 'prototype') {
return false;
}
const memberName = call.arguments[2];
return memberName !== undefined && ts.isStringLiteral(memberName);
} }
/** /**

View File

@ -78,6 +78,22 @@ export function convertToDirectTsLibImport(filesystem: TestFile[]) {
}); });
} }
export function convertToInlineTsLib(filesystem: TestFile[]) {
return filesystem.map(file => {
const contents = file.contents
.replace(`import * as tslib_1 from 'tslib';`, `
var __decorate = null;
var __metadata = null;
var __read = null;
var __values = null;
var __param = null;
var __extends = null;
var __assign = null;
`).replace(/tslib_1\./g, '');
return {...file, contents};
});
}
export function getRootFiles(testFiles: TestFile[]): AbsoluteFsPath[] { export function getRootFiles(testFiles: TestFile[]): AbsoluteFsPath[] {
return testFiles.filter(f => f.isRoot !== false).map(f => absoluteFrom(f.name)); return testFiles.filter(f => f.isRoot !== false).map(f => absoluteFrom(f.name));
} }

View File

@ -951,23 +951,6 @@ exports.ExternalModule = ExternalModule;
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'};
const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo);
const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
});
describe('(returned decorators `args`)', () => { describe('(returned decorators `args`)', () => {
it('should be an empty array if decorator has no `args` property', () => { it('should be an empty array if decorator has no `args` property', () => {
loadTestFiles([INVALID_DECORATOR_ARGS_FILE]); loadTestFiles([INVALID_DECORATOR_ARGS_FILE]);
@ -1185,22 +1168,17 @@ exports.ExternalModule = ExternalModule;
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import;
const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo);
const classNode = getDeclaration( const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Directive', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
}); });
describe('(returned prop decorators `args`)', () => { describe('(returned prop decorators `args`)', () => {
@ -1430,24 +1408,19 @@ exports.ExternalModule = ExternalModule;
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost); const host = new CommonJsReflectionHost(new MockLogger(), false, program, compilerHost);
const classNode = getDeclaration( const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'};
const spy = spyOn(CommonJsReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const parameters = host.getConstructorParameters(classNode); const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !; const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].name).toBe('Inject');
expect(decorators[0].import).toEqual({name: 'Inject', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
}); });
}); });

View File

@ -15,7 +15,7 @@ import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host'; import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils'; import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util'; import {expectTypeValueReferencesForParameters} from './util';
@ -111,14 +111,16 @@ runInEachFileSystem(() => {
]; ];
const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES); const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
const INLINE_FILES = convertToInlineTsLib(NAMESPACED_IMPORT_FILES);
FILES = { FILES = {
'namespaced': NAMESPACED_IMPORT_FILES, 'namespaced': NAMESPACED_IMPORT_FILES,
'direct import': DIRECT_IMPORT_FILES, 'direct import': DIRECT_IMPORT_FILES,
'inline': INLINE_FILES,
}; };
}); });
['namespaced', 'direct import'].forEach(label => { ['namespaced', 'direct import', 'inline'].forEach(label => {
describe(`[${label}]`, () => { describe(`[${label}]`, () => {
beforeEach(() => { beforeEach(() => {
const fs = getFileSystem(); const fs = getFileSystem();
@ -147,29 +149,6 @@ runInEachFileSystem(() => {
]); ]);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
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', () => { it('should support decorators being used inside @angular/core', () => {
const {program} = const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
@ -272,21 +251,6 @@ runInEachFileSystem(() => {
expect(staticProperty.value !.getText()).toEqual(`'static'`); expect(staticProperty.value !.getText()).toEqual(`'static'`);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host =
new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
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', () => { it('should support decorators being used inside @angular/core', () => {
const {program} = const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));

View File

@ -763,11 +763,7 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
const mockImportInfo = { from: '@angular/core' } as Import;
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -776,10 +772,7 @@ runInEachFileSystem(() => {
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Directive', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
}); });
describe('(returned decorators `args`)', () => { describe('(returned decorators `args`)', () => {
@ -839,11 +832,13 @@ runInEachFileSystem(() => {
expect(input1.kind).toEqual(ClassMemberKind.Property); expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false); expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
expect(input1.decorators ![0].import).toEqual({name: 'Input', from: '@angular/core'});
const input2 = members.find(member => member.name === 'input2') !; const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property); expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false); expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); expect(input2.decorators !.map(d => d.name)).toEqual(['Input']);
expect(input2.decorators ![0].import).toEqual({name: 'Input', from: '@angular/core'});
}); });
it('should find non decorated properties on a class', () => { it('should find non decorated properties on a class', () => {
@ -991,35 +986,6 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Input'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Input'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
let callCount = 0;
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => {
callCount++;
return {name: `name${callCount}`, from: '@angular/core'};
});
loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedClassDeclaration);
const members = host.getMembersOfClass(classNode);
expect(spy).toHaveBeenCalled();
expect(spy.calls.allArgs().map(arg => arg[0].getText())).toEqual([
'Input',
'Input',
'HostBinding',
'Input',
'HostListener',
]);
const member = members.find(member => member.name === 'input1') !;
expect(member.decorators !.length).toBe(1);
expect(member.decorators ![0].import).toEqual({name: 'name1', from: '@angular/core'});
});
describe('(returned prop decorators `args`)', () => { describe('(returned prop decorators `args`)', () => {
it('should be an empty array if prop decorator has no `args` property', () => { it('should be an empty array if prop decorator has no `args` property', () => {
loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]);
@ -1311,11 +1277,7 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
const mockImportInfo: Import = {name: 'mock', from: '@angular/core'};
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm2015ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -1325,10 +1287,7 @@ runInEachFileSystem(() => {
const decorators = parameters[2].decorators !; const decorators = parameters[2].decorators !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Inject', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
}); });
}); });

View File

@ -14,7 +14,7 @@ import {getDeclaration} from '../../../src/ngtsc/testing';
import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers'; import {loadFakeCore, loadTestFiles, loadTsLib} from '../../../test/helpers';
import {Esm5ReflectionHost} from '../../src/host/esm5_host'; import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {MockLogger} from '../helpers/mock_logger'; import {MockLogger} from '../helpers/mock_logger';
import {convertToDirectTsLibImport, makeTestBundleProgram} from '../helpers/utils'; import {convertToDirectTsLibImport, convertToInlineTsLib, makeTestBundleProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util'; import {expectTypeValueReferencesForParameters} from './util';
@ -132,14 +132,16 @@ export { SomeDirective };
]; ];
const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES); const DIRECT_IMPORT_FILES = convertToDirectTsLibImport(NAMESPACED_IMPORT_FILES);
const INLINE_FILES = convertToInlineTsLib(NAMESPACED_IMPORT_FILES);
FILES = { FILES = {
'namespaced': NAMESPACED_IMPORT_FILES, 'namespaced': NAMESPACED_IMPORT_FILES,
'direct import': DIRECT_IMPORT_FILES, 'direct import': DIRECT_IMPORT_FILES,
'inline': INLINE_FILES,
}; };
}); });
['namespaced', 'direct import'].forEach(label => { ['namespaced', 'direct import', 'inline'].forEach(label => {
describe(`[${label}]`, () => { describe(`[${label}]`, () => {
beforeEach(() => { beforeEach(() => {
const fs = getFileSystem(); const fs = getFileSystem();
@ -167,28 +169,6 @@ export { SomeDirective };
]); ]);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
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', () => { it('should support decorators being used inside @angular/core', () => {
const {program} = const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
@ -270,20 +250,6 @@ export { SomeDirective };
expect(staticProperty.value !.getText()).toEqual(`'static'`); expect(staticProperty.value !.getText()).toEqual(`'static'`);
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, _('/some_directive.js'), 'SomeDirective', isNamedVariableDeclaration);
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', () => { it('should support decorators being used inside @angular/core', () => {
const {program} = const {program} =
makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js')); makeTestBundleProgram(_('/node_modules/@angular/core/some_directive.js'));
@ -319,12 +285,7 @@ export { SomeDirective };
}); });
describe('(returned parameters `decorators`)', () => { describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const {program} = makeTestBundleProgram(_('/some_directive.js')); const {program} = makeTestBundleProgram(_('/some_directive.js'));
const host = const host =
new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -334,10 +295,7 @@ export { SomeDirective };
const decorators = parameters ![2].decorators !; const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Inject', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
}); });
}); });
}); });

View File

@ -942,11 +942,7 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -955,10 +951,7 @@ runInEachFileSystem(() => {
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Directive', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
}); });
describe('(returned decorators `args`)', () => { describe('(returned decorators `args`)', () => {
@ -1019,11 +1012,13 @@ runInEachFileSystem(() => {
expect(input1.kind).toEqual(ClassMemberKind.Property); expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false); expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']); expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
expect(input1.decorators ![0].import).toEqual({name: 'Input', from: '@angular/core'});
const input2 = members.find(member => member.name === 'input2') !; const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property); expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false); expect(input2.isStatic).toEqual(false);
expect(input2.decorators !.map(d => d.name)).toEqual(['Input']); expect(input2.decorators !.map(d => d.name)).toEqual(['Input']);
expect(input2.decorators ![0].import).toEqual({name: 'Input', from: '@angular/core'});
}); });
it('should find decorated members on a class', () => { it('should find decorated members on a class', () => {
@ -1232,30 +1227,6 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Input'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Input'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
let callCount = 0;
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.callFake(() => {
callCount++;
return {name: `name${callCount}`, from: `@angular/core`};
});
loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const members = host.getMembersOfClass(classNode);
expect(spy).toHaveBeenCalled();
spy.calls.allArgs().forEach(arg => expect(arg[0].getText()).toEqual('Input'));
const index = members.findIndex(member => member.name === 'input1');
expect(members[index].decorators !.length).toBe(1);
expect(members[index].decorators ![0].import)
.toEqual({name: 'name1', from: '@angular/core'});
});
describe('(returned prop decorators `args`)', () => { describe('(returned prop decorators `args`)', () => {
it('should be an empty array if prop decorator has no `args` property', () => { it('should be an empty array if prop decorator has no `args` property', () => {
loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]); loadTestFiles([INVALID_PROP_DECORATOR_ARGS_FILE]);
@ -1466,11 +1437,7 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Inject'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
const mockImportInfo = { name: 'mock', from: '@angulare/core' } as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker()); const host = new Esm5ReflectionHost(new MockLogger(), false, program.getTypeChecker());
@ -1480,10 +1447,7 @@ runInEachFileSystem(() => {
const decorators = parameters ![2].decorators !; const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Inject', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
}); });
}); });

View File

@ -1057,22 +1057,17 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const mockImportInfo: Import = {from: '@angular/core', name: 'Directive'};
const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo);
const classNode = getDeclaration( const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Directive', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
}); });
it('should find decorated members on a class at the top level', () => { it('should find decorated members on a class at the top level', () => {
@ -1290,22 +1285,17 @@ runInEachFileSystem(() => {
expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'})); expect(decorators[0]).toEqual(jasmine.objectContaining({name: 'Directive'}));
}); });
it('should use `getImportOfIdentifier()` to retrieve import info', () => { it('should have import information on decorators', () => {
loadTestFiles([SOME_DIRECTIVE_FILE]); loadTestFiles([SOME_DIRECTIVE_FILE]);
const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name); const {program, host: compilerHost} = makeTestBundleProgram(SOME_DIRECTIVE_FILE.name);
const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost); const host = new UmdReflectionHost(new MockLogger(), false, program, compilerHost);
const mockImportInfo = { name: 'mock', from: '@angular/core' } as Import;
const spy = spyOn(host, 'getImportOfIdentifier').and.returnValue(mockImportInfo);
const classNode = getDeclaration( const classNode = getDeclaration(
program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration); program, SOME_DIRECTIVE_FILE.name, 'SomeDirective', isNamedVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !; const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1); expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo); expect(decorators[0].import).toEqual({name: 'Directive', from: '@angular/core'});
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Directive');
}); });
describe('(returned prop decorators `args`)', () => { describe('(returned prop decorators `args`)', () => {