import * as ts from 'typescript'; import {Symbols} from './symbols'; import { MetadataValue, MetadataSymbolicCallExpression, MetadataSymbolicReferenceExpression } from './schema'; // TOOD: Remove when tools directory is upgraded to support es6 target interface Map { has(k: K): boolean; set(k: K, v: V): void; get(k: K): V; delete (k: K): void; } interface MapConstructor { new(): Map; } declare var Map: MapConstructor; function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { const expression = callExpression.expression; if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { const propertyAccessExpression = expression; const name = propertyAccessExpression.name; if (name.kind == ts.SyntaxKind.Identifier) { return name.text === memberName; } } return false; } function isCallOf(callExpression: ts.CallExpression, ident: string): boolean { const expression = callExpression.expression; if (expression.kind === ts.SyntaxKind.Identifier) { const identifier = expression; return identifier.text === ident; } return false; } /** * ts.forEachChild stops iterating children when the callback return a truthy value. * This method inverts this to implement an `every` style iterator. It will return * true if every call to `cb` returns `true`. */ function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) { return !ts.forEachChild(node, node => !cb(node)); } function isPrimitive(value: any): boolean { return Object(value) !== value; } function isDefined(obj: any): boolean { return obj !== undefined; } // import {propertyName as name} from 'place' // import {name} from 'place' export interface ImportSpecifierMetadata { name: string; propertyName?: string; } export interface ImportMetadata { defaultName?: string; // import d from 'place' namespace?: string; // import * as d from 'place' namedImports?: ImportSpecifierMetadata[]; // import {a} from 'place' from: string; // from 'place' } /** * Produce a symbolic representation of an expression folding values into their final value when * possible. */ export class Evaluator { constructor(private typeChecker: ts.TypeChecker, private symbols: Symbols, private imports: ImportMetadata[]) {} symbolReference(symbol: ts.Symbol): MetadataSymbolicReferenceExpression { if (symbol) { let module: string; let name = symbol.name; for (const eachImport of this.imports) { if (symbol.name === eachImport.defaultName) { module = eachImport.from; name = undefined; } if (eachImport.namedImports) { for (const named of eachImport.namedImports) { if (symbol.name === named.name) { name = named.propertyName ? named.propertyName : named.name; module = eachImport.from; break; } } } } return {__symbolic: "reference", name, module}; } } private findImportNamespace(node: ts.Node) { if (node.kind === ts.SyntaxKind.PropertyAccessExpression) { const lhs = (node).expression; if (lhs.kind === ts.SyntaxKind.Identifier) { // TOOD: Use Array.find when tools directory is upgraded to support es6 target for (const eachImport of this.imports) { if (eachImport.namespace === (lhs).text) { return eachImport; } } } } } private nodeSymbolReference(node: ts.Node): MetadataSymbolicReferenceExpression { const importNamespace = this.findImportNamespace(node); if (importNamespace) { const result = this.symbolReference( this.typeChecker.getSymbolAtLocation((node).name)); result.module = importNamespace.from; return result; } return this.symbolReference(this.typeChecker.getSymbolAtLocation(node)); } nameOf(node: ts.Node): string { if (node.kind == ts.SyntaxKind.Identifier) { return (node).text; } return this.evaluateNode(node); } /** * Returns true if the expression represented by `node` can be folded into a literal expression. * * For example, a literal is always foldable. This means that literal expressions such as `1.2` * `"Some value"` `true` `false` are foldable. * * - An object literal is foldable if all the properties in the literal are foldable. * - An array literal is foldable if all the elements are foldable. * - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR. * - A property access is foldable if the object is foldable. * - A array index is foldable if index expression is foldable and the array is foldable. * - Binary operator expressions are foldable if the left and right expressions are foldable and * it is one of '+', '-', '*', '/', '%', '||', and '&&'. * - An identifier is foldable if a value can be found for its symbol in the evaluator symbol * table. */ public isFoldable(node: ts.Node): boolean { return this.isFoldableWorker(node, new Map()); } private isFoldableWorker(node: ts.Node, folding: Map): boolean { if (node) { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: return everyNodeChild(node, child => { if (child.kind === ts.SyntaxKind.PropertyAssignment) { const propertyAssignment = child; return this.isFoldableWorker(propertyAssignment.initializer, folding); } return false; }); case ts.SyntaxKind.ArrayLiteralExpression: return everyNodeChild(node, child => this.isFoldableWorker(child, folding)); case ts.SyntaxKind.CallExpression: const callExpression = node; // We can fold a .concat(). if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) { const arrayNode = (callExpression.expression).expression; if (this.isFoldableWorker(arrayNode, folding) && this.isFoldableWorker(callExpression.arguments[0], folding)) { // It needs to be an array. const arrayValue = this.evaluateNode(arrayNode); if (arrayValue && Array.isArray(arrayValue)) { return true; } } } // We can fold a call to CONST_EXPR if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) return this.isFoldableWorker(callExpression.arguments[0], folding); return false; case ts.SyntaxKind.NoSubstitutionTemplateLiteral: case ts.SyntaxKind.StringLiteral: case ts.SyntaxKind.NumericLiteral: case ts.SyntaxKind.NullKeyword: case ts.SyntaxKind.TrueKeyword: case ts.SyntaxKind.FalseKeyword: return true; case ts.SyntaxKind.ParenthesizedExpression: const parenthesizedExpression = node; return this.isFoldableWorker(parenthesizedExpression.expression, folding); case ts.SyntaxKind.BinaryExpression: const binaryExpression = node; switch (binaryExpression.operatorToken.kind) { case ts.SyntaxKind.PlusToken: case ts.SyntaxKind.MinusToken: case ts.SyntaxKind.AsteriskToken: case ts.SyntaxKind.SlashToken: case ts.SyntaxKind.PercentToken: case ts.SyntaxKind.AmpersandAmpersandToken: case ts.SyntaxKind.BarBarToken: return this.isFoldableWorker(binaryExpression.left, folding) && this.isFoldableWorker(binaryExpression.right, folding); } case ts.SyntaxKind.PropertyAccessExpression: const propertyAccessExpression = node; return this.isFoldableWorker(propertyAccessExpression.expression, folding); case ts.SyntaxKind.ElementAccessExpression: const elementAccessExpression = node; return this.isFoldableWorker(elementAccessExpression.expression, folding) && this.isFoldableWorker(elementAccessExpression.argumentExpression, folding); case ts.SyntaxKind.Identifier: let symbol = this.typeChecker.getSymbolAtLocation(node); if (symbol.flags & ts.SymbolFlags.Alias) { symbol = this.typeChecker.getAliasedSymbol(symbol); } if (this.symbols.has(symbol)) return true; // If this is a reference to a foldable variable then it is foldable too. const variableDeclaration = ( symbol.declarations && symbol.declarations.length && symbol.declarations[0]); if (variableDeclaration.kind === ts.SyntaxKind.VariableDeclaration) { const initializer = variableDeclaration.initializer; if (folding.has(initializer)) { // A recursive reference is not foldable. return false; } folding.set(initializer, true); const result = this.isFoldableWorker(initializer, folding); folding.delete(initializer); return result; } break; } } return false; } /** * Produce a JSON serialiable object representing `node`. The foldable values in the expression * tree are folded. For example, a node representing `1 + 2` is folded into `3`. */ public evaluateNode(node: ts.Node): MetadataValue { switch (node.kind) { case ts.SyntaxKind.ObjectLiteralExpression: let obj: MetadataValue = {}; let allPropertiesDefined = true; ts.forEachChild(node, child => { switch (child.kind) { case ts.SyntaxKind.PropertyAssignment: const assignment = child; const propertyName = this.nameOf(assignment.name); const propertyValue = this.evaluateNode(assignment.initializer); obj[propertyName] = propertyValue; allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined; } }); if (allPropertiesDefined) return obj; break; case ts.SyntaxKind.ArrayLiteralExpression: let arr = []; let allElementsDefined = true; ts.forEachChild(node, child => { const value = this.evaluateNode(child); arr.push(value); allElementsDefined = isDefined(value) && allElementsDefined; }); if (allElementsDefined) return arr; break; case ts.SyntaxKind.CallExpression: const callExpression = node; const args = callExpression.arguments.map(arg => this.evaluateNode(arg)); if (this.isFoldable(callExpression)) { if (isMethodCallOf(callExpression, "concat")) { const arrayValue = this.evaluateNode( (callExpression.expression).expression); return arrayValue.concat(args[0]); } } // Always fold a CONST_EXPR even if the argument is not foldable. if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) { return args[0]; } if (isCallOf(callExpression, 'forwardRef') && callExpression.arguments.length === 1) { const firstArgument = callExpression.arguments[0]; if (firstArgument.kind == ts.SyntaxKind.ArrowFunction) { const arrowFunction = firstArgument; return this.evaluateNode(arrowFunction.body); } } const expression = this.evaluateNode(callExpression.expression); if (isDefined(expression) && args.every(isDefined)) { const result: MetadataSymbolicCallExpression = {__symbolic: "call", expression: expression}; if (args && args.length) { result.arguments = args; } return result; } break; case ts.SyntaxKind.NewExpression: const newExpression = node; const newArgs = newExpression.arguments.map(arg => this.evaluateNode(arg)); const newTarget = this.evaluateNode(newExpression.expression); if (isDefined(newTarget) && newArgs.every(isDefined)) { const result: MetadataSymbolicCallExpression = {__symbolic: "new", expression: newTarget}; if (newArgs.length) { result.arguments = newArgs; } return result; } break; case ts.SyntaxKind.PropertyAccessExpression: { const propertyAccessExpression = node; const expression = this.evaluateNode(propertyAccessExpression.expression); const member = this.nameOf(propertyAccessExpression.name); if (this.isFoldable(propertyAccessExpression.expression)) return expression[member]; if (this.findImportNamespace(propertyAccessExpression)) { return this.nodeSymbolReference(propertyAccessExpression); } if (isDefined(expression)) { return {__symbolic: "select", expression, member}; } break; } case ts.SyntaxKind.ElementAccessExpression: { const elementAccessExpression = node; const expression = this.evaluateNode(elementAccessExpression.expression); const index = this.evaluateNode(elementAccessExpression.argumentExpression); if (this.isFoldable(elementAccessExpression.expression) && this.isFoldable(elementAccessExpression.argumentExpression)) return expression[index]; if (isDefined(expression) && isDefined(index)) { return {__symbolic: "index", expression, index}; } break; } case ts.SyntaxKind.Identifier: let symbol = this.typeChecker.getSymbolAtLocation(node); if (symbol.flags & ts.SymbolFlags.Alias) { symbol = this.typeChecker.getAliasedSymbol(symbol); } if (this.symbols.has(symbol)) return this.symbols.get(symbol); if (this.isFoldable(node)) { // isFoldable implies, in this context, symbol declaration is a VariableDeclaration const variableDeclaration = ( symbol.declarations && symbol.declarations.length && symbol.declarations[0]); return this.evaluateNode(variableDeclaration.initializer); } return this.nodeSymbolReference(node); case ts.SyntaxKind.NoSubstitutionTemplateLiteral: return (node).text; case ts.SyntaxKind.StringLiteral: return (node).text; case ts.SyntaxKind.NumericLiteral: return parseFloat((node).text); case ts.SyntaxKind.NullKeyword: return null; case ts.SyntaxKind.TrueKeyword: return true; case ts.SyntaxKind.FalseKeyword: return false; case ts.SyntaxKind.ParenthesizedExpression: const parenthesizedExpression = node; return this.evaluateNode(parenthesizedExpression.expression); case ts.SyntaxKind.TypeAssertionExpression: const typeAssertion = node; return this.evaluateNode(typeAssertion.expression); case ts.SyntaxKind.PrefixUnaryExpression: const prefixUnaryExpression = node; const operand = this.evaluateNode(prefixUnaryExpression.operand); if (isDefined(operand) && isPrimitive(operand)) { switch (prefixUnaryExpression.operator) { case ts.SyntaxKind.PlusToken: return +operand; case ts.SyntaxKind.MinusToken: return -operand; case ts.SyntaxKind.TildeToken: return ~operand; case ts.SyntaxKind.ExclamationToken: return !operand; } } let operatorText: string; switch (prefixUnaryExpression.operator) { case ts.SyntaxKind.PlusToken: operatorText = '+'; break; case ts.SyntaxKind.MinusToken: operatorText = '-'; break; case ts.SyntaxKind.TildeToken: operatorText = '~'; break; case ts.SyntaxKind.ExclamationToken: operatorText = '!'; break; default: return undefined; } return {__symbolic: "pre", operator: operatorText, operand: operand }; case ts.SyntaxKind.BinaryExpression: const binaryExpression = node; const left = this.evaluateNode(binaryExpression.left); const right = this.evaluateNode(binaryExpression.right); if (isDefined(left) && isDefined(right)) { if (isPrimitive(left) && isPrimitive(right)) switch (binaryExpression.operatorToken.kind) { case ts.SyntaxKind.BarBarToken: return left || right; case ts.SyntaxKind.AmpersandAmpersandToken: return left && right; case ts.SyntaxKind.AmpersandToken: return left & right; case ts.SyntaxKind.BarToken: return left | right; case ts.SyntaxKind.CaretToken: return left ^ right; case ts.SyntaxKind.EqualsEqualsToken: return left == right; case ts.SyntaxKind.ExclamationEqualsToken: return left != right; case ts.SyntaxKind.EqualsEqualsEqualsToken: return left === right; case ts.SyntaxKind.ExclamationEqualsEqualsToken: return left !== right; case ts.SyntaxKind.LessThanToken: return left < right; case ts.SyntaxKind.GreaterThanToken: return left > right; case ts.SyntaxKind.LessThanEqualsToken: return left <= right; case ts.SyntaxKind.GreaterThanEqualsToken: return left >= right; case ts.SyntaxKind.LessThanLessThanToken: return (left) << (right); case ts.SyntaxKind.GreaterThanGreaterThanToken: return left >> right; case ts.SyntaxKind.GreaterThanGreaterThanGreaterThanToken: return left >>> right; case ts.SyntaxKind.PlusToken: return left + right; case ts.SyntaxKind.MinusToken: return left - right; case ts.SyntaxKind.AsteriskToken: return left * right; case ts.SyntaxKind.SlashToken: return left / right; case ts.SyntaxKind.PercentToken: return left % right; } return { __symbolic: "binop", operator: binaryExpression.operatorToken.getText(), left: left, right: right }; } break; } return undefined; } }