import * as ts from 'typescript'; import {Evaluator, errorSymbol, isPrimitive} from './evaluator'; import {ClassMetadata, ConstructorMetadata, FunctionMetadata, MemberMetadata, MetadataError, MetadataMap, MetadataObject, MetadataSymbolicExpression, MetadataSymbolicReferenceExpression, MetadataSymbolicSelectExpression, MetadataValue, MethodMetadata, ModuleExportMetadata, ModuleMetadata, VERSION, isMetadataError, isMetadataSymbolicReferenceExpression, isMetadataSymbolicSelectExpression} 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): ModuleMetadata { const locals = new Symbols(sourceFile); const evaluator = new Evaluator(locals); let metadata: {[name: string]: MetadataValue | ClassMetadata | FunctionMetadata}|undefined; let exports: ModuleExportMetadata[]; function objFromDecorator(decoratorNode: ts.Decorator): MetadataSymbolicExpression { return evaluator.evaluateNode(decoratorNode.expression); } 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 { func, name: functionName } } } } } } 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 ? result : undefined; } // Predeclare classes ts.forEachChild(sourceFile, node => { switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: const classDeclaration = node; 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; } }); 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; 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. if (node.flags & ts.NodeFlags.Export) { const functionDeclaration = node; const maybeFunc = maybeGetSimpleFunction(functionDeclaration); if (maybeFunc) { if (!metadata) metadata = {}; metadata[maybeFunc.name] = maybeFunc.func; } } // Otherwise don't record the function. break; case ts.SyntaxKind.EnumDeclaration: 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: {__symbolic: 'reference', name: enumName}, name } } } else { nextDefaultValue = errorSym('Unsuppported enum member name', member.name); }; } if (writtenMembers) { if (!metadata) metadata = {}; metadata[enumName] = enumValueHolder; } 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 = errorSym('Variable not initialized', nameNode); } if (variableStatement.flags & ts.NodeFlags.Export || variableDeclaration.flags & ts.NodeFlags.Export) { if (!metadata) metadata = {}; metadata[nameNode.text] = varValue; } if (isPrimitive(varValue)) { locals.define(nameNode.text, varValue); } } 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 = {}; const result: ModuleMetadata = {__symbolic: 'module', version: VERSION, metadata}; if (exports) result.exports = exports; return result; } } } // 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; }