1635 lines
64 KiB
TypeScript
Raw Normal View History

/**
* @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 {ClassDeclaration, ClassMember, ClassMemberKind, ClassSymbol, CtorParameter, Declaration, Decorator, Import, TypeScriptReflectionHost, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {Logger} from '../logging/logger';
import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined} from '../utils';
import {DecoratedClass} from './decorated_class';
import {ModuleWithProvidersFunction, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String;
export const PROP_DECORATORS = 'propDecorators' as ts.__String;
export const CONSTRUCTOR = '__constructor' as ts.__String;
export const CONSTRUCTOR_PARAMS = 'ctorParameters' as ts.__String;
/**
* Esm2015 packages contain ECMAScript 2015 classes, etc.
* Decorators are defined via static properties on the class. For example:
*
* ```
* class SomeDirective {
* }
* SomeDirective.decorators = [
* { type: Directive, args: [{ selector: '[someDirective]' },] }
* ];
* SomeDirective.ctorParameters = () => [
* { type: ViewContainerRef, },
* { type: TemplateRef, },
* { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
* ];
* SomeDirective.propDecorators = {
* "input1": [{ type: Input },],
* "input2": [{ type: Input },],
* };
* ```
*
* * Classes are decorated if they have a static property called `decorators`.
* * Members are decorated if there is a matching key on a static property
* called `propDecorators`.
* * Constructor parameters decorators are found on an object returned from
* a static method called `ctorParameters`.
*/
export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements NgccReflectionHost {
protected dtsDeclarationMap: Map<string, ts.Declaration>|null;
/**
* The set of source files that have already been preprocessed.
*/
protected preprocessedSourceFiles = new Set<ts.SourceFile>();
/**
* In ES2015, class declarations may have been down-leveled into variable declarations,
* initialized using a class expression. In certain scenarios, an additional variable
* is introduced that represents the class so that results in code such as:
*
* ```
* let MyClass_1; let MyClass = MyClass_1 = class MyClass {};
* ```
*
* This map tracks those aliased variables to their original identifier, i.e. the key
* corresponds with the declaration of `MyClass_1` and its value becomes the `MyClass` identifier
* of the variable declaration.
*
* This map is populated during the preprocessing of each source file.
*/
protected aliasedClassDeclarations = new Map<ts.Declaration, ts.Identifier>();
constructor(
protected logger: Logger, protected isCore: boolean, checker: ts.TypeChecker,
dts?: BundleProgram|null) {
super(checker);
this.dtsDeclarationMap = dts && this.computeDtsDeclarationMap(dts.path, dts.program) || null;
}
/**
* Find the declaration of a node that we think is a class.
* Classes should have a `name` identifier, because they may need to be referenced in other parts
* of the program.
*
* In ES2015, a class may be declared using a variable declaration of the following structure:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` node is returned as declaration of `MyClass`.
*
* @param node the node that represents the class whose declaration we are finding.
* @returns the declaration of the class or `undefined` if it is not a "class".
*/
getClassDeclaration(node: ts.Node): ClassDeclaration|undefined {
return getInnerClassDeclaration(node) || undefined;
}
/**
* Find a symbol for a node that we think is a class.
* @param node the node whose symbol we are finding.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
getClassSymbol(declaration: ts.Node): ClassSymbol|undefined {
const classDeclaration = this.getClassDeclaration(declaration);
return classDeclaration &&
this.checker.getSymbolAtLocation(classDeclaration.name) as ClassSymbol;
}
/**
* Examine a declaration (for example, of a class or function) and return metadata about any
* decorators present on the declaration.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class or function over
* which to reflect. For example, if the intent is to reflect the decorators of a class and the
* source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5
* format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the
* result of an IIFE execution.
*
* @returns an array of `Decorator` metadata if decorators are present on the declaration, or
* `null` if either no decorators were present or if the declaration is not of a decoratable type.
*/
getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null {
const symbol = this.getClassSymbol(declaration);
if (!symbol) {
return null;
}
return this.getDecoratorsOfSymbol(symbol);
}
/**
* Examine a declaration which should be of a class, and return metadata about the members of the
* class.
*
* @param clazz a `ClassDeclaration` representing the class over which to reflect.
*
* @returns an array of `ClassMember` metadata representing the members of the class.
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ClassDeclaration): ClassMember[] {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(`Attempted to get members of a non-class: "${clazz.getText()}"`);
}
return this.getMembersOfSymbol(classSymbol);
}
/**
* Reflect over the constructor of a class and return metadata about its parameters.
*
* This method only looks at the constructor of a class directly and not at any inherited
* constructors.
*
* @param clazz a `ClassDeclaration` representing the class over which to reflect.
*
* @returns an array of `Parameter` metadata representing the parameters of the constructor, if
* a constructor exists. If the constructor exists and has 0 parameters, this array will be empty.
* If the class has no constructor, this method returns `null`.
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getConstructorParameters(clazz: ClassDeclaration): CtorParameter[]|null {
const classSymbol = this.getClassSymbol(clazz);
if (!classSymbol) {
throw new Error(
`Attempted to get constructor parameters of a non-class: "${clazz.getText()}"`);
}
const parameterNodes = this.getConstructorParameterDeclarations(classSymbol);
if (parameterNodes) {
return this.getConstructorParamInfo(classSymbol, parameterNodes);
}
return null;
}
hasBaseClass(clazz: ClassDeclaration): boolean {
const superHasBaseClass = super.hasBaseClass(clazz);
if (superHasBaseClass) {
return superHasBaseClass;
}
const innerClassDeclaration = getInnerClassDeclaration(clazz);
if (innerClassDeclaration === null) {
return false;
}
return innerClassDeclaration.heritageClauses !== undefined &&
innerClassDeclaration.heritageClauses.some(
clause => clause.token === ts.SyntaxKind.ExtendsKeyword);
}
/**
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ClassDeclaration {
return super.isClass(node) || !!this.getClassDeclaration(node);
}
/**
* Trace an identifier to its declaration, if possible.
*
* This method attempts to resolve the declaration of the given identifier, tracing back through
* imports and re-exports until the original declaration statement is found. A `Declaration`
* object is returned if the original declaration is found, or `null` is returned otherwise.
*
* In ES2015, we need to account for identifiers that refer to aliased class declarations such as
* `MyClass_1`. Since such declarations are only available within the module itself, we need to
* find the original class declaration, e.g. `MyClass`, that is associated with the aliased one.
*
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
*
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
* otherwise.
*/
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
const superDeclaration = super.getDeclarationOfIdentifier(id);
// The identifier may have been of an additional class assignment such as `MyClass_1` that was
// present as alias for `MyClass`. If so, resolve such aliases to their original declaration.
if (superDeclaration !== null) {
const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node);
if (aliasedIdentifier !== null) {
return this.getDeclarationOfIdentifier(aliasedIdentifier);
}
}
return superDeclaration;
}
/**
* Search the given module for variable declarations in which the initializer
* is an identifier marked with the `PRE_R3_MARKER`.
* @param module the module in which to search for switchable declarations.
* @returns an array of variable declarations that match.
*/
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] {
// Don't bother to walk the AST if the marker is not found in the text
return module.getText().indexOf(PRE_R3_MARKER) >= 0 ?
findAll(module, isSwitchableVariableDeclaration) :
[];
}
getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null {
const value = super.getVariableValue(declaration);
if (value) {
return value;
}
// We have a variable declaration that has no initializer. For example:
//
// ```
// var HttpClientXsrfModule_1;
// ```
//
// So look for the special scenario where the variable is being assigned in
// a nearby statement to the return value of a call to `__decorate`.
// Then find the 2nd argument of that call, the "target", which will be the
// actual class identifier. For example:
//
// ```
// HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
// NgModule({
// providers: [],
// })
// ], HttpClientXsrfModule);
// ```
//
// And finally, find the declaration of the identifier in that argument.
// Note also that the assignment can occur within another assignment.
//
const block = declaration.parent.parent.parent;
const symbol = this.checker.getSymbolAtLocation(declaration.name);
if (symbol && (ts.isBlock(block) || ts.isSourceFile(block))) {
const decorateCall = this.findDecoratedVariableValue(block, symbol);
const target = decorateCall && decorateCall.arguments[1];
if (target && ts.isIdentifier(target)) {
const targetSymbol = this.checker.getSymbolAtLocation(target);
const targetDeclaration = targetSymbol && targetSymbol.valueDeclaration;
if (targetDeclaration) {
if (ts.isClassDeclaration(targetDeclaration) ||
ts.isFunctionDeclaration(targetDeclaration)) {
// The target is just a function or class declaration
// so return its identifier as the variable value.
return targetDeclaration.name || null;
} else if (ts.isVariableDeclaration(targetDeclaration)) {
// The target is a variable declaration, so find the far right expression,
// in the case of multiple assignments (e.g. `var1 = var2 = value`).
let targetValue = targetDeclaration.initializer;
while (targetValue && isAssignment(targetValue)) {
targetValue = targetValue.right;
}
if (targetValue) {
return targetValue;
}
}
}
}
}
return null;
}
/**
* Find all the classes that contain decorations in a given file.
* @param sourceFile The source file to search for decorated classes.
* @returns An array of decorated classes.
*/
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[] {
const classes: DecoratedClass[] = [];
this.getModuleStatements(sourceFile).forEach(statement => {
if (ts.isVariableStatement(statement)) {
statement.declarationList.declarations.forEach(declaration => {
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(declaration));
if (decoratedClass) {
classes.push(decoratedClass);
}
});
} else if (ts.isClassDeclaration(statement)) {
const decoratedClass = this.getDecoratedClassFromSymbol(this.getClassSymbol(statement));
if (decoratedClass) {
classes.push(decoratedClass);
}
}
});
return classes;
}
/**
* Get the number of generic type parameters of a given class.
*
* @param clazz a `ClassDeclaration` representing the class over which to reflect.
*
* @returns the number of type parameters of the class, if known, or `null` if the declaration
* is not a class or has an unknown number of type parameters.
*/
getGenericArityOfClass(clazz: ClassDeclaration): number|null {
const dtsDeclaration = this.getDtsDeclaration(clazz);
if (dtsDeclaration && ts.isClassDeclaration(dtsDeclaration)) {
return dtsDeclaration.typeParameters ? dtsDeclaration.typeParameters.length : 0;
}
return null;
}
/**
* Take an exported declaration of a class (maybe down-leveled to a variable) and look up the
* declaration of its type in a separate .d.ts tree.
*
* This function is allowed to return `null` if the current compilation unit does not have a
* separate .d.ts tree. When compiling TypeScript code this is always the case, since .d.ts files
* are produced only during the emit of such a compilation. When compiling .js code, however,
* there is frequently a parallel .d.ts tree which this method exposes.
*
* Note that the `ts.ClassDeclaration` returned from this function may not be from the same
* `ts.Program` as the input declaration.
*/
getDtsDeclaration(declaration: ts.Declaration): ts.Declaration|null {
if (!this.dtsDeclarationMap) {
return null;
}
if (!isNamedDeclaration(declaration)) {
throw new Error(
`Cannot get the dts file for a declaration that has no name: ${declaration.getText()} in ${declaration.getSourceFile().fileName}`);
}
return this.dtsDeclarationMap.get(declaration.name.text) || null;
}
/**
* Search the given source file for exported functions and static class methods that return
* ModuleWithProviders objects.
* @param f The source file to search for these functions
* @returns An array of function declarations that look like they return ModuleWithProviders
* objects.
*/
getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersFunction[] {
const exports = this.getExportsOfModule(f);
if (!exports) return [];
const infos: ModuleWithProvidersFunction[] = [];
exports.forEach((declaration, name) => {
if (this.isClass(declaration.node)) {
this.getMembersOfClass(declaration.node).forEach(member => {
if (member.isStatic) {
const info = this.parseForModuleWithProviders(
member.name, member.node, member.implementation, declaration.node);
if (info) {
infos.push(info);
}
}
});
} else {
if (isNamedDeclaration(declaration.node)) {
const info =
this.parseForModuleWithProviders(declaration.node.name.text, declaration.node);
if (info) {
infos.push(info);
}
}
}
});
return infos;
}
///////////// Protected Helpers /////////////
/**
* Finds the identifier of the actual class declaration for a potentially aliased declaration of a
* class.
*
* If the given declaration is for an alias of a class, this function will determine an identifier
* to the original declaration that represents this class.
*
* @param declaration The declaration to resolve.
* @returns The original identifier that the given class declaration resolves to, or `undefined`
* if the declaration does not represent an aliased class.
*/
protected resolveAliasedClassIdentifier(declaration: ts.Declaration): ts.Identifier|null {
this.ensurePreprocessed(declaration.getSourceFile());
return this.aliasedClassDeclarations.get(declaration) || null;
}
/**
* Ensures that the source file that `node` is part of has been preprocessed.
*
* During preprocessing, all statements in the source file will be visited such that certain
* processing steps can be done up-front and cached for subsequent usages.
*
* @param sourceFile The source file that needs to have gone through preprocessing.
*/
protected ensurePreprocessed(sourceFile: ts.SourceFile): void {
if (!this.preprocessedSourceFiles.has(sourceFile)) {
this.preprocessedSourceFiles.add(sourceFile);
for (const statement of sourceFile.statements) {
this.preprocessStatement(statement);
}
}
}
/**
* Analyzes the given statement to see if it corresponds with a variable declaration like
* `let MyClass = MyClass_1 = class MyClass {};`. If so, the declaration of `MyClass_1`
* is associated with the `MyClass` identifier.
*
* @param statement The statement that needs to be preprocessed.
*/
protected preprocessStatement(statement: ts.Statement): void {
if (!ts.isVariableStatement(statement)) {
return;
}
const declarations = statement.declarationList.declarations;
if (declarations.length !== 1) {
return;
}
const declaration = declarations[0];
const initializer = declaration.initializer;
if (!ts.isIdentifier(declaration.name) || !initializer || !isAssignment(initializer) ||
!ts.isIdentifier(initializer.left) || !ts.isClassExpression(initializer.right)) {
return;
}
const aliasedIdentifier = initializer.left;
const aliasedDeclaration = this.getDeclarationOfIdentifier(aliasedIdentifier);
if (aliasedDeclaration === null) {
throw new Error(
`Unable to locate declaration of ${aliasedIdentifier.text} in "${statement.getText()}"`);
}
this.aliasedClassDeclarations.set(aliasedDeclaration.node, declaration.name);
}
/** Get the top level statements for a module.
*
* In ES5 and ES2015 this is just the top level statements of the file.
* @param sourceFile The module whose statements we want.
* @returns An array of top level statements for the given module.
*/
protected getModuleStatements(sourceFile: ts.SourceFile): ts.Statement[] {
return Array.from(sourceFile.statements);
}
protected getDecoratorsOfSymbol(symbol: ClassSymbol): Decorator[]|null {
const decoratorsProperty = this.getStaticProperty(symbol, DECORATORS);
if (decoratorsProperty) {
return this.getClassDecoratorsFromStaticProperty(decoratorsProperty);
} else {
return this.getClassDecoratorsFromHelperCall(symbol);
}
}
protected getDecoratedClassFromSymbol(symbol: ClassSymbol|undefined): DecoratedClass|null {
if (symbol) {
const decorators = this.getDecoratorsOfSymbol(symbol);
if (decorators && decorators.length) {
return new DecoratedClass(symbol.name, symbol.valueDeclaration, decorators);
}
}
return null;
}
/**
* Walk the AST looking for an assignment to the specified symbol.
* @param node The current node we are searching.
* @returns an expression that represents the value of the variable, or undefined if none can be
* found.
*/
protected findDecoratedVariableValue(node: ts.Node|undefined, symbol: ts.Symbol):
ts.CallExpression|null {
if (!node) {
return null;
}
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
const left = node.left;
const right = node.right;
if (ts.isIdentifier(left) && this.checker.getSymbolAtLocation(left) === symbol) {
return (ts.isCallExpression(right) && getCalleeName(right) === '__decorate') ? right : null;
}
return this.findDecoratedVariableValue(right, symbol);
}
return node.forEachChild(node => this.findDecoratedVariableValue(node, symbol)) || null;
}
/**
* Try to retrieve the symbol of a static property on a class.
* @param symbol the class whose property we are interested in.
* @param propertyName the name of static property.
* @returns the symbol if it is found or `undefined` if not.
*/
protected getStaticProperty(symbol: ClassSymbol, propertyName: ts.__String): ts.Symbol|undefined {
return symbol.exports && symbol.exports.get(propertyName);
}
/**
* Get all class decorators for the given class, where the decorators are declared
* via a static property. For example:
*
* ```
* class SomeDirective {}
* SomeDirective.decorators = [
* { type: Directive, args: [{ selector: '[someDirective]' },] }
* ];
* ```
*
* @param decoratorsSymbol the property containing the decorators we want to get.
* @returns an array of decorators or null if none where found.
*/
protected getClassDecoratorsFromStaticProperty(decoratorsSymbol: ts.Symbol): Decorator[]|null {
const decoratorsIdentifier = decoratorsSymbol.valueDeclaration;
if (decoratorsIdentifier && decoratorsIdentifier.parent) {
if (ts.isBinaryExpression(decoratorsIdentifier.parent) &&
decoratorsIdentifier.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
// AST of the array of decorator values
const decoratorsArray = decoratorsIdentifier.parent.right;
return this.reflectDecorators(decoratorsArray)
.filter(decorator => this.isFromCore(decorator));
}
}
return null;
}
/**
* Get all class decorators for the given class, where the decorators are declared
* via the `__decorate` helper method. For example:
*
* ```
* let SomeDirective = class SomeDirective {}
* SomeDirective = __decorate([
* Directive({ selector: '[someDirective]' }),
* ], SomeDirective);
* ```
*
* @param symbol the class whose decorators we want to get.
* @returns an array of decorators or null if none where found.
*/
protected getClassDecoratorsFromHelperCall(symbol: 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.
*
* @param symbol the `ClassSymbol` representing the class over which to reflect.
* @returns an array of `ClassMember` metadata representing the members of the class.
*/
protected getMembersOfSymbol(symbol: ClassSymbol): ClassMember[] {
const members: ClassMember[] = [];
// The decorators map contains all the properties that are decorated
const decoratorsMap = this.getMemberDecorators(symbol);
// The member map contains all the method (instance and static); and any instance properties
// that are initialized in the class.
if (symbol.members) {
symbol.members.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators);
if (reflectedMembers) {
decoratorsMap.delete(key as string);
members.push(...reflectedMembers);
}
});
}
// The static property map contains all the static properties
if (symbol.exports) {
symbol.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators, true);
if (reflectedMembers) {
decoratorsMap.delete(key as string);
members.push(...reflectedMembers);
}
});
}
// 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 = ...;
// ```
const variableDeclaration = getVariableDeclarationOfDeclaration(symbol.valueDeclaration);
if (variableDeclaration !== undefined) {
const variableSymbol = this.checker.getSymbolAtLocation(variableDeclaration.name);
if (variableSymbol && variableSymbol.exports) {
variableSymbol.exports.forEach((value, key) => {
const decorators = decoratorsMap.get(key as string);
const reflectedMembers = this.reflectMembers(value, decorators, true);
if (reflectedMembers) {
decoratorsMap.delete(key as string);
members.push(...reflectedMembers);
}
});
}
}
// Deal with any decorated properties that were not initialized in the class
decoratorsMap.forEach((value, key) => {
members.push({
implementation: null,
decorators: value,
isStatic: false,
kind: ClassMemberKind.Property,
name: key,
nameNode: null,
node: null,
type: null,
value: null
});
});
return members;
}
/**
* 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:
*
* ```
* SomeDirective.propDecorators = {
* "ngForOf": [{ type: Input },],
* "ngForTrackBy": [{ type: Input },],
* "ngForTemplate": [{ type: Input },],
* };
* ```
*
* @param decoratorsProperty the class whose member decorators we are interested in.
* @returns a map whose keys are the name of the members and whose values are collections of
* decorators for the given member.
*/
protected getMemberDecoratorsFromStaticProperty(decoratorsProperty: ts.Symbol):
Map<string, Decorator[]> {
const memberDecorators = new Map<string, Decorator[]>();
// Symbol of the identifier for `SomeDirective.propDecorators`.
const propDecoratorsMap = getPropertyValueFromSymbol(decoratorsProperty);
if (propDecoratorsMap && ts.isObjectLiteralExpression(propDecoratorsMap)) {
const propertiesMap = reflectObjectLiteral(propDecoratorsMap);
propertiesMap.forEach((value, name) => {
const decorators =
this.reflectDecorators(value).filter(decorator => this.isFromCore(decorator));
if (decorators.length) {
memberDecorators.set(name, decorators);
}
});
}
return memberDecorators;
}
/**
* Member decorators may be declared via helper call statements.
*
* ```
* __decorate([
* Input(),
* __metadata("design:type", String)
* ], SomeDirective.prototype, "input1", void 0);
* ```
*
* @param classSymbol the class whose member decorators we are interested in.
* @returns a map whose keys are the name of the members and whose values are collections of
* decorators for the given member.
*/
protected getMemberDecoratorsFromHelperCalls(classSymbol: ClassSymbol): 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(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[]>();
// First check that the `target` argument is correct
if (targetFilter(helperCall.arguments[1])) {
// Grab the `decorators` argument which should be an array of calls
const decoratorCalls = helperCall.arguments[0];
if (decoratorCalls && ts.isArrayLiteralExpression(decoratorCalls)) {
decoratorCalls.elements.forEach(element => {
// We only care about those elements that are actual calls
if (ts.isCallExpression(element)) {
const decorator = this.reflectDecoratorCall(element);
if (decorator) {
const keyArg = helperCall.arguments[2];
const keyName = keyArg && ts.isStringLiteral(keyArg) ? keyArg.text : undefined;
if (keyName === undefined) {
classDecorators.push(decorator);
} else {
const decorators = memberDecorators.get(keyName) || [];
decorators.push(decorator);
memberDecorators.set(keyName, decorators);
}
}
}
});
}
}
return {classDecorators, memberDecorators};
}
/**
* Extract the decorator information from a call to a decorator as a function.
* This happens when the decorators has been used in a `__decorate` helper call.
* For example:
*
* ```
* __decorate([
* Directive({ selector: '[someDirective]' }),
* ], SomeDirective);
* ```
*
* Here the `Directive` decorator is decorating `SomeDirective` and the options for
* the decorator are passed as arguments to the `Directive()` call.
*
* @param call the call to the decorator.
* @returns a decorator containing the reflected information, or null if the call
* is not a valid decorator call.
*/
protected reflectDecoratorCall(call: ts.CallExpression): Decorator|null {
// The call could be of the form `Decorator(...)` or `namespace_1.Decorator(...)`
const decoratorExpression =
ts.isPropertyAccessExpression(call.expression) ? call.expression.name : call.expression;
if (ts.isIdentifier(decoratorExpression)) {
// We found a decorator!
const decoratorIdentifier = decoratorExpression;
return {
name: decoratorIdentifier.text,
identifier: decoratorIdentifier,
import: this.getImportOfIdentifier(decoratorIdentifier),
node: call,
args: Array.from(call.arguments)
};
}
return null;
}
/**
* Check the given statement to see if it is a call to the specified helper function or null if
* not found.
*
* Matching statements will look like: `tslib_1.__decorate(...);`.
* @param statement the statement that may contain the call.
* @param helperName the name of the helper we are looking for.
* @returns the node that corresponds to the `__decorate(...)` call or null if the statement
* does not match.
*/
protected getHelperCall(statement: ts.Statement, helperName: string): ts.CallExpression|null {
if (ts.isExpressionStatement(statement)) {
let expression = statement.expression;
while (isAssignment(expression)) {
expression = expression.right;
}
if (ts.isCallExpression(expression) && getCalleeName(expression) === helperName) {
return expression;
}
}
return null;
}
/**
* Reflect over the given array node and extract decorator information from each element.
*
* This is used for decorators that are defined in static properties. For example:
*
* ```
* SomeDirective.decorators = [
* { type: Directive, args: [{ selector: '[someDirective]' },] }
* ];
* ```
*
* @param decoratorsArray an expression that contains decorator information.
* @returns an array of decorator info that was reflected from the array node.
*/
protected reflectDecorators(decoratorsArray: ts.Expression): Decorator[] {
const decorators: Decorator[] = [];
if (ts.isArrayLiteralExpression(decoratorsArray)) {
// Add each decorator that is imported from `@angular/core` into the `decorators` array
decoratorsArray.elements.forEach(node => {
// If the decorator is not an object literal expression then we are not interested
if (ts.isObjectLiteralExpression(node)) {
// We are only interested in objects of the form: `{ type: DecoratorType, args: [...] }`
const decorator = reflectObjectLiteral(node);
// Is the value of the `type` property an identifier?
let typeIdentifier = decorator.get('type');
if (typeIdentifier) {
if (ts.isPropertyAccessExpression(typeIdentifier)) {
// the type is in a namespace, e.g. `core.Directive`
typeIdentifier = typeIdentifier.name;
}
if (ts.isIdentifier(typeIdentifier)) {
decorators.push({
name: typeIdentifier.text,
identifier: typeIdentifier,
import: this.getImportOfIdentifier(typeIdentifier), node,
args: getDecoratorArgs(node),
});
}
}
}
});
}
return decorators;
}
/**
* Reflect over a symbol and extract the member information, combining it with the
* provided decorator information, and whether it is a static member.
*
* A single symbol may represent multiple class members in the case of accessors;
* an equally named getter/setter accessor pair is combined into a single symbol.
* When the symbol is recognized as representing an accessor, its declarations are
* analyzed such that both the setter and getter accessor are returned as separate
* class members.
*
* One difference wrt the TypeScript host is that in ES2015, we cannot see which
* accessor originally had any decorators applied to them, as decorators are applied
* to the property descriptor in general, not a specific accessor. If an accessor
* has both a setter and getter, any decorators are only attached to the setter 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 reflectMembers(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean):
ClassMember[]|null {
if (symbol.flags & ts.SymbolFlags.Accessor) {
const members: ClassMember[] = [];
const setter = symbol.declarations && symbol.declarations.find(ts.isSetAccessor);
const getter = symbol.declarations && symbol.declarations.find(ts.isGetAccessor);
const setterMember =
setter && this.reflectMember(setter, ClassMemberKind.Setter, decorators, isStatic);
if (setterMember) {
members.push(setterMember);
// Prevent attaching the decorators to a potential getter. In ES2015, we can't tell where
// the decorators were originally attached to, however we only want to attach them to a
// single `ClassMember` as otherwise ngtsc would handle the same decorators twice.
decorators = undefined;
}
const getterMember =
getter && this.reflectMember(getter, ClassMemberKind.Getter, decorators, isStatic);
if (getterMember) {
members.push(getterMember);
}
return members;
}
let kind: ClassMemberKind|null = null;
if (symbol.flags & ts.SymbolFlags.Method) {
kind = ClassMemberKind.Method;
} else if (symbol.flags & ts.SymbolFlags.Property) {
kind = ClassMemberKind.Property;
}
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
if (!node) {
// If the symbol has been imported from a TypeScript typings file then the compiler
// may pass the `prototype` symbol as an export of the class.
// But this has no declaration. In this case we just quietly ignore it.
return null;
}
const member = this.reflectMember(node, kind, decorators, isStatic);
if (!member) {
return null;
}
return [member];
}
/**
* Reflect over a symbol and extract the member information, combining it with the
* provided decorator information, and whether it is a static member.
* @param node the declaration node for the member to reflect over.
* @param kind the assumed kind of the member, may become more accurate during reflection.
* @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(
node: ts.Declaration, kind: ClassMemberKind|null, decorators?: Decorator[],
isStatic?: boolean): ClassMember|null {
let value: ts.Expression|null = null;
let name: string|null = null;
let nameNode: ts.Identifier|null = null;
if (!isClassMemberType(node)) {
return null;
}
if (isStatic && isPropertyAccess(node)) {
name = node.name.text;
value = kind === ClassMemberKind.Property ? node.parent.right : null;
} else if (isThisAssignment(node)) {
kind = ClassMemberKind.Property;
name = node.left.name.text;
value = node.right;
isStatic = false;
} else if (ts.isConstructorDeclaration(node)) {
kind = ClassMemberKind.Constructor;
name = 'constructor';
isStatic = false;
}
if (kind === null) {
this.logger.warn(`Unknown member type: "${node.getText()}`);
return null;
}
if (!name) {
if (isNamedDeclaration(node)) {
name = node.name.text;
nameNode = node.name;
} else {
return null;
}
}
// If we have still not determined if this is a static or instance member then
// look for the `static` keyword on the declaration
if (isStatic === undefined) {
isStatic = node.modifiers !== undefined &&
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
}
const type: ts.TypeNode = (node as any).type || null;
return {
node,
implementation: node, kind, type, name, nameNode, value, isStatic,
decorators: decorators || []
};
}
/**
* Find the declarations of the constructor parameters of a class identified by its symbol.
* @param classSymbol the class whose parameters we want to find.
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
* the class's constructor or null if there is no constructor.
*/
protected getConstructorParameterDeclarations(classSymbol: ClassSymbol):
ts.ParameterDeclaration[]|null {
const constructorSymbol = classSymbol.members && classSymbol.members.get(CONSTRUCTOR);
if (constructorSymbol) {
// For some reason the constructor does not have a `valueDeclaration` ?!?
const constructor = constructorSymbol.declarations &&
constructorSymbol.declarations[0] as ts.ConstructorDeclaration | undefined;
if (!constructor) {
return [];
}
if (constructor.parameters.length > 0) {
return Array.from(constructor.parameters);
}
if (isSynthesizedConstructor(constructor)) {
return null;
}
return [];
}
return null;
}
/**
* Get the parameter decorators of a class constructor.
*
* @param classSymbol the class whose parameter info we want to get.
* @param parameterNodes the array of TypeScript parameter nodes for this class's constructor.
* @returns an array of constructor parameter info objects.
*/
protected getConstructorParamInfo(
classSymbol: ClassSymbol, 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, typeExpression} = paramInfo && paramInfo[index] ?
paramInfo[index] :
{decorators: null, typeExpression: null};
const nameNode = node.name;
return {
name: getNameText(nameNode),
nameNode,
fix(ivy): reuse default imports in type-to-value references (#29266) This fixes an issue with commit b6f6b117. In this commit, default imports processed in a type-to-value conversion were recorded as non-local imports with a '*' name, and the ImportManager generated a new default import for them. When transpiled to ES2015 modules, this resulted in the following correct code: import i3 from './module'; // somewhere in the file, a value reference of i3: {type: i3} However, when the AST with this synthetic import and reference was transpiled to non-ES2015 modules (for example, to commonjs) an issue appeared: var module_1 = require('./module'); {type: i3} TypeScript renames the imported identifier from i3 to module_1, but doesn't substitute later references to i3. This is because the import and reference are both synthetic, and never went through the TypeScript AST step of "binding" which associates the reference to its import. This association is important during emit when the identifiers might change. Synthetic (transformer-added) imports will never be bound properly. The only possible solution is to reuse the user's original import and the identifier from it, which will be properly downleveled. The issue with this approach (which prompted the fix in b6f6b117) is that if the import is only used in a type position, TypeScript will mark it for deletion in the generated JS, even though additional non-type usages are added in the transformer. This again would leave a dangling import. To work around this, it's necessary for the compiler to keep track of identifiers that it emits which came from default imports, and tell TS not to remove those imports during transpilation. A `DefaultImportTracker` class is implemented to perform this tracking. It implements a `DefaultImportRecorder` interface, which is used to record two significant pieces of information: * when a WrappedNodeExpr is generated which refers to a default imported value, the ts.Identifier is associated to the ts.ImportDeclaration via the recorder. * when that WrappedNodeExpr is later emitted as part of the statement / expression translators, the fact that the ts.Identifier was used is also recorded. Combined, this tracking gives the `DefaultImportTracker` enough information to implement another TS transformer, which can recognize default imports which were used in the output of the Ivy transform and can prevent them from being elided. This is done by creating a new ts.ImportDeclaration for the imports with the same ts.ImportClause. A test verifies that this works. PR Close #29266
2019-03-11 16:54:07 -07:00
typeValueReference: typeExpression !== null ?
{local: true as true, expression: typeExpression, defaultImportStatement: null} :
null,
typeNode: null, decorators
};
});
}
/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static method of the class.
*
* Note that in ESM2015, the method is defined by an arrow function that returns an array of
* decorator and type information.
*
* ```
* SomeDirective.ctorParameters = () => [
* { type: ViewContainerRef, },
* { type: TemplateRef, },
* { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
* ];
* ```
*
* @param paramDecoratorsProperty the property that holds the parameter info we want to get.
* @returns an array of objects containing the type and decorators for each parameter.
*/
protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null {
const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty);
if (paramDecorators && ts.isArrowFunction(paramDecorators)) {
if (ts.isArrayLiteralExpression(paramDecorators.body)) {
const elements = paramDecorators.body.elements;
return elements
.map(
element =>
ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null)
.map(paramInfo => {
const typeExpression = paramInfo && paramInfo.get('type') || null;
const decoratorInfo = paramInfo && paramInfo.get('decorators') || null;
const decorators = decoratorInfo &&
this.reflectDecorators(decoratorInfo)
.filter(decorator => this.isFromCore(decorator));
return {typeExpression, decorators};
});
}
}
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.
* @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: ClassSymbol, 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: ClassSymbol): ts.Statement[] {
return Array.from(classSymbol.valueDeclaration.getSourceFile().statements);
}
/**
* Try to get the import info for this identifier as though it is a namespaced import.
* For example, if the identifier is the `__metadata` part of a property access chain like:
*
* ```
* tslib_1.__metadata
* ```
*
* then it might be that `tslib_1` is a namespace import such as:
*
* ```
* import * as tslib_1 from 'tslib';
* ```
* @param id the TypeScript identifier to find the import info for.
* @returns The import info if this is a namespaced import or `null`.
*/
protected getImportOfNamespacedIdentifier(id: ts.Identifier): Import|null {
if (!(ts.isPropertyAccessExpression(id.parent) && id.parent.name === id)) {
return null;
}
const namespaceIdentifier = getFarLeftIdentifier(id.parent);
const namespaceSymbol =
namespaceIdentifier && this.checker.getSymbolAtLocation(namespaceIdentifier);
const declaration = namespaceSymbol && namespaceSymbol.declarations.length === 1 ?
namespaceSymbol.declarations[0] :
null;
const namespaceDeclaration =
declaration && ts.isNamespaceImport(declaration) ? declaration : null;
if (!namespaceDeclaration) {
return null;
}
const importDeclaration = namespaceDeclaration.parent.parent;
if (!ts.isStringLiteral(importDeclaration.moduleSpecifier)) {
// Should not happen as this would be invalid TypesScript
return null;
}
return {
from: importDeclaration.moduleSpecifier.text,
name: id.text,
};
}
/**
* Test whether a decorator was imported from `@angular/core`.
*
* Is the decorator:
* * externally imported from `@angular/core`?
* * the current hosted program is actually `@angular/core` and
* - relatively internally imported; or
* - not imported, from the current file.
*
* @param decorator the decorator to test.
*/
protected isFromCore(decorator: Decorator): boolean {
if (this.isCore) {
return !decorator.import || /^\./.test(decorator.import.from);
} else {
return !!decorator.import && decorator.import.from === '@angular/core';
}
}
/**
* Extract all the class declarations from the dtsTypings program, storing them in a map
* where the key is the declared name of the class and the value is the declaration itself.
*
* It is possible for there to be multiple class declarations with the same local name.
* Only the first declaration with a given name is added to the map; subsequent classes will be
* ignored.
*
* We are most interested in classes that are publicly exported from the entry point, so these
* are added to the map first, to ensure that they are not ignored.
*
* @param dtsRootFileName The filename of the entry-point to the `dtsTypings` program.
* @param dtsProgram The program containing all the typings files.
* @returns a map of class names to class declarations.
*/
protected computeDtsDeclarationMap(dtsRootFileName: string, dtsProgram: ts.Program):
Map<string, ts.Declaration> {
const dtsDeclarationMap = new Map<string, ts.Declaration>();
const checker = dtsProgram.getTypeChecker();
// First add all the classes that are publicly exported from the entry-point
const rootFile = dtsProgram.getSourceFile(dtsRootFileName);
if (!rootFile) {
throw new Error(`The given file ${dtsRootFileName} is not part of the typings program.`);
}
collectExportedDeclarations(checker, dtsDeclarationMap, rootFile);
// Now add any additional classes that are exported from individual dts files,
// but are not publicly exported from the entry-point.
dtsProgram.getSourceFiles().forEach(
sourceFile => { collectExportedDeclarations(checker, dtsDeclarationMap, sourceFile); });
return dtsDeclarationMap;
}
/**
* Parse a function/method node (or its implementation), to see if it returns a
* `ModuleWithProviders` object.
* @param name The name of the function.
* @param node the node to check - this could be a function, a method or a variable declaration.
* @param implementation the actual function expression if `node` is a variable declaration.
* @param container the class that contains the function, if it is a method.
* @returns info about the function if it does return a `ModuleWithProviders` object; `null`
* otherwise.
*/
protected parseForModuleWithProviders(
name: string, node: ts.Node|null, implementation: ts.Node|null = node,
container: ts.Declaration|null = null): ModuleWithProvidersFunction|null {
if (implementation === null ||
(!ts.isFunctionDeclaration(implementation) && !ts.isMethodDeclaration(implementation) &&
!ts.isFunctionExpression(implementation))) {
return null;
}
const declaration = implementation;
const body = this.getDefinitionOfFunction(declaration).body;
const lastStatement = body && body[body.length - 1];
const returnExpression =
lastStatement && ts.isReturnStatement(lastStatement) && lastStatement.expression || null;
const ngModuleProperty = returnExpression && ts.isObjectLiteralExpression(returnExpression) &&
returnExpression.properties.find(
prop =>
!!prop.name && ts.isIdentifier(prop.name) && prop.name.text === 'ngModule') ||
null;
if (!ngModuleProperty || !ts.isPropertyAssignment(ngModuleProperty)) {
return null;
}
// The ngModuleValue could be of the form `SomeModule` or `namespace_1.SomeModule`
const ngModuleValue = ngModuleProperty.initializer;
if (!ts.isIdentifier(ngModuleValue) && !ts.isPropertyAccessExpression(ngModuleValue)) {
return null;
}
const ngModuleDeclaration = this.getDeclarationOfExpression(ngModuleValue);
if (!ngModuleDeclaration) {
throw new Error(
`Cannot find a declaration for NgModule ${ngModuleValue.getText()} referenced in "${declaration!.getText()}"`);
}
if (!hasNameIdentifier(ngModuleDeclaration.node)) {
return null;
}
return {
name,
ngModule: ngModuleDeclaration as Declaration<ClassDeclaration>, declaration, container
};
}
protected getDeclarationOfExpression(expression: ts.Expression): Declaration|null {
if (ts.isIdentifier(expression)) {
return this.getDeclarationOfIdentifier(expression);
}
if (!ts.isPropertyAccessExpression(expression) || !ts.isIdentifier(expression.expression)) {
return null;
}
const namespaceDecl = this.getDeclarationOfIdentifier(expression.expression);
if (!namespaceDecl || !ts.isSourceFile(namespaceDecl.node)) {
return null;
}
const namespaceExports = this.getExportsOfModule(namespaceDecl.node);
if (namespaceExports === null) {
return null;
}
if (!namespaceExports.has(expression.name.text)) {
return null;
}
const exportDecl = namespaceExports.get(expression.name.text) !;
return {...exportDecl, viaModule: namespaceDecl.viaModule};
}
}
///////////// Exported Helpers /////////////
export type ParamInfo = {
decorators: Decorator[] | null,
typeExpression: ts.Expression | null
};
/**
* A statement node that represents an assignment.
*/
export type AssignmentStatement =
ts.ExpressionStatement & {expression: {left: ts.Identifier, right: ts.Expression}};
/**
* Test whether a statement node is an assignment statement.
* @param statement the statement to test.
*/
export function isAssignmentStatement(statement: ts.Statement): statement is AssignmentStatement {
return ts.isExpressionStatement(statement) && isAssignment(statement.expression) &&
ts.isIdentifier(statement.expression.left);
}
export function isAssignment(node: ts.Node): node is ts.AssignmentExpression<ts.EqualsToken> {
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken;
}
/**
* The type of a function that can be used to filter out helpers based on their target.
* This is used in `reflectDecoratorsFromHelperCall()`.
*/
export type TargetFilter = (target: ts.Expression) => boolean;
/**
* Creates a function that tests whether the given expression is a class target.
* @param className the name of the class we want to target.
*/
export function makeClassTargetFilter(className: string): TargetFilter {
return (target: ts.Expression): boolean => ts.isIdentifier(target) && target.text === className;
}
/**
* Creates a function that tests whether the given expression is a class member target.
* @param className the name of the class we want to target.
*/
export function makeMemberTargetFilter(className: string): TargetFilter {
return (target: ts.Expression): boolean => ts.isPropertyAccessExpression(target) &&
ts.isIdentifier(target.expression) && target.expression.text === className &&
target.name.text === 'prototype';
}
/**
* Helper method to extract the value of a property given the property's "symbol",
* which is actually the symbol of the identifier of the property.
*/
export function getPropertyValueFromSymbol(propSymbol: ts.Symbol): ts.Expression|undefined {
const propIdentifier = propSymbol.valueDeclaration;
const parent = propIdentifier && propIdentifier.parent;
return parent && ts.isBinaryExpression(parent) ? parent.right : undefined;
}
/**
* A callee could be one of: `__decorate(...)` or `tslib_1.__decorate`.
*/
function getCalleeName(call: ts.CallExpression): string|null {
if (ts.isIdentifier(call.expression)) {
return call.expression.text;
}
if (ts.isPropertyAccessExpression(call.expression)) {
return call.expression.name.text;
}
return null;
}
///////////// Internal Helpers /////////////
/**
* In ES2015, a class may be declared using a variable declaration of the following structure:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* Here, the intermediate `MyClass_1` assignment is optional. In the above example, the
* `class MyClass {}` expression is returned as declaration of `MyClass`. Note that if `node`
* represents a regular class declaration, it will be returned as-is.
*
* @param node the node that represents the class whose declaration we are finding.
* @returns the declaration of the class or `null` if it is not a "class".
*/
function getInnerClassDeclaration(node: ts.Node):
ClassDeclaration<ts.ClassDeclaration|ts.ClassExpression>|null {
// Recognize a variable declaration of the form `var MyClass = class MyClass {}` or
// `var MyClass = MyClass_1 = class MyClass {};`
if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
node = node.initializer;
while (isAssignment(node)) {
node = node.right;
}
}
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) {
return null;
}
return hasNameIdentifier(node) ? node : null;
}
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 isPropertyAccess(node: ts.Node): node is ts.PropertyAccessExpression&
{parent: ts.BinaryExpression} {
return !!node.parent && ts.isBinaryExpression(node.parent) && ts.isPropertyAccessExpression(node);
}
function isThisAssignment(node: ts.Declaration): node is ts.BinaryExpression&
{left: ts.PropertyAccessExpression} {
return ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left) &&
node.left.expression.kind === ts.SyntaxKind.ThisKeyword;
}
function isNamedDeclaration(node: ts.Declaration): node is ts.NamedDeclaration&
{name: ts.Identifier} {
const anyNode: any = node;
return !!anyNode.name && ts.isIdentifier(anyNode.name);
}
function isClassMemberType(node: ts.Declaration): node is ts.ClassElement|
ts.PropertyAccessExpression|ts.BinaryExpression {
return ts.isClassElement(node) || isPropertyAccess(node) || ts.isBinaryExpression(node);
}
/**
* Compute the left most identifier in a property access chain. E.g. the `a` of `a.b.c.d`.
* @param propertyAccess The starting property access expression from which we want to compute
* the left most identifier.
* @returns the left most identifier in the chain or `null` if it is not an identifier.
*/
function getFarLeftIdentifier(propertyAccess: ts.PropertyAccessExpression): ts.Identifier|null {
while (ts.isPropertyAccessExpression(propertyAccess.expression)) {
propertyAccess = propertyAccess.expression;
}
return ts.isIdentifier(propertyAccess.expression) ? propertyAccess.expression : null;
}
/**
* Collect mappings between exported declarations in a source file and its associated
* declaration in the typings program.
*/
function collectExportedDeclarations(
checker: ts.TypeChecker, dtsDeclarationMap: Map<string, ts.Declaration>,
srcFile: ts.SourceFile): void {
const srcModule = srcFile && checker.getSymbolAtLocation(srcFile);
const moduleExports = srcModule && checker.getExportsOfModule(srcModule);
if (moduleExports) {
moduleExports.forEach(exportedSymbol => {
if (exportedSymbol.flags & ts.SymbolFlags.Alias) {
exportedSymbol = checker.getAliasedSymbol(exportedSymbol);
}
const declaration = exportedSymbol.valueDeclaration;
const name = exportedSymbol.name;
if (declaration && !dtsDeclarationMap.has(name)) {
dtsDeclarationMap.set(name, declaration);
}
});
}
}
/**
* Attempt to resolve the variable declaration that the given declaration is assigned to.
* For example, for the following code:
*
* ```
* var MyClass = MyClass_1 = class MyClass {};
* ```
*
* and the provided declaration being `class MyClass {}`, this will return the `var MyClass`
* declaration.
*
* @param declaration The declaration for which any variable declaration should be obtained.
* @returns the outer variable declaration if found, undefined otherwise.
*/
function getVariableDeclarationOfDeclaration(declaration: ts.Declaration): ts.VariableDeclaration|
undefined {
let node = declaration.parent;
// Detect an intermediary variable assignment and skip over it.
if (isAssignment(node) && ts.isIdentifier(node.left)) {
node = node.parent;
}
return ts.isVariableDeclaration(node) ? node : undefined;
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. When a class has a superclass,
* a synthesized constructor must not be considered as a user-defined constructor as that
* prevents a base factory call from being created by ngtsc, resulting in a factory function
* that does not inject the dependencies of the superclass. Hence, we identify a default
* synthesized super call in the constructor body, according to the structure that TypeScript
* emits during JavaScript emit:
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/ts.ts#L1068-L1082
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
function isSynthesizedConstructor(constructor: ts.ConstructorDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement || !ts.isExpressionStatement(firstStatement)) return false;
return isSynthesizedSuperCall(firstStatement.expression);
}
/**
* Tests whether the expression appears to have been synthesized by TypeScript, i.e. whether
* it is of the following form:
*
* ```
* super(...arguments);
* ```
*
* @param expression the expression that is to be tested
* @returns true if the expression appears to be a synthesized super call
*/
function isSynthesizedSuperCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression)) return false;
if (expression.expression.kind !== ts.SyntaxKind.SuperKeyword) return false;
if (expression.arguments.length !== 1) return false;
const argument = expression.arguments[0];
return ts.isSpreadElement(argument) && ts.isIdentifier(argument.expression) &&
argument.expression.text === 'arguments';
}