/** * @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 {Evaluator, errorSymbol, isPrimitive} from './evaluator'; import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataEntry, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicBinaryExpression, MetadataSymbolicCallExpression, MetadataSymbolicExpression, MetadataSymbolicIfExpression, MetadataSymbolicIndexExpression, MetadataSymbolicPrefixExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataSymbolicSpreadExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isClassMetadata, isConstructorMetadata, isFunctionMetadata, isMetadataError, isMetadataGlobalReferenceExpression, isMetadataSymbolicExpression, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression, isMethodMetadata} from './schema'; import {Symbols} from './symbols'; /** * Collect decorator metadata from a TypeScript module. */ export class MetadataCollector { constructor() {} /** * Returns a JSON.stringify friendly form describing the decorators of the exported classes from * the source file that is expected to correspond to a module. */ public getMetadata(sourceFile: ts.SourceFile, strict: boolean = false): ModuleMetadata { const locals = new Symbols(sourceFile); const nodeMap = new Map(); const evaluator = new Evaluator(locals, nodeMap); let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; let exports: ModuleExportMetadata[]; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { return evaluator.evaluateNode(decoratorNode.expression); } function recordEntry(entry: T, node: ts.Node): T { nodeMap.set(entry, node); return entry; } function errorSym( message: string, node?: ts.Node, context?: {[name: string]: string}): MetadataError { return errorSymbol(message, node, context, sourceFile); } function maybeGetSimpleFunction( functionDeclaration: ts.FunctionDeclaration | ts.MethodDeclaration): {func: FunctionMetadata, name: string}|undefined { if (functionDeclaration.name.kind == ts.SyntaxKind.Identifier) { const nameNode = functionDeclaration.name; const functionName = nameNode.text; const functionBody = functionDeclaration.body; if (functionBody && functionBody.statements.length == 1) { const statement = functionBody.statements[0]; if (statement.kind === ts.SyntaxKind.ReturnStatement) { const returnStatement = statement; if (returnStatement.expression) { const func: FunctionMetadata = { __symbolic: 'function', parameters: namesOf(functionDeclaration.parameters), value: evaluator.evaluateNode(returnStatement.expression) }; if (functionDeclaration.parameters.some(p => p.initializer != null)) { const defaults: MetadataValue[] = []; func.defaults = functionDeclaration.parameters.map( p => p.initializer && evaluator.evaluateNode(p.initializer)); } return recordEntry({func, name: functionName}, functionDeclaration); } } } } } function classMetadataOf(classDeclaration: ts.ClassDeclaration): ClassMetadata { let result: ClassMetadata = {__symbolic: 'class'}; function getDecorators(decorators: ts.Decorator[]): MetadataSymbolicExpression[] { if (decorators && decorators.length) return decorators.map(decorator => objFromDecorator(decorator)); return undefined; } function referenceFrom(node: ts.Node): MetadataSymbolicReferenceExpression|MetadataError| MetadataSymbolicSelectExpression { const result = evaluator.evaluateNode(node); if (isMetadataError(result) || isMetadataSymbolicReferenceExpression(result) || isMetadataSymbolicSelectExpression(result)) { return result; } else { return errorSym('Symbol reference expected', node); } } // Add class decorators if (classDeclaration.decorators) { result.decorators = getDecorators(classDeclaration.decorators); } // member decorators let members: MetadataMap = null; function recordMember(name: string, metadata: MemberMetadata) { if (!members) members = {}; let data = members.hasOwnProperty(name) ? members[name] : []; data.push(metadata); members[name] = data; } // static member let statics: {[name: string]: MetadataValue | FunctionMetadata} = null; function recordStaticMember(name: string, value: MetadataValue | FunctionMetadata) { if (!statics) statics = {}; statics[name] = value; } for (const member of classDeclaration.members) { let isConstructor = false; switch (member.kind) { case ts.SyntaxKind.Constructor: case ts.SyntaxKind.MethodDeclaration: isConstructor = member.kind === ts.SyntaxKind.Constructor; const method = member; if (method.flags & ts.NodeFlags.Static) { const maybeFunc = maybeGetSimpleFunction(method); if (maybeFunc) { recordStaticMember(maybeFunc.name, maybeFunc.func); } continue; } const methodDecorators = getDecorators(method.decorators); const parameters = method.parameters; const parameterDecoratorData: (MetadataSymbolicExpression | MetadataError)[][] = []; const parametersData: (MetadataSymbolicReferenceExpression | MetadataError | MetadataSymbolicSelectExpression | null)[] = []; let hasDecoratorData: boolean = false; let hasParameterData: boolean = false; for (const parameter of parameters) { const parameterData = getDecorators(parameter.decorators); parameterDecoratorData.push(parameterData); hasDecoratorData = hasDecoratorData || !!parameterData; if (isConstructor) { if (parameter.type) { parametersData.push(referenceFrom(parameter.type)); } else { parametersData.push(null); } hasParameterData = true; } } const data: MethodMetadata = {__symbolic: isConstructor ? 'constructor' : 'method'}; const name = isConstructor ? '__ctor__' : evaluator.nameOf(member.name); if (methodDecorators) { data.decorators = methodDecorators; } if (hasDecoratorData) { data.parameterDecorators = parameterDecoratorData; } if (hasParameterData) { (data).parameters = parametersData; } if (!isMetadataError(name)) { recordMember(name, data); } break; case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.SetAccessor: const property = member; if (property.flags & ts.NodeFlags.Static) { const name = evaluator.nameOf(property.name); if (!isMetadataError(name)) { if (property.initializer) { const value = evaluator.evaluateNode(property.initializer); recordStaticMember(name, value); } else { recordStaticMember(name, errorSym('Variable not initialized', property.name)); } } } const propertyDecorators = getDecorators(property.decorators); if (propertyDecorators) { const name = evaluator.nameOf(property.name); if (!isMetadataError(name)) { recordMember(name, {__symbolic: 'property', decorators: propertyDecorators}); } } break; } } if (members) { result.members = members; } if (statics) { result.statics = statics; } return result.decorators || members || statics ? recordEntry(result, classDeclaration) : undefined; } // Predeclare classes and functions ts.forEachChild(sourceFile, node => { switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: const classDeclaration = node; if (classDeclaration.name) { const className = classDeclaration.name.text; if (node.flags & ts.NodeFlags.Export) { locals.define(className, {__symbolic: 'reference', name: className}); } else { locals.define( className, errorSym('Reference to non-exported class', node, {className})); } } break; case ts.SyntaxKind.FunctionDeclaration: if (!(node.flags & ts.NodeFlags.Export)) { // Report references to this function as an error. const functionDeclaration = node; const nameNode = functionDeclaration.name; if (nameNode && nameNode.text) { locals.define( nameNode.text, errorSym( 'Reference to a non-exported function', nameNode, {name: nameNode.text})); } } break; } }); ts.forEachChild(sourceFile, node => { switch (node.kind) { case ts.SyntaxKind.ExportDeclaration: // Record export declarations const exportDeclaration = node; const moduleSpecifier = exportDeclaration.moduleSpecifier; if (moduleSpecifier && moduleSpecifier.kind == ts.SyntaxKind.StringLiteral) { // Ignore exports that don't have string literals as exports. // This is allowed by the syntax but will be flagged as an error by the type checker. const from = (moduleSpecifier).text; const moduleExport: ModuleExportMetadata = {from}; if (exportDeclaration.exportClause) { moduleExport.export = exportDeclaration.exportClause.elements.map( element => element.propertyName ? {name: element.propertyName.text, as: element.name.text} : element.name.text); } if (!exports) exports = []; exports.push(moduleExport); } break; case ts.SyntaxKind.ClassDeclaration: const classDeclaration = node; if (classDeclaration.name) { const className = classDeclaration.name.text; if (node.flags & ts.NodeFlags.Export) { if (classDeclaration.decorators) { if (!metadata) metadata = {}; metadata[className] = classMetadataOf(classDeclaration); } } } // Otherwise don't record metadata for the class. break; case ts.SyntaxKind.FunctionDeclaration: // Record functions that return a single value. Record the parameter // names substitution will be performed by the StaticReflector. const functionDeclaration = node; if (node.flags & ts.NodeFlags.Export) { const maybeFunc = maybeGetSimpleFunction(functionDeclaration); if (maybeFunc) { if (!metadata) metadata = {}; metadata[maybeFunc.name] = recordEntry(maybeFunc.func, node); } } break; case ts.SyntaxKind.EnumDeclaration: if (node.flags & ts.NodeFlags.Export) { const enumDeclaration = node; let enumValueHolder: {[name: string]: MetadataValue} = {}; const enumName = enumDeclaration.name.text; let nextDefaultValue: MetadataValue = 0; let writtenMembers = 0; for (const member of enumDeclaration.members) { let enumValue: MetadataValue; if (!member.initializer) { enumValue = nextDefaultValue; } else { enumValue = evaluator.evaluateNode(member.initializer); } let name: string = undefined; if (member.name.kind == ts.SyntaxKind.Identifier) { const identifier = member.name; name = identifier.text; enumValueHolder[name] = enumValue; writtenMembers++; } if (typeof enumValue === 'number') { nextDefaultValue = enumValue + 1; } else if (name) { nextDefaultValue = { __symbolic: 'binary', operator: '+', left: { __symbolic: 'select', expression: recordEntry({__symbolic: 'reference', name: enumName}, node), name } }; } else { nextDefaultValue = recordEntry(errorSym('Unsuppported enum member name', member.name), node); }; } if (writtenMembers) { if (!metadata) metadata = {}; metadata[enumName] = recordEntry(enumValueHolder, node); } } break; case ts.SyntaxKind.VariableStatement: const variableStatement = node; for (let variableDeclaration of variableStatement.declarationList.declarations) { if (variableDeclaration.name.kind == ts.SyntaxKind.Identifier) { let nameNode = variableDeclaration.name; let varValue: MetadataValue; if (variableDeclaration.initializer) { varValue = evaluator.evaluateNode(variableDeclaration.initializer); } else { varValue = recordEntry(errorSym('Variable not initialized', nameNode), nameNode); } let exported = false; if (variableStatement.flags & ts.NodeFlags.Export || variableDeclaration.flags & ts.NodeFlags.Export) { if (!metadata) metadata = {}; metadata[nameNode.text] = recordEntry(varValue, node); exported = true; } if (isPrimitive(varValue)) { locals.define(nameNode.text, varValue); } else if (!exported) { if (varValue && !isMetadataError(varValue)) { locals.define(nameNode.text, recordEntry(varValue, node)); } else { locals.define( nameNode.text, recordEntry( errorSym('Reference to a local symbol', nameNode, {name: nameNode.text}), node)); } } } else { // Destructuring (or binding) declarations are not supported, // var {[, ]+} = ; // or // var [[, ; // are not supported. const report = (nameNode: ts.Node) => { switch (nameNode.kind) { case ts.SyntaxKind.Identifier: const name = nameNode; const varValue = errorSym('Destructuring not supported', nameNode); locals.define(name.text, varValue); if (node.flags & ts.NodeFlags.Export) { if (!metadata) metadata = {}; metadata[name.text] = varValue; } break; case ts.SyntaxKind.BindingElement: const bindingElement = nameNode; report(bindingElement.name); break; case ts.SyntaxKind.ObjectBindingPattern: case ts.SyntaxKind.ArrayBindingPattern: const bindings = nameNode; bindings.elements.forEach(report); break; } }; report(variableDeclaration.name); } } break; } }); if (metadata || exports) { if (!metadata) metadata = {}; else if (strict) { validateMetadata(sourceFile, nodeMap, metadata); } const result: ModuleMetadata = {__symbolic: 'module', version: VERSION, metadata}; if (exports) result.exports = exports; return result; } } } // This will throw if the metadata entry given contains an error node. function validateMetadata( sourceFile: ts.SourceFile, nodeMap: Map, metadata: {[name: string]: MetadataEntry}) { let locals: Set = new Set(['Array', 'Object', 'Set', 'Map', 'string', 'number', 'any']); function validateExpression( expression: MetadataValue | MetadataSymbolicExpression | MetadataError) { if (!expression) { return; } else if (Array.isArray(expression)) { expression.forEach(validateExpression); } else if (typeof expression === 'object' && !expression.hasOwnProperty('__symbolic')) { Object.getOwnPropertyNames(expression).forEach(v => validateExpression((expression)[v])); } else if (isMetadataError(expression)) { reportError(expression); } else if (isMetadataGlobalReferenceExpression(expression)) { if (!locals.has(expression.name)) { const reference = metadata[expression.name]; if (reference) { validateExpression(reference); } } } else if (isFunctionMetadata(expression)) { validateFunction(expression); } else if (isMetadataSymbolicExpression(expression)) { switch (expression.__symbolic) { case 'binary': const binaryExpression = expression; validateExpression(binaryExpression.left); validateExpression(binaryExpression.right); break; case 'call': case 'new': const callExpression = expression; validateExpression(callExpression.expression); if (callExpression.arguments) callExpression.arguments.forEach(validateExpression); break; case 'index': const indexExpression = expression; validateExpression(indexExpression.expression); validateExpression(indexExpression.index); break; case 'pre': const prefixExpression = expression; validateExpression(prefixExpression.operand); break; case 'select': const selectExpression = expression; validateExpression(selectExpression.expression); break; case 'spread': const spreadExpression = expression; validateExpression(spreadExpression.expression); break; case 'if': const ifExpression = expression; validateExpression(ifExpression.condition); validateExpression(ifExpression.elseExpression); validateExpression(ifExpression.thenExpression); break; } } } function validateMember(member: MemberMetadata) { if (member.decorators) { member.decorators.forEach(validateExpression); } if (isMethodMetadata(member) && member.parameterDecorators) { member.parameterDecorators.forEach(validateExpression); } if (isConstructorMetadata(member) && member.parameters) { member.parameters.forEach(validateExpression); } } function validateClass(classData: ClassMetadata) { if (classData.decorators) { classData.decorators.forEach(validateExpression); } if (classData.members) { Object.getOwnPropertyNames(classData.members) .forEach(name => classData.members[name].forEach(validateMember)); } } function validateFunction(functionDeclaration: FunctionMetadata) { if (functionDeclaration.value) { const oldLocals = locals; if (functionDeclaration.parameters) { locals = new Set(oldLocals.values()); if (functionDeclaration.parameters) functionDeclaration.parameters.forEach(n => locals.add(n)); } validateExpression(functionDeclaration.value); locals = oldLocals; } } function shouldReportNode(node: ts.Node) { if (node) { const nodeStart = node.getStart(); return !( node.pos != nodeStart && sourceFile.text.substring(node.pos, nodeStart).indexOf('@dynamic') >= 0); } return true; } function reportError(error: MetadataError) { const node = nodeMap.get(error); if (shouldReportNode(node)) { const lineInfo = error.line != undefined ? error.character != undefined ? `:${error.line + 1}:${error.character + 1}` : `:${error.line + 1}` : ''; throw new Error( `${sourceFile.fileName}${lineInfo}: Metadata collected contains an error that will be reported at runtime: ${expandedMessage(error)}.\n ${JSON.stringify(error)}`); } } Object.getOwnPropertyNames(metadata).forEach(name => { const entry = metadata[name]; try { if (isClassMetadata(entry)) { validateClass(entry); } } catch (e) { const node = nodeMap.get(entry); if (shouldReportNode(node)) { if (node) { let {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart()); throw new Error( `${sourceFile.fileName}:${line + 1}:${character + 1}: Error encountered in metadata generated for exported symbol '${name}': \n ${e.message}`); } throw new Error( `Error encountered in metadata generated for exported symbol ${name}: \n ${e.message}`); } } }); } // Collect parameter names from a function. function namesOf(parameters: ts.NodeArray): string[] { let result: string[] = []; function addNamesOf(name: ts.Identifier | ts.BindingPattern) { if (name.kind == ts.SyntaxKind.Identifier) { const identifier = name; result.push(identifier.text); } else { const bindingPattern = name; for (let element of bindingPattern.elements) { addNamesOf(element.name); } } } for (let parameter of parameters) { addNamesOf(parameter.name); } return result; } 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`; } } return error.message; }