/** * @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, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection'; import {isWithinPackage} from '../analysis/util'; import {Logger} from '../logging/logger'; import {BundleProgram} from '../packages/bundle_program'; import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils'; import {ClassSymbol, isSwitchableVariableDeclaration, NgccClassSymbol, NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration} from './ngcc_host'; import {stripParentheses} from './utils'; 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 { /** * A mapping from source declarations to typings declarations, which are both publicly exported. * * There should be one entry for every public export visible from the root file of the source * tree. Note that by definition the key and value declarations will not be in the same TS * program. */ protected publicDtsDeclarationMap: Map|null = null; /** * A mapping from source declarations to typings declarations, which are not publicly exported. * * This mapping is a best guess between declarations that happen to be exported from their file by * the same name in both the source and the dts file. Note that by definition the key and value * declarations will not be in the same TS program. */ protected privateDtsDeclarationMap: Map|null = null; /** * The set of source files that have already been preprocessed. */ protected preprocessedSourceFiles = new Set(); /** * 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(); /** * 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(); constructor( protected logger: Logger, protected isCore: boolean, protected src: BundleProgram, protected dts: BundleProgram|null = null) { super(src.program.getTypeChecker()); } /** * Find a symbol for 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 structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * * or * * ``` * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() * ``` * * Here, the intermediate `MyClass_1` assignment is optional. In the above example, the * `class MyClass {}` node is returned as declaration of `MyClass`. * * @param declaration the declaration 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): NgccClassSymbol|undefined { const symbol = this.getClassSymbolFromOuterDeclaration(declaration); if (symbol !== undefined) { return symbol; } return this.getClassSymbolFromInnerDeclaration(declaration); } /** * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * * or * * ``` * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() * ``` * * This method extracts the `NgccClassSymbol` for `MyClass` when provided with the `var MyClass` * declaration node. When the `class MyClass {}` node or any other node is given, this method will * return undefined instead. * * @param declaration the declaration whose symbol we are finding. * @returns the symbol for the node or `undefined` if it does not represent an outer declaration * of a class. */ protected getClassSymbolFromOuterDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { // Create a symbol without inner declaration if it is a regular "top level" class declaration. if (isNamedClassDeclaration(declaration) && isTopLevel(declaration)) { return this.createClassSymbol(declaration, null); } // Otherwise, the declaration may be a variable declaration, in which case it must be // initialized using a class expression as inner declaration. if (ts.isVariableDeclaration(declaration) && hasNameIdentifier(declaration)) { const innerDeclaration = getInnerClassDeclaration(declaration); if (innerDeclaration !== null) { return this.createClassSymbol(declaration, innerDeclaration); } } return undefined; } /** * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * * or * * ``` * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() * ``` * * This method extracts the `NgccClassSymbol` for `MyClass` when provided with the * `class MyClass {}` declaration node. When the `var MyClass` node or any other node is given, * this method will return undefined instead. * * @param declaration the declaration whose symbol we are finding. * @returns the symbol for the node or `undefined` if it does not represent an inner declaration * of a class. */ protected getClassSymbolFromInnerDeclaration(declaration: ts.Node): NgccClassSymbol|undefined { let outerDeclaration: ts.VariableDeclaration|undefined = undefined; if (isNamedClassDeclaration(declaration) && !isTopLevel(declaration)) { let node = declaration.parent; while (node !== undefined && !ts.isVariableDeclaration(node)) { node = node.parent; } outerDeclaration = node; } else if (ts.isClassExpression(declaration) && hasNameIdentifier(declaration)) { outerDeclaration = getVariableDeclarationOfDeclaration(declaration); } else { return undefined; } if (outerDeclaration === undefined || !hasNameIdentifier(outerDeclaration)) { return undefined; } return this.createClassSymbol(outerDeclaration, declaration); } /** * Creates an `NgccClassSymbol` from an outer and inner declaration. If a class only has an outer * declaration, the "implementation" symbol of the created `NgccClassSymbol` will be set equal to * the "declaration" symbol. * * @param outerDeclaration The outer declaration node of the class. * @param innerDeclaration The inner declaration node of the class, or undefined if no inner * declaration is present. * @returns the `NgccClassSymbol` representing the class, or undefined if a `ts.Symbol` for any of * the declarations could not be resolved. */ protected createClassSymbol( outerDeclaration: ClassDeclaration, innerDeclaration: ClassDeclaration|null): NgccClassSymbol |undefined { const declarationSymbol = this.checker.getSymbolAtLocation(outerDeclaration.name) as ClassSymbol | undefined; if (declarationSymbol === undefined) { return undefined; } const implementationSymbol = innerDeclaration !== null ? this.checker.getSymbolAtLocation(innerDeclaration.name) : declarationSymbol; if (implementationSymbol === undefined) { return undefined; } return { name: declarationSymbol.name, declaration: declarationSymbol, implementation: implementationSymbol, }; } /** * 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 super.hasBaseClass(innerClassDeclaration); } getBaseClassExpression(clazz: ClassDeclaration): ts.Expression|null { // First try getting the base class from the "outer" declaration const superBaseClassIdentifier = super.getBaseClassExpression(clazz); if (superBaseClassIdentifier) { return superBaseClassIdentifier; } // That didn't work so now try getting it from the "inner" declaration. const innerClassDeclaration = getInnerClassDeclaration(clazz); if (innerClassDeclaration === null) { return null; } return super.getBaseClassExpression(innerClassDeclaration); } /** * Check whether the given node actually represents a class. */ isClass(node: ts.Node): node is ClassDeclaration { return super.isClass(node) || this.getClassSymbol(node) !== undefined; } /** * 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); // If no declaration was found or it's an inline declaration, return as is. if (superDeclaration === null || superDeclaration.node === null) { return superDeclaration; } // If the declaration already has traits assigned to it, return as is. if (superDeclaration.known !== null || superDeclaration.identity !== null) { return superDeclaration; } // 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. const aliasedIdentifier = this.resolveAliasedClassIdentifier(superDeclaration.node); if (aliasedIdentifier !== null) { return this.getDeclarationOfIdentifier(aliasedIdentifier); } // Variable declarations may represent an enum declaration, so attempt to resolve its members. if (ts.isVariableDeclaration(superDeclaration.node)) { const enumMembers = this.resolveEnumMembers(superDeclaration.node); if (enumMembers !== null) { superDeclaration.identity = {kind: SpecialDeclarationKind.DownleveledEnum, enumMembers}; } } return superDeclaration; } /** * Gets all decorators of the given class symbol. Any decorator that have been synthetically * injected by a migration will not be present in the returned collection. */ getDecoratorsOfSymbol(symbol: NgccClassSymbol): Decorator[]|null { const {classDecorators} = this.acquireDecoratorInfo(symbol); if (classDecorators === null) { return null; } // Return a clone of the array to prevent consumers from mutating the cache. return Array.from(classDecorators); } /** * 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 top-level class symbols in the given file. * @param sourceFile The source file to search for classes. * @returns An array of class symbols. */ findClassSymbols(sourceFile: ts.SourceFile): NgccClassSymbol[] { const classes: NgccClassSymbol[] = []; this.getModuleStatements(sourceFile).forEach(statement => { if (ts.isVariableStatement(statement)) { statement.declarationList.declarations.forEach(declaration => { const classSymbol = this.getClassSymbol(declaration); if (classSymbol) { classes.push(classSymbol); } }); } else if (ts.isClassDeclaration(statement)) { const classSymbol = this.getClassSymbol(statement); if (classSymbol) { classes.push(classSymbol); } } }); 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.dts === null) { 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}`); } // Try to retrieve the dts declaration from the public map if (this.publicDtsDeclarationMap === null) { this.publicDtsDeclarationMap = this.computePublicDtsDeclarationMap(this.src, this.dts); } if (this.publicDtsDeclarationMap.has(declaration)) { return this.publicDtsDeclarationMap.get(declaration)!; } // No public export, try the private map if (this.privateDtsDeclarationMap === null) { this.privateDtsDeclarationMap = this.computePrivateDtsDeclarationMap(this.src, this.dts); } if (this.privateDtsDeclarationMap.has(declaration)) { return this.privateDtsDeclarationMap.get(declaration)!; } // No declaration found at all return null; } getEndOfClass(classSymbol: NgccClassSymbol): ts.Node { let last: ts.Node = classSymbol.declaration.valueDeclaration; // If there are static members on this class then find the last one if (classSymbol.declaration.exports !== undefined) { classSymbol.declaration.exports.forEach(exportSymbol => { if (exportSymbol.valueDeclaration === undefined) { return; } const exportStatement = getContainingStatement(exportSymbol.valueDeclaration); if (exportStatement !== null && last.getEnd() < exportStatement.getEnd()) { last = exportStatement; } }); } // If there are helper calls for this class then find the last one const helpers = this.getHelperCallsForClass( classSymbol, ['__decorate', '__extends', '__param', '__metadata']); helpers.forEach(helper => { const helperStatement = getContainingStatement(helper); if (helperStatement !== null && last.getEnd() < helperStatement.getEnd()) { last = helperStatement; } }); return last; } /** * Check whether a `Declaration` corresponds with a known declaration, such as `Object`, and set * its `known` property to the appropriate `KnownDeclaration`. * * @param decl The `Declaration` to check. * @return The passed in `Declaration` (potentially enhanced with a `KnownDeclaration`). */ detectKnownDeclaration(decl: null): null; detectKnownDeclaration(decl: T): T; detectKnownDeclaration(decl: T|null): T|null; detectKnownDeclaration(decl: T|null): T|null { if (decl !== null && decl.known === null && this.isJavaScriptObjectDeclaration(decl)) { // If the identifier resolves to the global JavaScript `Object`, update the declaration to // denote it as the known `JsGlobalObject` declaration. decl.known = KnownDeclaration.JsGlobalObject; } return decl; } ///////////// Protected Helpers ///////////// /** * Resolve a `ts.Symbol` to its declaration and detect whether it corresponds with a known * declaration. */ protected getDeclarationOfSymbol(symbol: ts.Symbol, originalId: ts.Identifier|null): Declaration |null { return this.detectKnownDeclaration(super.getDeclarationOfSymbol(symbol, originalId)); } /** * 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.has(declaration) ? 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 this.getModuleStatements(sourceFile)) { 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) || !this.isClass(declaration)) { return; } const aliasedIdentifier = initializer.left; const aliasedDeclaration = this.getDeclarationOfIdentifier(aliasedIdentifier); if (aliasedDeclaration === null || aliasedDeclaration.node === 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); } /** * 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. * * In some cases, a static property can either be set on the inner declaration inside the class' * IIFE, or it can be set on the outer variable declaration. Therefore, the host checks both * places, first looking up the property on the inner symbol, and if the property is not found it * will fall back to looking up the property on the outer symbol. * * @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: NgccClassSymbol, propertyName: ts.__String): ts.Symbol |undefined { return symbol.implementation.exports && symbol.implementation.exports.get(propertyName) || symbol.declaration.exports && symbol.declaration.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: NgccClassSymbol): DecoratorInfo { const decl = classSymbol.declaration.valueDeclaration; if (this.decoratorCache.has(decl)) { return this.decoratorCache.get(decl)!; } // Extract decorators from static properties and `__decorate` helper calls, then merge them // together where the information from the static properties is preferred. const staticProps = this.computeDecoratorInfoFromStaticProperties(classSymbol); const helperCalls = this.computeDecoratorInfoFromHelperCalls(classSymbol); const decoratorInfo: DecoratorInfo = { classDecorators: staticProps.classDecorators || helperCalls.classDecorators, memberDecorators: staticProps.memberDecorators || helperCalls.memberDecorators, constructorParamInfo: staticProps.constructorParamInfo || helperCalls.constructorParamInfo, }; this.decoratorCache.set(decl, 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: NgccClassSymbol): { classDecorators: Decorator[]|null; memberDecorators: Map| null; constructorParamInfo: ParamInfo[] | null; } { let classDecorators: Decorator[]|null = null; let memberDecorators: Map|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); } return {classDecorators, memberDecorators, constructorParamInfo}; } /** * 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; } /** * 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: NgccClassSymbol): ClassMember[] { const members: ClassMember[] = []; // The decorators map contains all the properties that are decorated 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 // that are initialized in the class. if (symbol.implementation.members) { symbol.implementation.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.implementation.exports) { symbol.implementation.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 = ...; // ``` if (ts.isVariableDeclaration(symbol.declaration.valueDeclaration)) { if (symbol.declaration.exports) { symbol.declaration.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; } /** * 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 { const memberDecorators = new Map(); // 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; } /** * 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([ * Input(), * __metadata("design:type", String) * ], SomeDirective.prototype, "input1", void 0); * ``` * * @param classSymbol The class symbol for which decorators should be extracted. * @returns All information on the decorators of the class. */ protected computeDecoratorInfoFromHelperCalls(classSymbol: NgccClassSymbol): DecoratorInfo { let classDecorators: Decorator[]|null = null; const memberDecorators = new Map(); const constructorParamInfo: ParamInfo[] = []; const getConstructorParamInfo = (index: number) => { let param = constructorParamInfo[index]; if (param === undefined) { param = constructorParamInfo[index] = {decorators: null, typeExpression: null}; } return param; }; // All relevant information can be extracted from calls to `__decorate`, obtain these first. // Note that although the helper calls are retrieved using the class symbol, the result may // contain helper calls corresponding with unrelated classes. Therefore, each helper call still // has to be checked to actually correspond with the class symbol. const helperCalls = this.getHelperCallsForClass(classSymbol, ['__decorate']); const outerDeclaration = classSymbol.declaration.valueDeclaration; const innerDeclaration = classSymbol.implementation.valueDeclaration; const matchesClass = (identifier: ts.Identifier) => { const decl = this.getDeclarationOfIdentifier(identifier); if (decl === null) { return false; } // The identifier corresponds with the class if its declaration is either the outer or inner // declaration. return decl.node === outerDeclaration || decl.node === innerDeclaration; }; for (const helperCall of helperCalls) { if (isClassDecorateCall(helperCall, matchesClass)) { // 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, matchesClass)) { // 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 = memberDecorators.has(memberName) ? memberDecorators.get(memberName)! : []; decorators.push(entry.decorator); memberDecorators.set(memberName, decorators); } } else { // Information on decorated parameters is not interesting for ngcc, so it's ignored. } } } } return {classDecorators, memberDecorators, constructorParamInfo}; } /** * Extract the details of an entry within a `__decorate` helper call. For example, given the * following code: * * ``` * __decorate([ * Directive({ selector: '[someDirective]' }), * tslib_1.__param(2, Inject(INJECTED_TOKEN)), * tslib_1.__metadata("design:paramtypes", [ViewContainerRef, TemplateRef, String]) * ], SomeDirective); * ``` * * it can be seen that there are calls to regular decorators (the `Directive`) and calls into * `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 expression the expression that needs to be reflected into a `DecorateHelperEntry` * @returns an object that indicates which of the three categories the call represents, together * 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 helperName = getCalleeName(call); if (helperName === '__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 (helperName === '__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 { const decoratorExpression = call.expression; if (!isDecoratorIdentifier(decoratorExpression)) { return null; } // We found a decorator! const decoratorIdentifier = ts.isIdentifier(decoratorExpression) ? decoratorExpression : decoratorExpression.name; return { name: decoratorIdentifier.text, identifier: decoratorExpression, import: this.getImportOfIdentifier(decoratorIdentifier), node: call, args: Array.from(call.arguments), }; } /** * Check the given statement to see if it is a call to any of the specified helper functions or * null if not found. * * Matching statements will look like: `tslib_1.__decorate(...);`. * @param statement the statement that may contain the call. * @param helperNames the names 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, helperNames: string[]): ts.CallExpression|null { if ((ts.isExpressionStatement(statement) || ts.isReturnStatement(statement)) && statement.expression) { let expression = statement.expression; while (isAssignment(expression)) { expression = expression.right; } if (ts.isCallExpression(expression)) { const calleeName = getCalleeName(expression); if (calleeName !== null && helperNames.includes(calleeName)) { 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? if (decorator.has('type')) { let decoratorType = decorator.get('type')!; if (isDecoratorIdentifier(decoratorType)) { const decoratorIdentifier = ts.isIdentifier(decoratorType) ? decoratorType : decoratorType.name; decorators.push({ name: decoratorIdentifier.text, identifier: decoratorType, import: this.getImportOfIdentifier(decoratorIdentifier), 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: NgccClassSymbol): ts.ParameterDeclaration[]|null { const members = classSymbol.implementation.members; if (members && members.has(CONSTRUCTOR)) { const constructorSymbol = members.get(CONSTRUCTOR)!; // 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: NgccClassSymbol, parameterNodes: ts.ParameterDeclaration[]): CtorParameter[] { const {constructorParamInfo} = this.acquireDecoratorInfo(classSymbol); return parameterNodes.map((node, index) => { const {decorators, typeExpression} = constructorParamInfo[index] ? constructorParamInfo[index] : {decorators: null, typeExpression: null}; const nameNode = node.name; let typeValueReference: TypeValueReference|null = null; if (typeExpression !== null) { // `typeExpression` is an expression in a "type" context. Resolve it to a declared value. // Either it's a reference to an imported type, or a type declared locally. Distinguish the // two cases with `getDeclarationOfExpression`. const decl = this.getDeclarationOfExpression(typeExpression); if (decl !== null && decl.node !== null && decl.viaModule !== null && isNamedDeclaration(decl.node)) { typeValueReference = { local: false, valueDeclaration: decl.node, moduleName: decl.viaModule, importedName: decl.node.name.text, nestedPath: null, }; } else { typeValueReference = { local: true, expression: typeExpression, defaultImportStatement: null, }; } } return { name: getNameText(nameNode), nameNode, typeValueReference, typeNode: null, decorators }; }); } /** * Get the parameter type and decorators for the constructor of a class, * where the information is stored on a static property of the class. * * Note that in ESM2015, the property is defined an array, or by an arrow function that returns * an array, of decorator and type information. * * For example, * * ``` * SomeDirective.ctorParameters = () => [ * {type: ViewContainerRef}, * {type: TemplateRef}, * {type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN]}]}, * ]; * ``` * * or * * ``` * 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) { // The decorators array may be wrapped in an arrow function. If so unwrap it. const container = ts.isArrowFunction(paramDecorators) ? paramDecorators.body : paramDecorators; if (ts.isArrayLiteralExpression(container)) { const elements = container.elements; return elements .map( element => ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null) .map(paramInfo => { const typeExpression = paramInfo && paramInfo.has('type') ? paramInfo.get('type')! : null; const decoratorInfo = paramInfo && paramInfo.has('decorators') ? paramInfo.get('decorators')! : null; const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo) .filter(decorator => this.isFromCore(decorator)); return {typeExpression, decorators}; }); } else if (paramDecorators !== undefined) { this.logger.warn( 'Invalid constructor parameter decorator in ' + paramDecorators.getSourceFile().fileName + ':\n', paramDecorators.getText()); } } return null; } /** * 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 helperNames the names of the helpers (e.g. `__decorate`) whose calls we are interested * in. * @returns an array of CallExpression nodes for each matching helper call. */ protected getHelperCallsForClass(classSymbol: NgccClassSymbol, helperNames: string[]): ts.CallExpression[] { return this.getStatementsForClass(classSymbol) .map(statement => this.getHelperCall(statement, helperNames)) .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: NgccClassSymbol): ts.Statement[] { const classNode = classSymbol.implementation.valueDeclaration; if (isTopLevel(classNode)) { return this.getModuleStatements(classNode.getSourceFile()); } else if (ts.isBlock(classNode.parent)) { return Array.from(classNode.parent.statements); } // We should never arrive here throw new Error(`Unable to find adjacent statements for ${classSymbol.name}`); } /** * 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'; } } /** * Create a mapping between the public exports in a src program and the public exports of a dts * program. * * @param src the program bundle containing the source files. * @param dts the program bundle containing the typings files. * @returns a map of source declarations to typings declarations. */ protected computePublicDtsDeclarationMap(src: BundleProgram, dts: BundleProgram): Map { const declarationMap = new Map(); const dtsDeclarationMap = new Map(); const rootDts = getRootFileOrFail(dts); this.collectDtsExportedDeclarations(dtsDeclarationMap, rootDts, dts.program.getTypeChecker()); const rootSrc = getRootFileOrFail(src); this.collectSrcExportedDeclarations(declarationMap, dtsDeclarationMap, rootSrc); return declarationMap; } /** * Create a mapping between the "private" exports in a src program and the "private" exports of a * dts program. These exports may be exported from individual files in the src or dts programs, * but not exported from the root file (i.e publicly from the entry-point). * * This mapping is a "best guess" since we cannot guarantee that two declarations that happen to * be exported from a file with the same name are actually equivalent. But this is a reasonable * estimate for the purposes of ngcc. * * @param src the program bundle containing the source files. * @param dts the program bundle containing the typings files. * @returns a map of source declarations to typings declarations. */ protected computePrivateDtsDeclarationMap(src: BundleProgram, dts: BundleProgram): Map { const declarationMap = new Map(); const dtsDeclarationMap = new Map(); const typeChecker = dts.program.getTypeChecker(); const dtsFiles = getNonRootPackageFiles(dts); for (const dtsFile of dtsFiles) { this.collectDtsExportedDeclarations(dtsDeclarationMap, dtsFile, typeChecker); } const srcFiles = getNonRootPackageFiles(src); for (const srcFile of srcFiles) { this.collectSrcExportedDeclarations(declarationMap, dtsDeclarationMap, srcFile); } return declarationMap; } /** * Collect mappings between names of exported declarations in a file and its actual declaration. * * Any new mappings are added to the `dtsDeclarationMap`. */ protected collectDtsExportedDeclarations( dtsDeclarationMap: Map, srcFile: ts.SourceFile, checker: ts.TypeChecker): void { const srcModule = srcFile && checker.getSymbolAtLocation(srcFile); const moduleExports = srcModule && checker.getExportsOfModule(srcModule); if (moduleExports) { moduleExports.forEach(exportedSymbol => { const name = exportedSymbol.name; if (exportedSymbol.flags & ts.SymbolFlags.Alias) { exportedSymbol = checker.getAliasedSymbol(exportedSymbol); } const declaration = exportedSymbol.valueDeclaration; if (declaration && !dtsDeclarationMap.has(name)) { dtsDeclarationMap.set(name, declaration); } }); } } protected collectSrcExportedDeclarations( declarationMap: Map, dtsDeclarationMap: Map, srcFile: ts.SourceFile): void { const fileExports = this.getExportsOfModule(srcFile); if (fileExports !== null) { for (const [exportName, {node: declaration}] of fileExports) { if (declaration !== null && dtsDeclarationMap.has(exportName)) { declarationMap.set(declaration, dtsDeclarationMap.get(exportName)!); } } } } 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 || namespaceDecl.node === null || !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}; } /** Checks if the specified declaration resolves to the known JavaScript global `Object`. */ protected isJavaScriptObjectDeclaration(decl: Declaration): boolean { if (decl.node === null) { return false; } const node = decl.node; // The default TypeScript library types the global `Object` variable through // a variable declaration with a type reference resolving to `ObjectConstructor`. if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || node.name.text !== 'Object' || node.type === undefined) { return false; } const typeNode = node.type; // If the variable declaration does not have a type resolving to `ObjectConstructor`, // we cannot guarantee that the declaration resolves to the global `Object` variable. if (!ts.isTypeReferenceNode(typeNode) || !ts.isIdentifier(typeNode.typeName) || typeNode.typeName.text !== 'ObjectConstructor') { return false; } // Finally, check if the type definition for `Object` originates from a default library // definition file. This requires default types to be enabled for the host program. return this.src.program.isSourceFileDefaultLibrary(node.getSourceFile()); } /** * In JavaScript, enum declarations are emitted as a regular variable declaration followed by an * IIFE in which the enum members are assigned. * * export var Enum; * (function (Enum) { * Enum["a"] = "A"; * Enum["b"] = "B"; * })(Enum || (Enum = {})); * * @param declaration A variable declaration that may represent an enum * @returns An array of enum members if the variable declaration is followed by an IIFE that * declares the enum members, or null otherwise. */ protected resolveEnumMembers(declaration: ts.VariableDeclaration): EnumMember[]|null { // Initialized variables don't represent enum declarations. if (declaration.initializer !== undefined) return null; const variableStmt = declaration.parent.parent; if (!ts.isVariableStatement(variableStmt)) return null; const block = variableStmt.parent; if (!ts.isBlock(block) && !ts.isSourceFile(block)) return null; const declarationIndex = block.statements.findIndex(statement => statement === variableStmt); if (declarationIndex === -1 || declarationIndex === block.statements.length - 1) return null; const subsequentStmt = block.statements[declarationIndex + 1]; if (!ts.isExpressionStatement(subsequentStmt)) return null; const iife = stripParentheses(subsequentStmt.expression); if (!ts.isCallExpression(iife) || !isEnumDeclarationIife(iife)) return null; const fn = stripParentheses(iife.expression); if (!ts.isFunctionExpression(fn)) return null; return this.reflectEnumMembers(fn); } /** * Attempts to extract all `EnumMember`s from a function that is according to the JavaScript emit * format for enums: * * function (Enum) { * Enum["MemberA"] = "a"; * Enum["MemberB"] = "b"; * } * * @param fn The function expression that is assumed to contain enum members. * @returns All enum members if the function is according to the correct syntax, null otherwise. */ private reflectEnumMembers(fn: ts.FunctionExpression): EnumMember[]|null { if (fn.parameters.length !== 1) return null; const enumName = fn.parameters[0].name; if (!ts.isIdentifier(enumName)) return null; const enumMembers: EnumMember[] = []; for (const statement of fn.body.statements) { const enumMember = this.reflectEnumMember(enumName, statement); if (enumMember === null) { return null; } enumMembers.push(enumMember); } return enumMembers; } /** * Attempts to extract a single `EnumMember` from a statement in the following syntax: * * Enum["MemberA"] = "a"; * * or, for enum member with numeric values: * * Enum[Enum["MemberA"] = 0] = "MemberA"; * * @param enumName The identifier of the enum that the members should be set on. * @param statement The statement to inspect. * @returns An `EnumMember` if the statement is according to the expected syntax, null otherwise. */ protected reflectEnumMember(enumName: ts.Identifier, statement: ts.Statement): EnumMember|null { if (!ts.isExpressionStatement(statement)) return null; const expression = statement.expression; // Check for the `Enum[X] = Y;` case. if (!isEnumAssignment(enumName, expression)) { return null; } const assignment = reflectEnumAssignment(expression); if (assignment != null) { return assignment; } // Check for the `Enum[Enum[X] = Y] = ...;` case. const innerExpression = expression.left.argumentExpression; if (!isEnumAssignment(enumName, innerExpression)) { return null; } return reflectEnumAssignment(innerExpression); } } ///////////// Exported Helpers ///////////// /** * Checks whether the iife has the following call signature: * * (Enum || (Enum = {}) * * Note that the `Enum` identifier is not checked, as it could also be something * like `exports.Enum`. Instead, only the structure of binary operators is checked. * * @param iife The call expression to check. * @returns true if the iife has a call signature that corresponds with a potential * enum declaration. */ function isEnumDeclarationIife(iife: ts.CallExpression): boolean { if (iife.arguments.length !== 1) return false; const arg = iife.arguments[0]; if (!ts.isBinaryExpression(arg) || arg.operatorToken.kind !== ts.SyntaxKind.BarBarToken || !ts.isParenthesizedExpression(arg.right)) { return false; } const right = arg.right.expression; if (!ts.isBinaryExpression(right) || right.operatorToken.kind !== ts.SyntaxKind.EqualsToken) { return false; } if (!ts.isObjectLiteralExpression(right.right) || right.right.properties.length !== 0) { return false; } return true; } /** * An enum member assignment that looks like `Enum[X] = Y;`. */ export type EnumMemberAssignment = ts.BinaryExpression&{left: ts.ElementAccessExpression}; /** * Checks whether the expression looks like an enum member assignment targeting `Enum`: * * Enum[X] = Y; * * Here, X and Y can be any expression. * * @param enumName The identifier of the enum that the members should be set on. * @param expression The expression that should be checked to conform to the above form. * @returns true if the expression is of the correct form, false otherwise. */ function isEnumAssignment( enumName: ts.Identifier, expression: ts.Expression): expression is EnumMemberAssignment { if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isElementAccessExpression(expression.left)) { return false; } // Verify that the outer assignment corresponds with the enum declaration. const enumIdentifier = expression.left.expression; return ts.isIdentifier(enumIdentifier) && enumIdentifier.text === enumName.text; } /** * Attempts to create an `EnumMember` from an expression that is believed to represent an enum * assignment. * * @param expression The expression that is believed to be an enum assignment. * @returns An `EnumMember` or null if the expression did not represent an enum member after all. */ function reflectEnumAssignment(expression: EnumMemberAssignment): EnumMember|null { const memberName = expression.left.argumentExpression; if (!ts.isPropertyName(memberName)) return null; return {name: memberName, initializer: expression.right}; } export type ParamInfo = { decorators: Decorator[]|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; /** * 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. */ 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); } /** * Parse the `expression` that is believed to be an IIFE and return the AST node that corresponds to * the body of the IIFE. * * The expression may be wrapped in parentheses, which are stripped off. * * If the IIFE is an arrow function then its body could be a `ts.Expression` rather than a * `ts.FunctionBody`. * * @param expression the expression to parse. * @returns the `ts.Expression` or `ts.FunctionBody` that holds the body of the IIFE or `undefined` * if the `expression` did not have the correct shape. */ export function getIifeConciseBody(expression: ts.Expression): ts.ConciseBody|undefined { const call = stripParentheses(expression); if (!ts.isCallExpression(call)) { return undefined; } const fn = stripParentheses(call.expression); if (!ts.isFunctionExpression(fn) && !ts.isArrowFunction(fn)) { return undefined; } return fn.body; } /** * Returns true if the `node` is an assignment of the form `a = b`. * * @param node The AST node to check. */ export function isAssignment(node: ts.Node): node is ts.AssignmentExpression { return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken; } /** * Tests whether the provided call expression targets a class, by verifying its arguments are * according to the following form: * * ``` * __decorate([], SomeDirective); * ``` * * @param call the call expression that is tested to represent a class decorator call. * @param matches predicate function to test whether the call is associated with the desired class. */ export function isClassDecorateCall( call: ts.CallExpression, matches: (identifier: ts.Identifier) => boolean): 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]; return target !== undefined && ts.isIdentifier(target) && matches(target); } /** * Tests whether the provided call expression targets a member of the class, by verifying its * 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 matches predicate function to test whether the call is associated with the desired class. */ export function isMemberDecorateCall( call: ts.CallExpression, matches: (identifier: ts.Identifier) => boolean): call is ts.CallExpression& {arguments: [ts.ArrayLiteralExpression, ts.StringLiteral, ts.StringLiteral]} { 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) || !matches(target.expression) || target.name.text !== 'prototype') { return false; } const memberName = call.arguments[2]; return memberName !== undefined && ts.isStringLiteral(memberName); } /** * 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 stripDollarSuffix(call.expression.text); } if (ts.isPropertyAccessExpression(call.expression)) { return stripDollarSuffix(call.expression.name.text); } return null; } ///////////// Internal Helpers ///////////// /** * In ES2015, a class may be declared using a variable declaration of the following structures: * * ``` * var MyClass = MyClass_1 = class MyClass {}; * ``` * * or * * ``` * var MyClass = MyClass_1 = (() => { class MyClass {} ... return MyClass; })() * ``` * * Here, the intermediate `MyClass_1` assignment is optional. In the above example, the * `class MyClass {}` expression is returned as declaration of `var MyClass`. If the variable * is not initialized using a class expression, null is returned. * * @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|null { if (!ts.isVariableDeclaration(node) || node.initializer === undefined) { return null; } // Recognize a variable declaration of the form `var MyClass = class MyClass {}` or // `var MyClass = MyClass_1 = class MyClass {};` let expression = node.initializer; while (isAssignment(expression)) { expression = expression.right; } if (ts.isClassExpression(expression) && hasNameIdentifier(expression)) { return expression; } // Try to parse out a class declaration wrapped in an IIFE (as generated by TS 3.9) // e.g. // /* @class */ = (() => { // class MyClass {} // ... // return MyClass; // })(); const iifeBody = getIifeConciseBody(expression); if (iifeBody === undefined) { return null; } // Extract the class declaration from inside the IIFE. const innerDeclaration = ts.isBlock(iifeBody) ? iifeBody.statements.find(ts.isClassDeclaration) : ts.isClassExpression(iifeBody) ? iifeBody : undefined; if (innerDeclaration === undefined || !hasNameIdentifier(innerDeclaration)) { return null; } return innerDeclaration; } 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)) && // Additionally, ensure `node` is not an index signature, for example on an abstract class: // `abstract class Foo { [key: string]: any; }` !ts.isIndexSignatureDeclaration(node); } /** * 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 {}; * ``` * * or * * ``` * var MyClass = MyClass_1 = (() => { * class MyClass {} * ... * return 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'; } /** * Find the statement that contains the given node * @param node a node whose containing statement we wish to find */ function getContainingStatement(node: ts.Node): ts.ExpressionStatement|null { while (node) { if (ts.isExpressionStatement(node)) { break; } node = node.parent; } return node || null; } function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile { const rootFile = bundle.program.getSourceFile(bundle.path); if (rootFile === undefined) { throw new Error(`The given rootPath ${rootFile} is not a file of the program.`); } return rootFile; } function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] { const rootFile = bundle.program.getSourceFile(bundle.path); return bundle.program.getSourceFiles().filter( f => (f !== rootFile) && isWithinPackage(bundle.package, f)); } function isTopLevel(node: ts.Node): boolean { while (node = node.parent) { if (ts.isBlock(node)) { return false; } } return true; }