├── .gitignore ├── README.md ├── demo.html ├── esoptimize.js ├── esscope.js ├── package.json └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Esoptimize 2 | 3 | Esoptimize is a JavaScript optimizer that is designed to work well with [esprima](http://github.com/Constellation/esprima) and [escodegen](http://github.com/Constellation/escodegen). 4 | 5 | ### Usage 6 | 7 | Esoptimize can be installed using `npm install esoptimize` and used by calling `esoptimize.optimize(ast)` where `ast` is a JavaScript abstract syntax tree that conforms to the [SpiderMonkey Parser API](https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API) format. 8 | 9 | ### Features 10 | 11 | * Constant propagation 12 | * Dead code elimination 13 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Esoptimize Demo 6 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |

Esoptimize Demo

49 |

50 | This is a demo of esoptimize, a JavaScript AST optimizer. 51 | It performs constant folding and dead code elimination. 52 |

53 | 54 | 55 | 56 | 62 | 66 | 67 |
57 |

Input

58 | 61 |
63 |

Output

64 | 65 |
68 | 69 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /esoptimize.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var esscope = typeof window !== 'undefined' ? window.esscope : require('./esscope'); 5 | var estraverse = typeof window !== 'undefined' ? window.estraverse : require('estraverse'); 6 | var esoptimize = typeof window !== 'undefined' ? (window.esoptimize = {}) : exports; 7 | 8 | var isValidIdentifier = new RegExp('^(?!(?:' + [ 9 | 'do', 10 | 'if', 11 | 'in', 12 | 'for', 13 | 'let', 14 | 'new', 15 | 'try', 16 | 'var', 17 | 'case', 18 | 'else', 19 | 'enum', 20 | 'eval', 21 | 'false', 22 | 'null', 23 | 'this', 24 | 'true', 25 | 'void', 26 | 'with', 27 | 'break', 28 | 'catch', 29 | 'class', 30 | 'const', 31 | 'super', 32 | 'throw', 33 | 'while', 34 | 'yield', 35 | 'delete', 36 | 'export', 37 | 'import', 38 | 'public', 39 | 'return', 40 | 'static', 41 | 'switch', 42 | 'typeof', 43 | 'default', 44 | 'extends', 45 | 'finally', 46 | 'package', 47 | 'private', 48 | 'continue', 49 | 'debugger', 50 | 'function', 51 | 'arguments', 52 | 'interface', 53 | 'protected', 54 | 'implements', 55 | 'instanceof' 56 | ].join('|') + ')$)[$A-Z_a-z][$A-Z_a-z0-9]*$'); 57 | 58 | var oppositeOperator = { 59 | '&&': '||', 60 | '||': '&&', 61 | '<': '>=', 62 | '>': '<=', 63 | '<=': '>', 64 | '>=': '<', 65 | '==': '!=', 66 | '!=': '==', 67 | '!==': '===', 68 | '===': '!==' 69 | }; 70 | 71 | var parent = null; 72 | var scope = null; 73 | 74 | function assert(truth) { 75 | if (!truth) { 76 | throw new Error('assertion failed'); 77 | } 78 | } 79 | 80 | function hasSideEffects(node) { 81 | if (node.type === 'Literal' || node.type === 'Identifier' || node.type === 'FunctionExpression') { 82 | return false; 83 | } 84 | 85 | if (node.type === 'MemberExpression') { 86 | return hasSideEffects(node.object) || hasSideEffects(node.property); 87 | } 88 | 89 | if (node.type === 'SequenceExpression') { 90 | return node.expressions.some(hasSideEffects); 91 | } 92 | 93 | if (node.type === 'ArrayExpression') { 94 | return node.elements.some(hasSideEffects); 95 | } 96 | 97 | if (node.type === 'ObjectExpression') { 98 | return node.properties.some(function(property) { 99 | return hasSideEffects(property.value); 100 | }); 101 | } 102 | 103 | return true; 104 | } 105 | 106 | function declareScopeVariables() { 107 | var variables = scope.variables; 108 | var node = scope.node; 109 | 110 | variables = variables.filter(function(variable) { 111 | return variable.isVariable() && !variable.isArgument(); 112 | }); 113 | 114 | if (variables.length === 0) { 115 | return { 116 | type: 'EmptyStatement' 117 | }; 118 | } 119 | 120 | return { 121 | type: 'VariableDeclaration', 122 | declarations: variables.map(function(variable) { 123 | return { 124 | type: 'VariableDeclarator', 125 | id: { 126 | type: 'Identifier', 127 | name: variable.name 128 | }, 129 | init: null 130 | }; 131 | }), 132 | kind: 'var' 133 | }; 134 | } 135 | 136 | function normalize(node) { 137 | return estraverse.replace(node, wrapVisitorScope({ 138 | leave: function(node) { 139 | // Hoist global variables 140 | if (node.type === 'Program') { 141 | return { 142 | type: 'Program', 143 | body: [declareScopeVariables()].concat(node.body) 144 | }; 145 | } 146 | 147 | // Hoist local variables 148 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') { 149 | return { 150 | type: node.type, 151 | id: node.id, 152 | params: node.params, 153 | defaults: node.defaults, 154 | body: { 155 | type: 'BlockStatement', 156 | body: [declareScopeVariables()].concat(node.body.body) 157 | }, 158 | rest: node.rest, 159 | generator: node.generator, 160 | expression: node.expression 161 | }; 162 | } 163 | 164 | if (node.type === 'Property') { 165 | assert(node.key.type === 'Literal' || node.key.type === 'Identifier'); 166 | return { 167 | type: 'Property', 168 | key: { 169 | type: 'Literal', 170 | value: node.key.type === 'Literal' ? node.key.value + '' : node.key.name 171 | }, 172 | value: node.value, 173 | kind: node.kind 174 | }; 175 | } 176 | 177 | if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier') { 178 | return { 179 | type: 'MemberExpression', 180 | computed: true, 181 | object: node.object, 182 | property: { 183 | type: 'Literal', 184 | value: node.property.name 185 | } 186 | }; 187 | } 188 | 189 | if (node.type === 'VariableDeclaration') { 190 | var expressions = node.declarations.filter(function(node) { 191 | return node.init !== null; 192 | }).map(function(node) { 193 | return { 194 | type: 'AssignmentExpression', 195 | operator: '=', 196 | left: node.id, 197 | right: node.init 198 | } 199 | }); 200 | 201 | if (expressions.length === 0) { 202 | return { 203 | type: 'EmptyStatement' 204 | }; 205 | } 206 | 207 | return { 208 | type: 'ExpressionStatement', 209 | expression: { 210 | type: 'SequenceExpression', 211 | expressions: expressions 212 | } 213 | }; 214 | } 215 | 216 | if (node.type === 'ForStatement' && node.init !== null) { 217 | return { 218 | type: 'ForStatement', 219 | init: 220 | node.init.type === 'EmptyStatement' ? null : 221 | node.init.type === 'ExpressionStatement' ? node.init.expression : 222 | node.init, 223 | test: node.test, 224 | update: node.update, 225 | body: node.body 226 | }; 227 | } 228 | } 229 | })); 230 | } 231 | 232 | function denormalize(node) { 233 | return estraverse.replace(node, { 234 | leave: function(node) { 235 | if (node.type === 'Literal') { 236 | if (node.value === void 0) { 237 | return { 238 | type: 'UnaryExpression', 239 | operator: 'void', 240 | argument: { 241 | type: 'Literal', 242 | value: 0 243 | } 244 | }; 245 | } 246 | 247 | if (typeof node.value === 'number') { 248 | if (isNaN(node.value)) { 249 | return { 250 | type: 'BinaryExpression', 251 | operator: '/', 252 | left: { 253 | type: 'Literal', 254 | value: 0 255 | }, 256 | right: { 257 | type: 'Literal', 258 | value: 0 259 | } 260 | } 261 | } 262 | 263 | if (!isFinite(node.value)) { 264 | return { 265 | type: 'BinaryExpression', 266 | operator: '/', 267 | left: node.value < 0 ? { 268 | type: 'UnaryExpression', 269 | operator: '-', 270 | argument: { 271 | type: 'Literal', 272 | value: 1 273 | } 274 | } : { 275 | type: 'Literal', 276 | value: 1 277 | }, 278 | right: { 279 | type: 'Literal', 280 | value: 0 281 | } 282 | } 283 | } 284 | 285 | if (node.value < 0) { 286 | return { 287 | type: 'UnaryExpression', 288 | operator: '-', 289 | argument: { 290 | type: 'Literal', 291 | value: -node.value 292 | } 293 | }; 294 | } 295 | 296 | if (node.value === 0 && 1 / node.value < 0) { 297 | return { 298 | type: 'Literal', 299 | value: 0 300 | }; 301 | } 302 | } 303 | } 304 | 305 | if (node.type === 'Property') { 306 | var key = node.key; 307 | assert(key.type === 'Literal'); 308 | if (isValidIdentifier.test(key.value)) { 309 | key = { 310 | type: 'Identifier', 311 | name: key.value 312 | }; 313 | } 314 | return { 315 | type: 'Property', 316 | key: key, 317 | value: node.value, 318 | kind: node.kind 319 | }; 320 | } 321 | 322 | if (node.type === 'MemberExpression' && node.computed && node.property.type === 'Literal' && isValidIdentifier.test(node.property.value)) { 323 | return { 324 | type: 'MemberExpression', 325 | computed: false, 326 | object: node.object, 327 | property: { 328 | type: 'Identifier', 329 | name: node.property.value 330 | } 331 | }; 332 | } 333 | } 334 | }); 335 | } 336 | 337 | function foldConstants(node) { 338 | return estraverse.replace(node, { 339 | enter: function(node) { 340 | if (node.type === 'UnaryExpression' && node.operator === '!') { 341 | if (node.argument.type === 'BinaryExpression' && node.argument.operator in oppositeOperator) { 342 | return { 343 | type: 'BinaryExpression', 344 | operator: oppositeOperator[node.argument.operator], 345 | left: node.argument.left, 346 | right: node.argument.right 347 | }; 348 | } 349 | 350 | if (node.argument.type === 'LogicalExpression' && node.argument.operator in oppositeOperator) { 351 | return { 352 | type: 'LogicalExpression', 353 | operator: oppositeOperator[node.argument.operator], 354 | left: { 355 | type: 'UnaryExpression', 356 | operator: '!', 357 | argument: node.argument.left 358 | }, 359 | right: { 360 | type: 'UnaryExpression', 361 | operator: '!', 362 | argument: node.argument.right 363 | } 364 | }; 365 | } 366 | } 367 | }, 368 | 369 | leave: function(node) { 370 | if (node.type === 'SequenceExpression') { 371 | var expressions = node.expressions; 372 | 373 | expressions = Array.prototype.concat.apply([], expressions.map(function(node) { 374 | return node.type === 'SequenceExpression' ? node.expressions : node; 375 | })); 376 | 377 | expressions = expressions.slice(0, -1).filter(hasSideEffects).concat(expressions.slice(-1)); 378 | 379 | if (expressions.length > 1) { 380 | return { 381 | type: 'SequenceExpression', 382 | expressions: expressions 383 | }; 384 | } 385 | 386 | return expressions[0]; 387 | } 388 | 389 | if (node.type === 'UnaryExpression' && node.argument.type === 'Literal') { 390 | var operator = new Function('a', 'return ' + node.operator + ' a;'); 391 | return { 392 | type: 'Literal', 393 | value: operator(node.argument.value) 394 | } 395 | } 396 | 397 | if ((node.type === 'BinaryExpression' || node.type === 'LogicalExpression') && node.left.type === 'Literal' && node.right.type === 'Literal') { 398 | var operator = new Function('a', 'b', 'return a ' + node.operator + ' b;'); 399 | return { 400 | type: 'Literal', 401 | value: operator(node.left.value, node.right.value) 402 | }; 403 | } 404 | 405 | if (node.type === 'ConditionalExpression' && node.test.type === 'Literal') { 406 | return node.test.value ? node.consequent : node.alternate; 407 | } 408 | 409 | if (node.type === 'MemberExpression' && node.property.type === 'Literal' && !hasSideEffects(node.object)) { 410 | assert(node.computed); 411 | 412 | if (node.object.type === 'ObjectExpression') { 413 | for (var i = 0; i < node.object.properties.length; i++) { 414 | var property = node.object.properties[i]; 415 | assert(property.key.type === 'Literal' && typeof property.key.value === 'string'); 416 | if (property.key.value === node.property.value + '') { 417 | return property.value; 418 | } 419 | } 420 | } 421 | 422 | if (node.object.type === 'Literal' && typeof node.object.value === 'string') { 423 | if (node.property.value === 'length') { 424 | return { 425 | type: 'Literal', 426 | value: node.object.value.length 427 | }; 428 | } 429 | 430 | if (typeof node.property.value === 'number') { 431 | // Check for a match inside the string literal 432 | var index = node.property.value >>> 0; 433 | if (index === +node.property.value && index < node.object.value.length) { 434 | return { 435 | type: 'Literal', 436 | value: node.object.value[index] 437 | }; 438 | } 439 | 440 | // Optimize to an empty string literal (may still be a numeric property on String.prototype) 441 | return { 442 | type: 'MemberExpression', 443 | computed: true, 444 | object: { 445 | type: 'Literal', 446 | value: '' 447 | }, 448 | property: node.property 449 | } 450 | } 451 | } 452 | 453 | if (node.object.type === 'ArrayExpression') { 454 | if (node.property.value === 'length') { 455 | return { 456 | type: 'Literal', 457 | value: node.object.elements.length 458 | }; 459 | } 460 | 461 | if (typeof node.property.value === 'number') { 462 | // Check for a match inside the array literal 463 | var index = node.property.value >>> 0; 464 | if (index === +node.property.value && index < node.object.elements.length) { 465 | return node.object.elements[index]; 466 | } 467 | 468 | // Optimize to an empty array literal (may still be a numeric property on Array.prototype) 469 | return { 470 | type: 'MemberExpression', 471 | computed: true, 472 | object: { 473 | type: 'ArrayExpression', 474 | elements: [] 475 | }, 476 | property: node.property 477 | } 478 | } 479 | } 480 | } 481 | } 482 | }); 483 | } 484 | 485 | function filterDeadCode(nodes) { 486 | return nodes.filter(function(node) { 487 | if (node.type === 'EmptyStatement') { 488 | return false; 489 | } 490 | 491 | if (node.type === 'VariableDeclaration' && node.declarations.length === 0) { 492 | return false; 493 | } 494 | 495 | // Users won't like it if we remove 'use strict' directives 496 | if (node.type === 'ExpressionStatement' && !hasSideEffects(node.expression) && 497 | (node.expression.type !== 'Literal' || node.expression.value !== 'use strict')) { 498 | return false; 499 | } 500 | 501 | return true; 502 | }); 503 | } 504 | 505 | function flattenNodeList(nodes) { 506 | return Array.prototype.concat.apply([], nodes.map(function(node) { 507 | if (node.type === 'BlockStatement') { 508 | return node.body; 509 | } 510 | 511 | if (node.type === 'ExpressionStatement' && node.expression.type === 'SequenceExpression') { 512 | return flattenNodeList(node.expression.expressions).map(function(node) { 513 | return { 514 | type: 'ExpressionStatement', 515 | expression: node 516 | } 517 | }); 518 | } 519 | 520 | if (node.type === 'SequenceExpression') { 521 | return flattenNodeList(node.expressions); 522 | } 523 | 524 | return node; 525 | })); 526 | } 527 | 528 | function hoistUseStrict(nodes) { 529 | var useStrict = false; 530 | 531 | nodes = nodes.filter(function(node) { 532 | if (node.type === 'ExpressionStatement' && node.expression.type === 'Literal' && node.expression.value === 'use strict') { 533 | useStrict = true; 534 | return false; 535 | } 536 | return true; 537 | }); 538 | 539 | if (useStrict) { 540 | return [{ 541 | type: 'ExpressionStatement', 542 | expression: { 543 | type: 'Literal', 544 | value: 'use strict' 545 | } 546 | }].concat(nodes); 547 | } 548 | 549 | return nodes; 550 | } 551 | 552 | function canRemoveVariable(name) { 553 | var variable = scope.variableForName(name); 554 | assert(variable !== null); 555 | return !variable.isGlobal() && !variable.isArgument() && (!variable.isReadFrom() || !variable.isWrittenTo()); 556 | } 557 | 558 | function removeDeadCode(node) { 559 | return estraverse.replace(node, wrapVisitorScope(wrapVisitorParent({ 560 | leave: function(node) { 561 | if (node.type === 'Program') { 562 | return { 563 | type: 'Program', 564 | body: hoistUseStrict(flattenNodeList(filterDeadCode(node.body))) 565 | }; 566 | } 567 | 568 | if (node.type === 'BlockStatement') { 569 | var body = hoistUseStrict(flattenNodeList(filterDeadCode(node.body))); 570 | 571 | if (parent === null || (parent.type !== 'FunctionExpression' && parent.type !== 'FunctionDeclaration' && 572 | parent.type !== 'TryStatement' && parent.type !== 'CatchClause')) { 573 | if (body.length === 0) { 574 | return { 575 | type: 'EmptyStatement' 576 | }; 577 | } 578 | 579 | if (body.length === 1) { 580 | return body[0]; 581 | } 582 | } 583 | 584 | return { 585 | type: 'BlockStatement', 586 | body: body 587 | }; 588 | } 589 | 590 | if (node.type === 'ForStatement') { 591 | if (node.test !== null && node.test.type === 'Literal' && !node.test.value) { 592 | if (node.init === null) { 593 | return { 594 | type: 'EmptyStatement' 595 | }; 596 | } 597 | 598 | if (node.init.type === 'VariableDeclaration') { 599 | return node.init; 600 | } 601 | 602 | return { 603 | type: 'ExpressionStatement', 604 | expression: node.init 605 | }; 606 | } 607 | } 608 | 609 | if (node.type === 'TryStatement') { 610 | var handler = node.handlers.length === 1 ? node.handlers[0] : null; 611 | var finalizer = node.finalizer; 612 | assert(node.handlers.length < 2); 613 | 614 | if (node.block.body.length === 0) { 615 | return { 616 | type: 'EmptyStatement' 617 | }; 618 | } 619 | 620 | if (handler !== null && handler.body.body.length === 0) { 621 | handler = null; 622 | } 623 | 624 | if (finalizer !== null && finalizer.body.length === 0) { 625 | finalizer = null; 626 | } 627 | 628 | if (handler === null && finalizer === null) { 629 | return node.block; 630 | } 631 | 632 | return { 633 | type: 'TryStatement', 634 | block: node.block, 635 | guardedHandlers: [], 636 | handlers: handler !== null ? [handler] : [], 637 | finalizer: finalizer 638 | }; 639 | } 640 | 641 | if (node.type === 'SwitchStatement' && (!node.cases || node.cases.length === 0)) { 642 | return { 643 | type: 'ExpressionStatement', 644 | expression: node.discriminant 645 | }; 646 | } 647 | 648 | if (node.type === 'ReturnStatement' && node.argument !== null && node.argument.type === 'Literal' && node.argument.value === void 0) { 649 | return { 650 | type: 'ReturnStatement', 651 | argument: null 652 | }; 653 | } 654 | 655 | if (node.type === 'WhileStatement') { 656 | if (node.test.type === 'Literal' && !node.test.value) { 657 | return { 658 | type: 'EmptyStatement' 659 | }; 660 | } 661 | } 662 | 663 | if (node.type === 'DoWhileStatement') { 664 | if (node.test.type === 'Literal' && !node.test.value) { 665 | return node.body; 666 | } 667 | } 668 | 669 | if (node.type === 'WithStatement') { 670 | if (node.body.type === 'EmptyStatement') { 671 | return { 672 | type: 'ExpressionStatement', 673 | expression: node.object 674 | }; 675 | } 676 | } 677 | 678 | if (node.type === 'IfStatement') { 679 | if (node.test.type === 'Literal') { 680 | return node.test.value ? node.consequent : node.alternate || { 681 | type: 'EmptyStatement' 682 | }; 683 | } 684 | 685 | if (node.consequent.type === 'EmptyStatement' && (node.alternate === null || node.alternate.type === 'EmptyStatement')) { 686 | return { 687 | type: 'ExpressionStatement', 688 | expression: node.test 689 | }; 690 | } 691 | 692 | if (node.alternate !== null && node.alternate.type === 'EmptyStatement') { 693 | return { 694 | type: 'IfStatement', 695 | test: node.test, 696 | consequent: node.consequent, 697 | alternate: null 698 | }; 699 | } 700 | } 701 | 702 | if (node.type === 'Identifier' && canRemoveVariable(node.name)) { 703 | var variable = scope.variableForName(node.name); 704 | assert(variable !== null); 705 | if (variable.referenceForNode(node).isRead) { 706 | return { 707 | type: 'Literal', 708 | value: void 0 709 | }; 710 | } 711 | } 712 | 713 | if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier' && canRemoveVariable(node.left.name)) { 714 | return node.right; 715 | } 716 | 717 | if (node.type === 'FunctionDeclaration' && canRemoveVariable(node.id.name)) { 718 | return { 719 | type: 'EmptyStatement' 720 | }; 721 | } 722 | 723 | if (node.type === 'VariableDeclaration') { 724 | return { 725 | type: 'VariableDeclaration', 726 | declarations: node.declarations.filter(function(node) { 727 | return !canRemoveVariable(node.id.name); 728 | }), 729 | kind: node.kind 730 | }; 731 | } 732 | } 733 | }))); 734 | } 735 | 736 | function wrapVisitorParent(visitor) { 737 | var parentStack = []; 738 | return { 739 | enter: function(node) { 740 | if (visitor.enter) node = visitor.enter(node) || node; 741 | parentStack.push(parent); 742 | parent = node; 743 | return node; 744 | }, 745 | 746 | leave: function(node) { 747 | parent = parentStack.pop(); 748 | if (visitor.leave) node = visitor.leave(node) || node; 749 | return node; 750 | } 751 | }; 752 | } 753 | 754 | function wrapVisitorScope(visitor) { 755 | scope = null; 756 | return { 757 | enter: function(node) { 758 | if (esscope.nodeStartsNewScope(node)) { 759 | scope = scope === null ? esscope.analyze(node) : scope.childScopeForNode(node); 760 | assert(scope !== null); 761 | } 762 | if (visitor.enter) node = visitor.enter(node) || node; 763 | return node; 764 | }, 765 | 766 | leave: function(node) { 767 | if (visitor.leave) node = visitor.leave(node) || node; 768 | if (esscope.nodeStartsNewScope(node)) scope = scope.parentScope; 769 | return node; 770 | } 771 | }; 772 | } 773 | 774 | function optimize(node) { 775 | node = normalize(node); 776 | node = foldConstants(node); 777 | node = removeDeadCode(node); 778 | node = denormalize(node); 779 | return node; 780 | } 781 | 782 | esoptimize.optimize = optimize; 783 | 784 | }.call(this)); 785 | -------------------------------------------------------------------------------- /esscope.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var estraverse = typeof window !== 'undefined' ? window.estraverse : require('estraverse'); 5 | var esscope = typeof window !== 'undefined' ? (window.esscope = {}) : exports; 6 | 7 | function assert(truth) { 8 | if (!truth) { 9 | throw new Error('assertion failed'); 10 | } 11 | } 12 | 13 | function Variable(name, scope) { 14 | this.name = name; 15 | this.scope = scope; 16 | this.references = []; 17 | } 18 | 19 | Variable.prototype.referenceForNode = function(node) { 20 | for (var i = 0; i < this.references.length; i++) { 21 | if (this.references[i].node === node) return this.references[i]; 22 | } 23 | return null; 24 | }; 25 | 26 | Variable.prototype.isGlobal = function() { 27 | return this.scope.parentScope === null; 28 | }; 29 | 30 | Variable.prototype.isVariable = function() { 31 | return this.references.some(function(reference) { 32 | var decl = reference.declarationNode; 33 | return decl !== null && decl.type === 'VariableDeclarator'; 34 | }); 35 | }; 36 | 37 | Variable.prototype.isArgument = function() { 38 | return this.references.some(function(reference) { 39 | var decl = reference.declarationNode; 40 | return decl !== null && (decl.type === 'FunctionExpression' || 41 | decl.type === 'FunctionDeclaration') && decl.params.indexOf(reference.node) >= 0; 42 | }); 43 | }; 44 | 45 | Variable.prototype.isCaptured = function() { 46 | return this.references.some(function(reference) { 47 | return reference.scope !== this.scope; 48 | }, this); 49 | }; 50 | 51 | Variable.prototype.isReadFrom = function() { 52 | return this.references.some(function(reference) { 53 | return reference.isRead; 54 | }); 55 | }; 56 | 57 | Variable.prototype.isWrittenTo = function() { 58 | return this.references.some(function(reference) { 59 | return reference.isWrite; 60 | }); 61 | }; 62 | 63 | function Reference(node, scope) { 64 | this.node = node; 65 | this.scope = scope; 66 | this.variable = null; 67 | this.isRead = false; 68 | this.isWrite = false; 69 | this.declarationNode = null; 70 | } 71 | 72 | function Scope(parentScope, node) { 73 | this.parentScope = parentScope; 74 | this.childScopes = []; 75 | this.node = node; 76 | this.variables = []; 77 | } 78 | 79 | function variableForName(scope, name) { 80 | for (var i = 0; i < scope.variables.length; i++) { 81 | if (scope.variables[i].name === name) return scope.variables[i]; 82 | } 83 | return null; 84 | } 85 | 86 | Scope.prototype.childScopeForNode = function(node) { 87 | for (var i = 0; i < this.childScopes.length; i++) { 88 | if (this.childScopes[i].node === node) { 89 | return this.childScopes[i]; 90 | } 91 | } 92 | return null; 93 | }; 94 | 95 | Scope.prototype.variableForName = function(name) { 96 | for (var scope = this; scope !== null; scope = scope.parentScope) { 97 | var variable = variableForName(scope, name); 98 | if (variable !== null) return variable; 99 | } 100 | return null; 101 | }; 102 | 103 | Scope.prototype.define = function(name) { 104 | var variable = variableForName(this, name); 105 | if (variable === null) { 106 | variable = new Variable(name, this); 107 | this.variables.push(variable); 108 | } 109 | return variable; 110 | }; 111 | 112 | function Resolver(parentResolver, node) { 113 | var parentScope = parentResolver !== null ? parentResolver.scope : null; 114 | this.parentResolver = parentResolver; 115 | this.scope = new Scope(parentScope, node); 116 | this.references = []; 117 | if (parentScope !== null) parentScope.childScopes.push(this.scope); 118 | } 119 | 120 | Resolver.prototype.close = function() { 121 | var globalScope = this.scope; 122 | while (globalScope.parentScope !== null) { 123 | globalScope = globalScope.parentScope; 124 | } 125 | for (var i = 0; i < this.references.length; i++) { 126 | var reference = this.references[i]; 127 | reference.variable = this.scope.variableForName(reference.node.name); 128 | if (reference.variable === null) reference.variable = globalScope.define(reference.node.name); 129 | reference.variable.references.push(reference); 130 | } 131 | }; 132 | 133 | Resolver.prototype.recordReference = function(node) { 134 | var reference = new Reference(node, this.scope); 135 | this.references.push(reference); 136 | return reference; 137 | }; 138 | 139 | function nodeStartsNewScope(node) { 140 | return node.type === 'Program' || node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration'; 141 | } 142 | 143 | function analyze(node) { 144 | var currentResolver = null; 145 | var parentStack = []; 146 | var parent = null; 147 | 148 | function enter(node) { 149 | if (node.type === 'VariableDeclarator' || node.type === 'FunctionDeclaration') { 150 | currentResolver.scope.define(node.id.name); 151 | } 152 | } 153 | 154 | function leave(node) { 155 | if (node.type === 'Identifier') { 156 | var reference = currentResolver.recordReference(node); 157 | 158 | if (parent.type === 'VariableDeclarator') { 159 | reference.isWrite = parent.init !== null; 160 | reference.declarationNode = parent; 161 | } 162 | 163 | else if (parent.type === 'FunctionExpression' || parent.type === 'FunctionDeclaration') { 164 | reference.isWrite = true; 165 | reference.declarationNode = parent; 166 | } 167 | 168 | else if (parent.type === 'AssignmentExpression' && parent.left === node) { 169 | reference.isWrite = true; 170 | 171 | if (parent.operator !== '=') { 172 | reference.isRead = true; 173 | } 174 | } 175 | 176 | else { 177 | reference.isRead = true; 178 | } 179 | } 180 | 181 | if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') { 182 | for (var i = 0; i < node.params.length; i++) { 183 | currentResolver.scope.define(node.params[i].name); 184 | } 185 | } 186 | } 187 | 188 | estraverse.traverse(node, { 189 | enter: function(node) { 190 | enter(node); 191 | if (nodeStartsNewScope(node)) { 192 | currentResolver = new Resolver(currentResolver, node); 193 | } 194 | parentStack.push(parent); 195 | parent = node; 196 | }, 197 | 198 | leave: function(node) { 199 | parent = parentStack.pop(); 200 | leave(node); 201 | if (nodeStartsNewScope(node)) { 202 | currentResolver.close(); 203 | if (currentResolver.parentResolver !== null) { 204 | currentResolver = currentResolver.parentResolver; 205 | } 206 | } 207 | } 208 | }); 209 | 210 | return currentResolver.scope; 211 | } 212 | 213 | esscope.Variable = Variable; 214 | esscope.Reference = Reference; 215 | esscope.Scope = Scope; 216 | esscope.nodeStartsNewScope = nodeStartsNewScope; 217 | esscope.analyze = analyze; 218 | 219 | }.call(this)); 220 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esoptimize", 3 | "description": "A JavaScript AST optimizer", 4 | "main": "esoptimize.js", 5 | "version": "0.0.1", 6 | "repository": { 7 | "type": "git", 8 | "url": "http://github.com/evanw/esoptimize.git" 9 | }, 10 | "dependencies": { 11 | "estraverse": "1.1.1" 12 | }, 13 | "devDependencies": { 14 | "mocha": "1.9.0", 15 | "esprima": "1.0.2", 16 | "escodegen": "0.0.21", 17 | "codemirror": "3.11.01" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var esprima = require('esprima'); 3 | var escodegen = require('escodegen'); 4 | var esoptimize = require('./esoptimize'); 5 | 6 | function bodyOfFunction(f) { 7 | return f.toString().replace(/^[^\{]*\{((?:.|\n)*)\}[^\}]*$/, '$1'); 8 | } 9 | 10 | function test(input, expected) { 11 | input = esprima.parse(bodyOfFunction(input)); 12 | expected = esprima.parse(bodyOfFunction(expected)); 13 | var output = esoptimize.optimize(input); 14 | var options = { format: { indent: { style: ' ' } } }; 15 | assert.strictEqual(escodegen.generate(output, options), escodegen.generate(expected, options)); 16 | assert.strictEqual(JSON.stringify(output, null, 2), JSON.stringify(expected, null, 2)); 17 | } 18 | 19 | it('numeric constants', function() { 20 | test(function() { 21 | var a = 0 / 0 * 2; 22 | var b = 100 / 0; 23 | var c = 100 / 0 * -2; 24 | var d = -0; 25 | }, function() { 26 | var a, b, c, d; 27 | a = 0 / 0; 28 | b = 1 / 0; 29 | c = -1 / 0; 30 | d = 0; 31 | }); 32 | }); 33 | 34 | it('unary operators', function() { 35 | test(function() { 36 | a( 37 | !1, 38 | ~1, 39 | +1, 40 | -1, 41 | void 1, 42 | typeof 1, 43 | delete b 44 | ); 45 | b++; 46 | b--; 47 | ++b; 48 | --b; 49 | }, function() { 50 | a( 51 | false, 52 | -2, 53 | 1, 54 | -1, 55 | void 0, 56 | 'number', 57 | delete b 58 | ); 59 | b++; 60 | b--; 61 | ++b; 62 | --b; 63 | }); 64 | }); 65 | 66 | it('binary operators', function() { 67 | test(function() { 68 | a( 69 | 1 + 2, 70 | 1 - 2, 71 | 1 * 2, 72 | 1 / 2, 73 | 1 % 2, 74 | 1 & 2, 75 | 1 | 2, 76 | 1 ^ 2, 77 | 1 << 2, 78 | 1 >> 2, 79 | 1 >>> 2, 80 | 1 < 2, 81 | 1 > 2, 82 | 1 <= 2, 83 | 1 >= 2, 84 | 1 == 2, 85 | 1 != 2, 86 | 1 === 2, 87 | 1 !== 2, 88 | 0 && 1, 89 | 0 || 1, 90 | 1 instanceof b, 91 | 1 in b 92 | ); 93 | b = 2; 94 | b += 2; 95 | b -= 2; 96 | b *= 2; 97 | b /= 2; 98 | b %= 2; 99 | b &= 2; 100 | b |= 2; 101 | b ^= 2; 102 | b <<= 2; 103 | b >>= 2; 104 | b >>>= 2; 105 | }, function() { 106 | a( 107 | 3, 108 | -1, 109 | 2, 110 | 0.5, 111 | 1, 112 | 0, 113 | 3, 114 | 3, 115 | 4, 116 | 0, 117 | 0, 118 | true, 119 | false, 120 | true, 121 | false, 122 | false, 123 | true, 124 | false, 125 | true, 126 | 0, 127 | 1, 128 | 1 instanceof b, 129 | 1 in b 130 | ); 131 | b = 2; 132 | b += 2; 133 | b -= 2; 134 | b *= 2; 135 | b /= 2; 136 | b %= 2; 137 | b &= 2; 138 | b |= 2; 139 | b ^= 2; 140 | b <<= 2; 141 | b >>= 2; 142 | b >>>= 2; 143 | }); 144 | }); 145 | 146 | it('sequence folding', function() { 147 | test(function() { 148 | var a = (1, 2, 3); 149 | var b = (1, x(), 2, y(), 3); 150 | var c = (1, (x(), 2), 3); 151 | }, function() { 152 | var a, b, c; 153 | a = 3; 154 | b = (x(), y(), 3); 155 | c = (x(), 3); 156 | }); 157 | }); 158 | 159 | it('logical negation', function() { 160 | test(function() { 161 | a(!(b < c)); 162 | a(!(b > c)); 163 | a(!(b <= c)); 164 | a(!(b >= c)); 165 | a(!(b == c)); 166 | a(!(b != c)); 167 | a(!(b === c)); 168 | a(!(b !== c)); 169 | a(!(b && c)); 170 | a(!(b || c)); 171 | a(!(b < c && d || e > f)); 172 | a(!(b < c || d && e > f)); 173 | }, function() { 174 | a(b >= c); 175 | a(b <= c); 176 | a(b > c); 177 | a(b < c); 178 | a(b != c); 179 | a(b == c); 180 | a(b !== c); 181 | a(b === c); 182 | a(!b || !c); 183 | a(!b && !c); 184 | a((b >= c || !d) && e <= f); 185 | a(b >= c && (!d || e <= f)); 186 | }); 187 | }); 188 | 189 | it('array folding', function() { 190 | test(function() { 191 | var a = [1, 2][0]; 192 | var b = [1, c()][0]; 193 | var c = [1, 2][-1]; 194 | var d = [1, 2][0.5]; 195 | var e = [1, 2, 3]['len' + 'gth']; 196 | }, function() { 197 | var a, b, c, d, e; 198 | a = 1; 199 | b = [1, c()][0]; 200 | c = [][-1]; 201 | d = [][0.5]; 202 | e = 3; 203 | }); 204 | }); 205 | 206 | it('string folding', function() { 207 | test(function() { 208 | var a = '12'[0]; 209 | var b = '12'[-1]; 210 | var c = '12'[0.5]; 211 | var d = '123'['len' + 'gth']; 212 | }, function() { 213 | var a, b, c, d; 214 | a = '1'; 215 | b = ''[-1]; 216 | c = ''[0.5]; 217 | d = 3; 218 | }); 219 | }); 220 | 221 | it('object literal folding', function() { 222 | test(function() { 223 | var a = { 'x': 0, 'y': 1 }['x']; 224 | var b = { 'x': 0, 'y': 1 }.x; 225 | var c = { 1: 2, 3: 4 }[1]; 226 | }, function() { 227 | var a, b, c; 228 | a = 0; 229 | b = 0; 230 | c = 2; 231 | }); 232 | }); 233 | 234 | it('property normalization', function() { 235 | test(function() { 236 | a(b['c']); 237 | a(b['c d']); 238 | a({ 1: 2, 'b': 'c' }); 239 | }, function() { 240 | a(b.c); 241 | a(b['c d']); 242 | a({ '1': 2, b: 'c' }); 243 | }); 244 | }); 245 | 246 | it('side-effect-free code removal', function() { 247 | test(function() { 248 | 'use strict'; 249 | if (false) var x; 250 | 'not use strict'; 251 | 1; 252 | x; 253 | x.y; 254 | (function() {}); 255 | var foo = function() {}; 256 | function foo() {} 257 | }, function() { 258 | 'use strict'; 259 | var x, foo; 260 | foo = function() {}; 261 | function foo() {} 262 | }); 263 | }); 264 | 265 | it('block flattening', function() { 266 | test(function() { 267 | a(); 268 | ; 269 | { ; b(); { c(); } d(), e(); } 270 | f(); 271 | }, function() { 272 | a(); 273 | b(); 274 | c(); 275 | d(); 276 | e(); 277 | f(); 278 | }); 279 | }); 280 | 281 | it('function block flattening', function() { 282 | test(function() { 283 | function foo(a, b) { 284 | a(); 285 | ; 286 | { ; b(); { c(); } d(), e(); } 287 | f(); 288 | } 289 | }, function() { 290 | function foo(a, b) { 291 | a(); 292 | b(); 293 | c(); 294 | d(); 295 | e(); 296 | f(); 297 | } 298 | }); 299 | }); 300 | 301 | it('unused variable removal', function() { 302 | test(function() { 303 | var a, b; 304 | function foo(a) { 305 | var a, b, c; 306 | a(); 307 | b(); 308 | } 309 | (function(a) { 310 | function foo() { 311 | var b, c; 312 | a(); 313 | b(); 314 | } 315 | function bar() {} 316 | foo(); 317 | }()); 318 | b = 0; 319 | }, function() { 320 | var a, b; 321 | function foo(a) { 322 | a(); 323 | (void 0)(); 324 | } 325 | (function(a) { 326 | function foo() { 327 | a(); 328 | (void 0)(); 329 | } 330 | foo(); 331 | }()); 332 | b = 0; 333 | }); 334 | }); 335 | 336 | it('ternary expression folding', function() { 337 | test(function() { 338 | a(true ? b() : c()); 339 | }, function() { 340 | a(b()); 341 | }); 342 | }); 343 | 344 | it('if statement dead code removal', function() { 345 | test(function() { 346 | if (0) a(); 347 | if (0) a(); else b(); 348 | if (1) a(); else b(); 349 | if (a()) { b(); } else {} 350 | if (a()) { 1; } 351 | if (a()) { 1; } else { 2; } 352 | }, function() { 353 | b(); 354 | a(); 355 | if (a()) b(); 356 | a(); 357 | a(); 358 | }); 359 | }); 360 | 361 | it('while statement dead code removal', function() { 362 | test(function() { 363 | while (false) foo(); 364 | while (true) foo(); 365 | }, function() { 366 | while (true) foo(); 367 | }); 368 | }); 369 | 370 | it('do-while statement dead code removal', function() { 371 | test(function() { 372 | do foo(); while (false); 373 | do foo(); while (true); 374 | }, function() { 375 | foo(); 376 | do foo(); while (true); 377 | }); 378 | }); 379 | 380 | it('for statement dead code removal', function() { 381 | test(function() { 382 | for (;0;) foo(); 383 | for (foo();0;) foo(); 384 | for (var bar;0;) foo(); 385 | for (var bar = 0;0;) foo(); 386 | for (;1;) foo(); 387 | for (;;) foo(); 388 | }, function() { 389 | var bar; 390 | foo(); 391 | bar = 0; 392 | for (;1;) foo(); 393 | for (;;) foo(); 394 | }); 395 | }); 396 | 397 | it('with statement dead code removal', function() { 398 | test(function() { 399 | with (foo) {} 400 | with (foo) foo(); 401 | }, function() { 402 | with (foo) foo(); 403 | }); 404 | }); 405 | 406 | it('try statement dead code removal', function() { 407 | test(function() { 408 | try { foo(); } catch (e) { foo(); } finally { foo(); } 409 | try { foo(); } catch (e) { foo(); } finally {} 410 | try { foo(); } catch (e) { foo(); } 411 | try { foo(); } catch (e) {} finally { foo(); } 412 | try { foo(); } catch (e) {} finally {} 413 | try { foo(); } catch (e) {} 414 | try { foo(); } finally { foo(); } 415 | try { foo(); } finally {} 416 | try {} catch (e) { foo(); } finally { foo(); } 417 | try {} catch (e) { foo(); } finally {} 418 | try {} catch (e) { foo(); } 419 | try {} catch (e) {} finally { foo(); } 420 | try {} catch (e) {} finally {} 421 | try {} catch (e) {} 422 | try {} finally { foo(); } 423 | try {} finally {} 424 | }, function() { 425 | try { foo(); } catch (e) { foo(); } finally { foo(); } 426 | try { foo(); } catch (e) { foo(); } 427 | try { foo(); } catch (e) { foo(); } 428 | try { foo(); } finally { foo(); } 429 | foo(); 430 | foo(); 431 | try { foo(); } finally { foo(); } 432 | foo(); 433 | }); 434 | }); 435 | 436 | it('return statement folding', function() { 437 | test(function() { 438 | function foo() { if (bar()) return void 0; bar(); } 439 | function foo() { var x; if (bar()) return x; bar(); } 440 | }, function() { 441 | function foo() { if (bar()) return; bar(); } 442 | function foo() { if (bar()) return; bar(); } 443 | }); 444 | }); 445 | 446 | it('switch statement dead code removal', function() { 447 | test(function() { 448 | switch (0) {} 449 | switch (foo()) {} 450 | }, function() { 451 | foo(); 452 | }); 453 | }); 454 | --------------------------------------------------------------------------------