|
| 1 | +import * as ts from 'typescript'; |
| 2 | +import {Symbols} from './symbols'; |
| 3 | + |
| 4 | +function isMethodCallOf(callExpression: ts.CallExpression, memberName: string): boolean { |
| 5 | + const expression = callExpression.expression; |
| 6 | + if (expression.kind === ts.SyntaxKind.PropertyAccessExpression) { |
| 7 | + const propertyAccessExpression = <ts.PropertyAccessExpression>expression; |
| 8 | + const name = propertyAccessExpression.name; |
| 9 | + if (name.kind == ts.SyntaxKind.Identifier) { |
| 10 | + return name.text === memberName; |
| 11 | + } |
| 12 | + } |
| 13 | + return false; |
| 14 | +} |
| 15 | + |
| 16 | +function isCallOf(callExpression: ts.CallExpression, ident: string): boolean { |
| 17 | + const expression = callExpression.expression; |
| 18 | + if (expression.kind === ts.SyntaxKind.Identifier) { |
| 19 | + const identifier = <ts.Identifier>expression; |
| 20 | + return identifier.text === ident; |
| 21 | + } |
| 22 | + return false; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * ts.forEachChild stops iterating children when the callback return a truthy value. |
| 27 | + * This method inverts this to implement an `every` style iterator. It will return |
| 28 | + * true if every call to `cb` returns `true`. |
| 29 | + */ |
| 30 | +function everyNodeChild(node: ts.Node, cb: (node: ts.Node) => boolean) { |
| 31 | + return !ts.forEachChild(node, node => !cb(node)); |
| 32 | +} |
| 33 | + |
| 34 | +export interface SymbolReference { |
| 35 | + __symbolic: string; // TODO: Change this to type "reference" when we move to TypeScript 1.8 |
| 36 | + name: string; |
| 37 | + module: string; |
| 38 | +} |
| 39 | + |
| 40 | +function isPrimitive(value: any): boolean { |
| 41 | + return Object(value) !== value; |
| 42 | +} |
| 43 | + |
| 44 | +function isDefined(obj: any): boolean { |
| 45 | + return obj !== undefined; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * Produce a symbolic representation of an expression folding values into their final value when |
| 50 | + * possible. |
| 51 | + */ |
| 52 | +export class Evaluator { |
| 53 | + constructor(private service: ts.LanguageService, private typeChecker: ts.TypeChecker, |
| 54 | + private symbols: Symbols, private moduleNameOf: (fileName: string) => string) {} |
| 55 | + |
| 56 | + // TODO: Determine if the first declaration is deterministic. |
| 57 | + private symbolFileName(symbol: ts.Symbol): string { |
| 58 | + if (symbol) { |
| 59 | + if (symbol.flags & ts.SymbolFlags.Alias) { |
| 60 | + symbol = this.typeChecker.getAliasedSymbol(symbol); |
| 61 | + } |
| 62 | + const declarations = symbol.getDeclarations(); |
| 63 | + if (declarations && declarations.length > 0) { |
| 64 | + const sourceFile = declarations[0].getSourceFile(); |
| 65 | + if (sourceFile) { |
| 66 | + return sourceFile.fileName; |
| 67 | + } |
| 68 | + } |
| 69 | + } |
| 70 | + return undefined; |
| 71 | + } |
| 72 | + |
| 73 | + private symbolReference(symbol: ts.Symbol): SymbolReference { |
| 74 | + if (symbol) { |
| 75 | + const name = symbol.name; |
| 76 | + const module = this.moduleNameOf(this.symbolFileName(symbol)); |
| 77 | + return {__symbolic: "reference", name, module}; |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + private nodeSymbolReference(node: ts.Node): SymbolReference { |
| 82 | + return this.symbolReference(this.typeChecker.getSymbolAtLocation(node)); |
| 83 | + } |
| 84 | + |
| 85 | + nameOf(node: ts.Node): string { |
| 86 | + if (node.kind == ts.SyntaxKind.Identifier) { |
| 87 | + return (<ts.Identifier>node).text; |
| 88 | + } |
| 89 | + return this.evaluateNode(node); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * Returns true if the expression represented by `node` can be folded into a literal expression. |
| 94 | + * |
| 95 | + * For example, a literal is always foldable. This means that literal expressions such as `1.2` |
| 96 | + * `"Some value"` `true` `false` are foldable. |
| 97 | + * |
| 98 | + * - An object literal is foldable if all the properties in the literal are foldable. |
| 99 | + * - An array literal is foldable if all the elements are foldable. |
| 100 | + * - A call is foldable if it is a call to a Array.prototype.concat or a call to CONST_EXPR. |
| 101 | + * - A property access is foldable if the object is foldable. |
| 102 | + * - A array index is foldable if index expression is foldable and the array is foldable. |
| 103 | + * - Binary operator expressions are foldable if the left and right expressions are foldable and |
| 104 | + * it is one of '+', '-', '*', '/', '%', '||', and '&&'. |
| 105 | + * - An identifier is foldable if a value can be found for its symbol is in the evaluator symbol |
| 106 | + * table. |
| 107 | + */ |
| 108 | + public isFoldable(node: ts.Node) { |
| 109 | + if (node) { |
| 110 | + switch (node.kind) { |
| 111 | + case ts.SyntaxKind.ObjectLiteralExpression: |
| 112 | + return everyNodeChild(node, child => { |
| 113 | + if (child.kind === ts.SyntaxKind.PropertyAssignment) { |
| 114 | + const propertyAssignment = <ts.PropertyAssignment>child; |
| 115 | + return this.isFoldable(propertyAssignment.initializer) |
| 116 | + } |
| 117 | + return false; |
| 118 | + }); |
| 119 | + case ts.SyntaxKind.ArrayLiteralExpression: |
| 120 | + return everyNodeChild(node, child => this.isFoldable(child)); |
| 121 | + case ts.SyntaxKind.CallExpression: |
| 122 | + const callExpression = <ts.CallExpression>node; |
| 123 | + // We can fold a <array>.concat(<v>). |
| 124 | + if (isMethodCallOf(callExpression, "concat") && callExpression.arguments.length === 1) { |
| 125 | + const arrayNode = (<ts.PropertyAccessExpression>callExpression.expression).expression; |
| 126 | + if (this.isFoldable(arrayNode) && this.isFoldable(callExpression.arguments[0])) { |
| 127 | + // It needs to be an array. |
| 128 | + const arrayValue = this.evaluateNode(arrayNode); |
| 129 | + if (arrayValue && Array.isArray(arrayValue)) { |
| 130 | + return true; |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + // We can fold a call to CONST_EXPR |
| 135 | + if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) |
| 136 | + return this.isFoldable(callExpression.arguments[0]); |
| 137 | + return false; |
| 138 | + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: |
| 139 | + case ts.SyntaxKind.StringLiteral: |
| 140 | + case ts.SyntaxKind.NumericLiteral: |
| 141 | + case ts.SyntaxKind.NullKeyword: |
| 142 | + case ts.SyntaxKind.TrueKeyword: |
| 143 | + case ts.SyntaxKind.FalseKeyword: |
| 144 | + return true; |
| 145 | + case ts.SyntaxKind.BinaryExpression: |
| 146 | + const binaryExpression = <ts.BinaryExpression>node; |
| 147 | + switch (binaryExpression.operatorToken.kind) { |
| 148 | + case ts.SyntaxKind.PlusToken: |
| 149 | + case ts.SyntaxKind.MinusToken: |
| 150 | + case ts.SyntaxKind.AsteriskToken: |
| 151 | + case ts.SyntaxKind.SlashToken: |
| 152 | + case ts.SyntaxKind.PercentToken: |
| 153 | + case ts.SyntaxKind.AmpersandAmpersandToken: |
| 154 | + case ts.SyntaxKind.BarBarToken: |
| 155 | + return this.isFoldable(binaryExpression.left) && |
| 156 | + this.isFoldable(binaryExpression.right); |
| 157 | + } |
| 158 | + case ts.SyntaxKind.PropertyAccessExpression: |
| 159 | + const propertyAccessExpression = <ts.PropertyAccessExpression>node; |
| 160 | + return this.isFoldable(propertyAccessExpression.expression); |
| 161 | + case ts.SyntaxKind.ElementAccessExpression: |
| 162 | + const elementAccessExpression = <ts.ElementAccessExpression>node; |
| 163 | + return this.isFoldable(elementAccessExpression.expression) && |
| 164 | + this.isFoldable(elementAccessExpression.argumentExpression); |
| 165 | + case ts.SyntaxKind.Identifier: |
| 166 | + const symbol = this.typeChecker.getSymbolAtLocation(node); |
| 167 | + if (this.symbols.has(symbol)) return true; |
| 168 | + break; |
| 169 | + } |
| 170 | + } |
| 171 | + return false; |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Produce a JSON serialiable object representing `node`. The foldable values in the expression |
| 176 | + * tree are folded. For example, a node representing `1 + 2` is folded into `3`. |
| 177 | + */ |
| 178 | + public evaluateNode(node: ts.Node): any { |
| 179 | + switch (node.kind) { |
| 180 | + case ts.SyntaxKind.ObjectLiteralExpression: |
| 181 | + let obj = {}; |
| 182 | + let allPropertiesDefined = true; |
| 183 | + ts.forEachChild(node, child => { |
| 184 | + switch (child.kind) { |
| 185 | + case ts.SyntaxKind.PropertyAssignment: |
| 186 | + const assignment = <ts.PropertyAssignment>child; |
| 187 | + const propertyName = this.nameOf(assignment.name); |
| 188 | + const propertyValue = this.evaluateNode(assignment.initializer); |
| 189 | + obj[propertyName] = propertyValue; |
| 190 | + allPropertiesDefined = isDefined(propertyValue) && allPropertiesDefined; |
| 191 | + } |
| 192 | + }); |
| 193 | + if (allPropertiesDefined) return obj; |
| 194 | + break; |
| 195 | + case ts.SyntaxKind.ArrayLiteralExpression: |
| 196 | + let arr = []; |
| 197 | + let allElementsDefined = true; |
| 198 | + ts.forEachChild(node, child => { |
| 199 | + const value = this.evaluateNode(child); |
| 200 | + arr.push(value); |
| 201 | + allElementsDefined = isDefined(value) && allElementsDefined; |
| 202 | + }); |
| 203 | + if (allElementsDefined) return arr; |
| 204 | + break; |
| 205 | + case ts.SyntaxKind.CallExpression: |
| 206 | + const callExpression = <ts.CallExpression>node; |
| 207 | + const args = callExpression.arguments.map(arg => this.evaluateNode(arg)); |
| 208 | + if (this.isFoldable(callExpression)) { |
| 209 | + if (isMethodCallOf(callExpression, "concat")) { |
| 210 | + const arrayValue = this.evaluateNode( |
| 211 | + (<ts.PropertyAccessExpression>callExpression.expression).expression); |
| 212 | + return arrayValue.concat(args[0]); |
| 213 | + } |
| 214 | + } |
| 215 | + // Always fold a CONST_EXPR even if the argument is not foldable. |
| 216 | + if (isCallOf(callExpression, "CONST_EXPR") && callExpression.arguments.length === 1) { |
| 217 | + return args[0]; |
| 218 | + } |
| 219 | + const expression = this.evaluateNode(callExpression.expression); |
| 220 | + if (isDefined(expression) && args.every(isDefined)) { |
| 221 | + return { |
| 222 | + __symbolic: "call", |
| 223 | + expression: this.evaluateNode(callExpression.expression), |
| 224 | + arguments: args |
| 225 | + }; |
| 226 | + } |
| 227 | + break; |
| 228 | + case ts.SyntaxKind.PropertyAccessExpression: { |
| 229 | + const propertyAccessExpression = <ts.PropertyAccessExpression>node; |
| 230 | + const expression = this.evaluateNode(propertyAccessExpression.expression); |
| 231 | + const member = this.nameOf(propertyAccessExpression.name); |
| 232 | + if (this.isFoldable(propertyAccessExpression.expression)) return expression[member]; |
| 233 | + if (isDefined(expression)) { |
| 234 | + return {__symbolic: "select", expression, member}; |
| 235 | + } |
| 236 | + break; |
| 237 | + } |
| 238 | + case ts.SyntaxKind.ElementAccessExpression: { |
| 239 | + const elementAccessExpression = <ts.ElementAccessExpression>node; |
| 240 | + const expression = this.evaluateNode(elementAccessExpression.expression); |
| 241 | + const index = this.evaluateNode(elementAccessExpression.argumentExpression); |
| 242 | + if (this.isFoldable(elementAccessExpression.expression) && |
| 243 | + this.isFoldable(elementAccessExpression.argumentExpression)) |
| 244 | + return expression[index]; |
| 245 | + if (isDefined(expression) && isDefined(index)) { |
| 246 | + return { |
| 247 | + __symbolic: "index", |
| 248 | + expression, |
| 249 | + index: this.evaluateNode(elementAccessExpression.argumentExpression) |
| 250 | + }; |
| 251 | + } |
| 252 | + break; |
| 253 | + } |
| 254 | + case ts.SyntaxKind.Identifier: |
| 255 | + const symbol = this.typeChecker.getSymbolAtLocation(node); |
| 256 | + if (this.symbols.has(symbol)) return this.symbols.get(symbol); |
| 257 | + return this.nodeSymbolReference(node); |
| 258 | + case ts.SyntaxKind.NoSubstitutionTemplateLiteral: |
| 259 | + return (<ts.LiteralExpression>node).text; |
| 260 | + case ts.SyntaxKind.StringLiteral: |
| 261 | + return (<ts.StringLiteral>node).text; |
| 262 | + case ts.SyntaxKind.NumericLiteral: |
| 263 | + return parseFloat((<ts.LiteralExpression>node).text); |
| 264 | + case ts.SyntaxKind.NullKeyword: |
| 265 | + return null; |
| 266 | + case ts.SyntaxKind.TrueKeyword: |
| 267 | + return true; |
| 268 | + case ts.SyntaxKind.FalseKeyword: |
| 269 | + return false; |
| 270 | + |
| 271 | + case ts.SyntaxKind.BinaryExpression: |
| 272 | + const binaryExpression = <ts.BinaryExpression>node; |
| 273 | + const left = this.evaluateNode(binaryExpression.left); |
| 274 | + const right = this.evaluateNode(binaryExpression.right); |
| 275 | + if (isDefined(left) && isDefined(right)) { |
| 276 | + if (isPrimitive(left) && isPrimitive(right)) |
| 277 | + switch (binaryExpression.operatorToken.kind) { |
| 278 | + case ts.SyntaxKind.PlusToken: |
| 279 | + return left + right; |
| 280 | + case ts.SyntaxKind.MinusToken: |
| 281 | + return left - right; |
| 282 | + case ts.SyntaxKind.AsteriskToken: |
| 283 | + return left * right; |
| 284 | + case ts.SyntaxKind.SlashToken: |
| 285 | + return left / right; |
| 286 | + case ts.SyntaxKind.PercentToken: |
| 287 | + return left % right; |
| 288 | + case ts.SyntaxKind.AmpersandAmpersandToken: |
| 289 | + return left && right; |
| 290 | + case ts.SyntaxKind.BarBarToken: |
| 291 | + return left || right; |
| 292 | + } |
| 293 | + return { |
| 294 | + __symbolic: "binop", |
| 295 | + operator: binaryExpression.operatorToken.getText(), |
| 296 | + left: left, |
| 297 | + right: right |
| 298 | + }; |
| 299 | + } |
| 300 | + break; |
| 301 | + } |
| 302 | + return undefined; |
| 303 | + } |
| 304 | +} |
0 commit comments