/** * @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 {Attribute, Component, ContentChild, ContentChildren, Directive, Host, HostBinding, HostListener, Inject, Injectable, Input, NgModule, Optional, Output, Pipe, Self, SkipSelf, ViewChild, ViewChildren, animate, group, keyframes, sequence, state, style, transition, trigger} from '@angular/core'; import {ReflectorReader} from '../private_import_core'; import {StaticSymbol, StaticSymbolCache} from './static_symbol'; import {ResolvedStaticSymbol, StaticSymbolResolver} from './static_symbol_resolver'; const ANGULAR_IMPORT_LOCATIONS = { coreDecorators: '@angular/core/src/metadata', diDecorators: '@angular/core/src/di/metadata', diMetadata: '@angular/core/src/di/metadata', diOpaqueToken: '@angular/core/src/di/opaque_token', animationMetadata: '@angular/core/src/animation/metadata', provider: '@angular/core/src/di/provider' }; const HIDDEN_KEY = /^\$.*\$$/; /** * A static reflector implements enough of the Reflector API that is necessary to compile * templates statically. */ export class StaticReflector implements ReflectorReader { private annotationCache = new Map(); private propertyCache = new Map(); private parameterCache = new Map(); private methodCache = new Map(); private conversionMap = new Map any>(); private opaqueToken: StaticSymbol; constructor( private symbolResolver: StaticSymbolResolver, knownMetadataClasses: {name: string, filePath: string, ctor: any}[] = [], knownMetadataFunctions: {name: string, filePath: string, fn: any}[] = [], private errorRecorder?: (error: any, fileName: string) => void) { this.initializeConversionMap(); knownMetadataClasses.forEach( (kc) => this._registerDecoratorOrConstructor( this.getStaticSymbol(kc.filePath, kc.name), kc.ctor)); knownMetadataFunctions.forEach( (kf) => this._registerFunction(this.getStaticSymbol(kf.filePath, kf.name), kf.fn)); } importUri(typeOrFunc: StaticSymbol): string { const staticSymbol = this.findSymbolDeclaration(typeOrFunc); return staticSymbol ? staticSymbol.filePath : null; } resolveIdentifier(name: string, moduleUrl: string): StaticSymbol { return this.findDeclaration(moduleUrl, name); } findDeclaration(moduleUrl: string, name: string, containingFile?: string): StaticSymbol { return this.findSymbolDeclaration( this.symbolResolver.getSymbolByModule(moduleUrl, name, containingFile)); } findSymbolDeclaration(symbol: StaticSymbol): StaticSymbol { const resolvedSymbol = this.symbolResolver.resolveSymbol(symbol); if (resolvedSymbol && resolvedSymbol.metadata instanceof StaticSymbol) { return this.findSymbolDeclaration(resolvedSymbol.metadata); } else { return symbol; } } resolveEnum(enumIdentifier: any, name: string): any { const staticSymbol: StaticSymbol = enumIdentifier; return this.getStaticSymbol(staticSymbol.filePath, staticSymbol.name, [name]); } public annotations(type: StaticSymbol): any[] { let annotations = this.annotationCache.get(type); if (!annotations) { annotations = []; const classMetadata = this.getTypeMetadata(type); if (classMetadata['extends']) { const parentType = this.simplify(type, classMetadata['extends']); if (parentType instanceof StaticSymbol) { const parentAnnotations = this.annotations(parentType); annotations.push(...parentAnnotations); } } if (classMetadata['decorators']) { const ownAnnotations: any[] = this.simplify(type, classMetadata['decorators']); annotations.push(...ownAnnotations); } this.annotationCache.set(type, annotations.filter(ann => !!ann)); } return annotations; } public propMetadata(type: StaticSymbol): {[key: string]: any[]} { let propMetadata = this.propertyCache.get(type); if (!propMetadata) { const classMetadata = this.getTypeMetadata(type); propMetadata = {}; if (classMetadata['extends']) { const parentType = this.simplify(type, classMetadata['extends']); if (parentType instanceof StaticSymbol) { const parentPropMetadata = this.propMetadata(parentType); Object.keys(parentPropMetadata).forEach((parentProp) => { propMetadata[parentProp] = parentPropMetadata[parentProp]; }); } } const members = classMetadata['members'] || {}; Object.keys(members).forEach((propName) => { const propData = members[propName]; const prop = (propData) .find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method'); const decorators: any[] = []; if (propMetadata[propName]) { decorators.push(...propMetadata[propName]); } propMetadata[propName] = decorators; if (prop && prop['decorators']) { decorators.push(...this.simplify(type, prop['decorators'])); } }); this.propertyCache.set(type, propMetadata); } return propMetadata; } public parameters(type: StaticSymbol): any[] { if (!(type instanceof StaticSymbol)) { this.reportError( new Error(`parameters received ${JSON.stringify(type)} which is not a StaticSymbol`), type); return []; } try { let parameters = this.parameterCache.get(type); if (!parameters) { const classMetadata = this.getTypeMetadata(type); const members = classMetadata ? classMetadata['members'] : null; const ctorData = members ? members['__ctor__'] : null; if (ctorData) { const ctor = (ctorData).find(a => a['__symbolic'] == 'constructor'); const parameterTypes = this.simplify(type, ctor['parameters'] || []); const parameterDecorators = this.simplify(type, ctor['parameterDecorators'] || []); parameters = []; parameterTypes.forEach((paramType, index) => { const nestedResult: any[] = []; if (paramType) { nestedResult.push(paramType); } const decorators = parameterDecorators ? parameterDecorators[index] : null; if (decorators) { nestedResult.push(...decorators); } parameters.push(nestedResult); }); } else if (classMetadata['extends']) { const parentType = this.simplify(type, classMetadata['extends']); if (parentType instanceof StaticSymbol) { parameters = this.parameters(parentType); } } if (!parameters) { parameters = []; } this.parameterCache.set(type, parameters); } return parameters; } catch (e) { console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`); throw e; } } private _methodNames(type: any): {[key: string]: boolean} { let methodNames = this.methodCache.get(type); if (!methodNames) { const classMetadata = this.getTypeMetadata(type); methodNames = {}; if (classMetadata['extends']) { const parentType = this.simplify(type, classMetadata['extends']); if (parentType instanceof StaticSymbol) { const parentMethodNames = this._methodNames(parentType); Object.keys(parentMethodNames).forEach((parentProp) => { methodNames[parentProp] = parentMethodNames[parentProp]; }); } } const members = classMetadata['members'] || {}; Object.keys(members).forEach((propName) => { const propData = members[propName]; const isMethod = (propData).some(a => a['__symbolic'] == 'method'); methodNames[propName] = methodNames[propName] || isMethod; }); this.methodCache.set(type, methodNames); } return methodNames; } hasLifecycleHook(type: any, lcProperty: string): boolean { if (!(type instanceof StaticSymbol)) { this.reportError( new Error( `hasLifecycleHook received ${JSON.stringify(type)} which is not a StaticSymbol`), type); } try { return !!this._methodNames(type)[lcProperty]; } catch (e) { console.error(`Failed on type ${JSON.stringify(type)} with error ${e}`); throw e; } } private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void { this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args)); } private _registerFunction(type: StaticSymbol, fn: any): void { this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => fn.apply(undefined, args)); } private initializeConversionMap(): void { const {coreDecorators, diDecorators, diMetadata, diOpaqueToken, animationMetadata, provider} = ANGULAR_IMPORT_LOCATIONS; this.opaqueToken = this.findDeclaration(diOpaqueToken, 'OpaqueToken'); this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Host'), Host); this._registerDecoratorOrConstructor( this.findDeclaration(diDecorators, 'Injectable'), Injectable); this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Self'), Self); this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'SkipSelf'), SkipSelf); this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Inject'), Inject); this._registerDecoratorOrConstructor(this.findDeclaration(diDecorators, 'Optional'), Optional); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'Attribute'), Attribute); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'ContentChild'), ContentChild); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'ContentChildren'), ContentChildren); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'ViewChild'), ViewChild); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'ViewChildren'), ViewChildren); this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Input'), Input); this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Output'), Output); this._registerDecoratorOrConstructor(this.findDeclaration(coreDecorators, 'Pipe'), Pipe); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'HostBinding'), HostBinding); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'HostListener'), HostListener); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'Directive'), Directive); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'Component'), Component); this._registerDecoratorOrConstructor( this.findDeclaration(coreDecorators, 'NgModule'), NgModule); // Note: Some metadata classes can be used directly with Provider.deps. this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Host'), Host); this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Self'), Self); this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'SkipSelf'), SkipSelf); this._registerDecoratorOrConstructor(this.findDeclaration(diMetadata, 'Optional'), Optional); this._registerFunction(this.findDeclaration(animationMetadata, 'trigger'), trigger); this._registerFunction(this.findDeclaration(animationMetadata, 'state'), state); this._registerFunction(this.findDeclaration(animationMetadata, 'transition'), transition); this._registerFunction(this.findDeclaration(animationMetadata, 'style'), style); this._registerFunction(this.findDeclaration(animationMetadata, 'animate'), animate); this._registerFunction(this.findDeclaration(animationMetadata, 'keyframes'), keyframes); this._registerFunction(this.findDeclaration(animationMetadata, 'sequence'), sequence); this._registerFunction(this.findDeclaration(animationMetadata, 'group'), group); } /** * getStaticSymbol produces a Type whose metadata is known but whose implementation is not loaded. * All types passed to the StaticResolver should be pseudo-types returned by this method. * * @param declarationFile the absolute path of the file where the symbol is declared * @param name the name of the type. */ getStaticSymbol(declarationFile: string, name: string, members?: string[]): StaticSymbol { return this.symbolResolver.getStaticSymbol(declarationFile, name, members); } private reportError(error: Error, context: StaticSymbol, path?: string) { if (this.errorRecorder) { this.errorRecorder(error, (context && context.filePath) || path); } else { throw error; } } /** @internal */ public simplify(context: StaticSymbol, value: any): any { const self = this; let scope = BindingScope.empty; const calling = new Map(); function simplifyInContext(context: StaticSymbol, value: any, depth: number): any { function resolveReferenceValue(staticSymbol: StaticSymbol): any { const resolvedSymbol = self.symbolResolver.resolveSymbol(staticSymbol); return resolvedSymbol ? resolvedSymbol.metadata : null; } function simplifyCall(functionSymbol: StaticSymbol, targetFunction: any, args: any[]) { if (targetFunction && targetFunction['__symbolic'] == 'function') { if (calling.get(functionSymbol)) { throw new Error('Recursion not supported'); } calling.set(functionSymbol, true); try { const value = targetFunction['value']; if (value && (depth != 0 || value.__symbolic != 'error')) { const parameters: string[] = targetFunction['parameters']; const defaults: any[] = targetFunction.defaults; if (defaults && defaults.length > args.length) { args.push(...defaults.slice(args.length).map((value: any) => simplify(value))); } const functionScope = BindingScope.build(); for (let i = 0; i < parameters.length; i++) { functionScope.define(parameters[i], args[i]); } const oldScope = scope; let result: any; try { scope = functionScope.done(); result = simplifyInContext(functionSymbol, value, depth + 1); } finally { scope = oldScope; } return result; } } finally { calling.delete(functionSymbol); } } if (depth === 0) { // If depth is 0 we are evaluating the top level expression that is describing element // decorator. In this case, it is a decorator we don't understand, such as a custom // non-angular decorator, and we should just ignore it. return {__symbolic: 'ignore'}; } return simplify( {__symbolic: 'error', message: 'Function call not supported', context: functionSymbol}); } function simplify(expression: any): any { if (isPrimitive(expression)) { return expression; } if (expression instanceof Array) { const result: any[] = []; for (const item of (expression)) { // Check for a spread expression if (item && item.__symbolic === 'spread') { const spreadArray = simplify(item.expression); if (Array.isArray(spreadArray)) { for (const spreadItem of spreadArray) { result.push(spreadItem); } continue; } } const value = simplify(item); if (shouldIgnore(value)) { continue; } result.push(value); } return result; } if (expression instanceof StaticSymbol) { // Stop simplification at builtin symbols if (expression === self.opaqueToken || self.conversionMap.has(expression)) { return expression; } else { const staticSymbol = expression; const declarationValue = resolveReferenceValue(staticSymbol); if (declarationValue) { return simplifyInContext(staticSymbol, declarationValue, depth + 1); } else { return staticSymbol; } } } if (expression) { if (expression['__symbolic']) { let staticSymbol: StaticSymbol; switch (expression['__symbolic']) { case 'binop': let left = simplify(expression['left']); if (shouldIgnore(left)) return left; let right = simplify(expression['right']); if (shouldIgnore(right)) return right; switch (expression['operator']) { case '&&': return left && right; case '||': return left || right; case '|': return left | right; case '^': return left ^ right; case '&': return left & right; case '==': return left == right; case '!=': return left != right; case '===': return left === right; case '!==': return left !== right; case '<': return left < right; case '>': return left > right; case '<=': return left <= right; case '>=': return left >= right; case '<<': return left << right; case '>>': return left >> right; case '+': return left + right; case '-': return left - right; case '*': return left * right; case '/': return left / right; case '%': return left % right; } return null; case 'if': let condition = simplify(expression['condition']); return condition ? simplify(expression['thenExpression']) : simplify(expression['elseExpression']); case 'pre': let operand = simplify(expression['operand']); if (shouldIgnore(operand)) return operand; switch (expression['operator']) { case '+': return operand; case '-': return -operand; case '!': return !operand; case '~': return ~operand; } return null; case 'index': let indexTarget = simplify(expression['expression']); let index = simplify(expression['index']); if (indexTarget && isPrimitive(index)) return indexTarget[index]; return null; case 'select': const member = expression['member']; let selectContext = context; let selectTarget = simplify(expression['expression']); if (selectTarget instanceof StaticSymbol) { const members = selectTarget.members.concat(member); selectContext = self.getStaticSymbol(selectTarget.filePath, selectTarget.name, members); const declarationValue = resolveReferenceValue(selectContext); if (declarationValue) { return simplifyInContext(selectContext, declarationValue, depth + 1); } else { return selectContext; } } if (selectTarget && isPrimitive(member)) return simplifyInContext(selectContext, selectTarget[member], depth + 1); return null; case 'reference': // Note: This only has to deal with variable references, // as symbol references have been converted into StaticSymbols already // in the StaticSymbolResolver! const name: string = expression['name']; const localValue = scope.resolve(name); if (localValue != BindingScope.missing) { return localValue; } break; case 'class': return context; case 'function': return context; case 'new': case 'call': // Determine if the function is a built-in conversion staticSymbol = simplifyInContext(context, expression['expression'], depth + 1); if (staticSymbol instanceof StaticSymbol) { if (staticSymbol === self.opaqueToken) { // if somebody calls new OpaqueToken, don't create an OpaqueToken, // but rather return the symbol to which the OpaqueToken is assigned to. return context; } const argExpressions: any[] = expression['arguments'] || []; const args = argExpressions.map(arg => simplifyInContext(context, arg, depth + 1)); let converter = self.conversionMap.get(staticSymbol); if (converter) { return converter(context, args); } else { // Determine if the function is one we can simplify. const targetFunction = resolveReferenceValue(staticSymbol); return simplifyCall(staticSymbol, targetFunction, args); } } break; case 'error': let message = produceErrorMessage(expression); if (expression['line']) { message = `${message} (position ${expression['line']+1}:${expression['character']+1} in the original .ts file)`; throw positionalError( message, context.filePath, expression['line'], expression['character']); } throw new Error(message); } return null; } return mapStringMap(expression, (value, name) => simplify(value)); } return null; } try { return simplify(value); } catch (e) { const members = context.members.length ? `.${context.members.join('.')}` : ''; const message = `${e.message}, resolving symbol ${context.name}${members} in ${context.filePath}`; if (e.fileName) { throw positionalError(message, e.fileName, e.line, e.column); } throw new Error(message); } } const recordedSimplifyInContext = (context: StaticSymbol, value: any, depth: number) => { try { return simplifyInContext(context, value, depth); } catch (e) { this.reportError(e, context); } }; const result = this.errorRecorder ? recordedSimplifyInContext(context, value, 0) : simplifyInContext(context, value, 0); if (shouldIgnore(result)) { return undefined; } return result; } private getTypeMetadata(type: StaticSymbol): {[key: string]: any} { const resolvedSymbol = this.symbolResolver.resolveSymbol(type); return resolvedSymbol && resolvedSymbol.metadata ? resolvedSymbol.metadata : {__symbolic: 'class'}; } } function expandedMessage(error: any): string { switch (error.message) { case 'Reference to non-exported class': if (error.context && error.context.className) { return `Reference to a non-exported class ${error.context.className}. Consider exporting the class`; } break; case 'Variable not initialized': return 'Only initialized variables and constants can be referenced because the value of this variable is needed by the template compiler'; case 'Destructuring not supported': return 'Referencing an exported destructured variable or constant is not supported by the template compiler. Consider simplifying this to avoid destructuring'; case 'Could not resolve type': if (error.context && error.context.typeName) { return `Could not resolve type ${error.context.typeName}`; } break; case 'Function call not supported': let prefix = error.context && error.context.name ? `Calling function '${error.context.name}', f` : 'F'; return prefix + 'unction calls are not supported. Consider replacing the function or lambda with a reference to an exported function'; case 'Reference to a local symbol': if (error.context && error.context.name) { return `Reference to a local (non-exported) symbol '${error.context.name}'. Consider exporting the symbol`; } break; } return error.message; } function produceErrorMessage(error: any): string { return `Error encountered resolving symbol values statically. ${expandedMessage(error)}`; } function mapStringMap(input: {[key: string]: any}, transform: (value: any, key: string) => any): {[key: string]: any} { if (!input) return {}; const result: {[key: string]: any} = {}; Object.keys(input).forEach((key) => { const value = transform(input[key], key); if (!shouldIgnore(value)) { if (HIDDEN_KEY.test(key)) { Object.defineProperty(result, key, {enumerable: false, configurable: true, value: value}); } else { result[key] = value; } } }); return result; } function isPrimitive(o: any): boolean { return o === null || (typeof o !== 'function' && typeof o !== 'object'); } interface BindingScopeBuilder { define(name: string, value: any): BindingScopeBuilder; done(): BindingScope; } abstract class BindingScope { abstract resolve(name: string): any; public static missing = {}; public static empty: BindingScope = {resolve: name => BindingScope.missing}; public static build(): BindingScopeBuilder { const current = new Map(); return { define: function(name, value) { current.set(name, value); return this; }, done: function() { return current.size > 0 ? new PopulatedScope(current) : BindingScope.empty; } }; } } class PopulatedScope extends BindingScope { constructor(private bindings: Map) { super(); } resolve(name: string): any { return this.bindings.has(name) ? this.bindings.get(name) : BindingScope.missing; } } function sameSymbol(a: StaticSymbol, b: StaticSymbol): boolean { return a === b; } function shouldIgnore(value: any): boolean { return value && value.__symbolic == 'ignore'; } function positionalError(message: string, fileName: string, line: number, column: number): Error { const result = new Error(message); (result as any).fileName = fileName; (result as any).line = line; (result as any).column = column; return result; }