estraverse.js
1 /* 2 Copyright (C) 2012-2013 Yusuke Suzuki <utatane.tea@gmail.com> 3 Copyright (C) 2012 Ariya Hidayat <ariya.hidayat@gmail.com> 4 5 Redistribution and use in source and binary forms, with or without 6 modification, are permitted provided that the following conditions are met: 7 8 * Redistributions of source code must retain the above copyright 9 notice, this list of conditions and the following disclaimer. 10 * Redistributions in binary form must reproduce the above copyright 11 notice, this list of conditions and the following disclaimer in the 12 documentation and/or other materials provided with the distribution. 13 14 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 17 ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY 18 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 23 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 */ 25 /*jslint vars:false, bitwise:true*/ 26 /*jshint indent:4*/ 27 /*global exports:true*/ 28 (function clone(exports) { 29 'use strict'; 30 31 var Syntax, 32 VisitorOption, 33 VisitorKeys, 34 BREAK, 35 SKIP, 36 REMOVE; 37 38 function deepCopy(obj) { 39 var ret = {}, key, val; 40 for (key in obj) { 41 if (obj.hasOwnProperty(key)) { 42 val = obj[key]; 43 if (typeof val === 'object' && val !== null) { 44 ret[key] = deepCopy(val); 45 } else { 46 ret[key] = val; 47 } 48 } 49 } 50 return ret; 51 } 52 53 // based on LLVM libc++ upper_bound / lower_bound 54 // MIT License 55 56 function upperBound(array, func) { 57 var diff, len, i, current; 58 59 len = array.length; 60 i = 0; 61 62 while (len) { 63 diff = len >>> 1; 64 current = i + diff; 65 if (func(array[current])) { 66 len = diff; 67 } else { 68 i = current + 1; 69 len -= diff + 1; 70 } 71 } 72 return i; 73 } 74 75 Syntax = { 76 AssignmentExpression: 'AssignmentExpression', 77 AssignmentPattern: 'AssignmentPattern', 78 ArrayExpression: 'ArrayExpression', 79 ArrayPattern: 'ArrayPattern', 80 ArrowFunctionExpression: 'ArrowFunctionExpression', 81 AwaitExpression: 'AwaitExpression', // CAUTION: It's deferred to ES7. 82 BlockStatement: 'BlockStatement', 83 BinaryExpression: 'BinaryExpression', 84 BreakStatement: 'BreakStatement', 85 CallExpression: 'CallExpression', 86 CatchClause: 'CatchClause', 87 ChainExpression: 'ChainExpression', 88 ClassBody: 'ClassBody', 89 ClassDeclaration: 'ClassDeclaration', 90 ClassExpression: 'ClassExpression', 91 ComprehensionBlock: 'ComprehensionBlock', // CAUTION: It's deferred to ES7. 92 ComprehensionExpression: 'ComprehensionExpression', // CAUTION: It's deferred to ES7. 93 ConditionalExpression: 'ConditionalExpression', 94 ContinueStatement: 'ContinueStatement', 95 DebuggerStatement: 'DebuggerStatement', 96 DirectiveStatement: 'DirectiveStatement', 97 DoWhileStatement: 'DoWhileStatement', 98 EmptyStatement: 'EmptyStatement', 99 ExportAllDeclaration: 'ExportAllDeclaration', 100 ExportDefaultDeclaration: 'ExportDefaultDeclaration', 101 ExportNamedDeclaration: 'ExportNamedDeclaration', 102 ExportSpecifier: 'ExportSpecifier', 103 ExpressionStatement: 'ExpressionStatement', 104 ForStatement: 'ForStatement', 105 ForInStatement: 'ForInStatement', 106 ForOfStatement: 'ForOfStatement', 107 FunctionDeclaration: 'FunctionDeclaration', 108 FunctionExpression: 'FunctionExpression', 109 GeneratorExpression: 'GeneratorExpression', // CAUTION: It's deferred to ES7. 110 Identifier: 'Identifier', 111 IfStatement: 'IfStatement', 112 ImportExpression: 'ImportExpression', 113 ImportDeclaration: 'ImportDeclaration', 114 ImportDefaultSpecifier: 'ImportDefaultSpecifier', 115 ImportNamespaceSpecifier: 'ImportNamespaceSpecifier', 116 ImportSpecifier: 'ImportSpecifier', 117 Literal: 'Literal', 118 LabeledStatement: 'LabeledStatement', 119 LogicalExpression: 'LogicalExpression', 120 MemberExpression: 'MemberExpression', 121 MetaProperty: 'MetaProperty', 122 MethodDefinition: 'MethodDefinition', 123 ModuleSpecifier: 'ModuleSpecifier', 124 NewExpression: 'NewExpression', 125 ObjectExpression: 'ObjectExpression', 126 ObjectPattern: 'ObjectPattern', 127 PrivateIdentifier: 'PrivateIdentifier', 128 Program: 'Program', 129 Property: 'Property', 130 PropertyDefinition: 'PropertyDefinition', 131 RestElement: 'RestElement', 132 ReturnStatement: 'ReturnStatement', 133 SequenceExpression: 'SequenceExpression', 134 SpreadElement: 'SpreadElement', 135 Super: 'Super', 136 SwitchStatement: 'SwitchStatement', 137 SwitchCase: 'SwitchCase', 138 TaggedTemplateExpression: 'TaggedTemplateExpression', 139 TemplateElement: 'TemplateElement', 140 TemplateLiteral: 'TemplateLiteral', 141 ThisExpression: 'ThisExpression', 142 ThrowStatement: 'ThrowStatement', 143 TryStatement: 'TryStatement', 144 UnaryExpression: 'UnaryExpression', 145 UpdateExpression: 'UpdateExpression', 146 VariableDeclaration: 'VariableDeclaration', 147 VariableDeclarator: 'VariableDeclarator', 148 WhileStatement: 'WhileStatement', 149 WithStatement: 'WithStatement', 150 YieldExpression: 'YieldExpression' 151 }; 152 153 VisitorKeys = { 154 AssignmentExpression: ['left', 'right'], 155 AssignmentPattern: ['left', 'right'], 156 ArrayExpression: ['elements'], 157 ArrayPattern: ['elements'], 158 ArrowFunctionExpression: ['params', 'body'], 159 AwaitExpression: ['argument'], // CAUTION: It's deferred to ES7. 160 BlockStatement: ['body'], 161 BinaryExpression: ['left', 'right'], 162 BreakStatement: ['label'], 163 CallExpression: ['callee', 'arguments'], 164 CatchClause: ['param', 'body'], 165 ChainExpression: ['expression'], 166 ClassBody: ['body'], 167 ClassDeclaration: ['id', 'superClass', 'body'], 168 ClassExpression: ['id', 'superClass', 'body'], 169 ComprehensionBlock: ['left', 'right'], // CAUTION: It's deferred to ES7. 170 ComprehensionExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. 171 ConditionalExpression: ['test', 'consequent', 'alternate'], 172 ContinueStatement: ['label'], 173 DebuggerStatement: [], 174 DirectiveStatement: [], 175 DoWhileStatement: ['body', 'test'], 176 EmptyStatement: [], 177 ExportAllDeclaration: ['source'], 178 ExportDefaultDeclaration: ['declaration'], 179 ExportNamedDeclaration: ['declaration', 'specifiers', 'source'], 180 ExportSpecifier: ['exported', 'local'], 181 ExpressionStatement: ['expression'], 182 ForStatement: ['init', 'test', 'update', 'body'], 183 ForInStatement: ['left', 'right', 'body'], 184 ForOfStatement: ['left', 'right', 'body'], 185 FunctionDeclaration: ['id', 'params', 'body'], 186 FunctionExpression: ['id', 'params', 'body'], 187 GeneratorExpression: ['blocks', 'filter', 'body'], // CAUTION: It's deferred to ES7. 188 Identifier: [], 189 IfStatement: ['test', 'consequent', 'alternate'], 190 ImportExpression: ['source'], 191 ImportDeclaration: ['specifiers', 'source'], 192 ImportDefaultSpecifier: ['local'], 193 ImportNamespaceSpecifier: ['local'], 194 ImportSpecifier: ['imported', 'local'], 195 Literal: [], 196 LabeledStatement: ['label', 'body'], 197 LogicalExpression: ['left', 'right'], 198 MemberExpression: ['object', 'property'], 199 MetaProperty: ['meta', 'property'], 200 MethodDefinition: ['key', 'value'], 201 ModuleSpecifier: [], 202 NewExpression: ['callee', 'arguments'], 203 ObjectExpression: ['properties'], 204 ObjectPattern: ['properties'], 205 PrivateIdentifier: [], 206 Program: ['body'], 207 Property: ['key', 'value'], 208 PropertyDefinition: ['key', 'value'], 209 RestElement: [ 'argument' ], 210 ReturnStatement: ['argument'], 211 SequenceExpression: ['expressions'], 212 SpreadElement: ['argument'], 213 Super: [], 214 SwitchStatement: ['discriminant', 'cases'], 215 SwitchCase: ['test', 'consequent'], 216 TaggedTemplateExpression: ['tag', 'quasi'], 217 TemplateElement: [], 218 TemplateLiteral: ['quasis', 'expressions'], 219 ThisExpression: [], 220 ThrowStatement: ['argument'], 221 TryStatement: ['block', 'handler', 'finalizer'], 222 UnaryExpression: ['argument'], 223 UpdateExpression: ['argument'], 224 VariableDeclaration: ['declarations'], 225 VariableDeclarator: ['id', 'init'], 226 WhileStatement: ['test', 'body'], 227 WithStatement: ['object', 'body'], 228 YieldExpression: ['argument'] 229 }; 230 231 // unique id 232 BREAK = {}; 233 SKIP = {}; 234 REMOVE = {}; 235 236 VisitorOption = { 237 Break: BREAK, 238 Skip: SKIP, 239 Remove: REMOVE 240 }; 241 242 function Reference(parent, key) { 243 this.parent = parent; 244 this.key = key; 245 } 246 247 Reference.prototype.replace = function replace(node) { 248 this.parent[this.key] = node; 249 }; 250 251 Reference.prototype.remove = function remove() { 252 if (Array.isArray(this.parent)) { 253 this.parent.splice(this.key, 1); 254 return true; 255 } else { 256 this.replace(null); 257 return false; 258 } 259 }; 260 261 function Element(node, path, wrap, ref) { 262 this.node = node; 263 this.path = path; 264 this.wrap = wrap; 265 this.ref = ref; 266 } 267 268 function Controller() { } 269 270 // API: 271 // return property path array from root to current node 272 Controller.prototype.path = function path() { 273 var i, iz, j, jz, result, element; 274 275 function addToPath(result, path) { 276 if (Array.isArray(path)) { 277 for (j = 0, jz = path.length; j < jz; ++j) { 278 result.push(path[j]); 279 } 280 } else { 281 result.push(path); 282 } 283 } 284 285 // root node 286 if (!this.__current.path) { 287 return null; 288 } 289 290 // first node is sentinel, second node is root element 291 result = []; 292 for (i = 2, iz = this.__leavelist.length; i < iz; ++i) { 293 element = this.__leavelist[i]; 294 addToPath(result, element.path); 295 } 296 addToPath(result, this.__current.path); 297 return result; 298 }; 299 300 // API: 301 // return type of current node 302 Controller.prototype.type = function () { 303 var node = this.current(); 304 return node.type || this.__current.wrap; 305 }; 306 307 // API: 308 // return array of parent elements 309 Controller.prototype.parents = function parents() { 310 var i, iz, result; 311 312 // first node is sentinel 313 result = []; 314 for (i = 1, iz = this.__leavelist.length; i < iz; ++i) { 315 result.push(this.__leavelist[i].node); 316 } 317 318 return result; 319 }; 320 321 // API: 322 // return current node 323 Controller.prototype.current = function current() { 324 return this.__current.node; 325 }; 326 327 Controller.prototype.__execute = function __execute(callback, element) { 328 var previous, result; 329 330 result = undefined; 331 332 previous = this.__current; 333 this.__current = element; 334 this.__state = null; 335 if (callback) { 336 result = callback.call(this, element.node, this.__leavelist[this.__leavelist.length - 1].node); 337 } 338 this.__current = previous; 339 340 return result; 341 }; 342 343 // API: 344 // notify control skip / break 345 Controller.prototype.notify = function notify(flag) { 346 this.__state = flag; 347 }; 348 349 // API: 350 // skip child nodes of current node 351 Controller.prototype.skip = function () { 352 this.notify(SKIP); 353 }; 354 355 // API: 356 // break traversals 357 Controller.prototype['break'] = function () { 358 this.notify(BREAK); 359 }; 360 361 // API: 362 // remove node 363 Controller.prototype.remove = function () { 364 this.notify(REMOVE); 365 }; 366 367 Controller.prototype.__initialize = function(root, visitor) { 368 this.visitor = visitor; 369 this.root = root; 370 this.__worklist = []; 371 this.__leavelist = []; 372 this.__current = null; 373 this.__state = null; 374 this.__fallback = null; 375 if (visitor.fallback === 'iteration') { 376 this.__fallback = Object.keys; 377 } else if (typeof visitor.fallback === 'function') { 378 this.__fallback = visitor.fallback; 379 } 380 381 this.__keys = VisitorKeys; 382 if (visitor.keys) { 383 this.__keys = Object.assign(Object.create(this.__keys), visitor.keys); 384 } 385 }; 386 387 function isNode(node) { 388 if (node == null) { 389 return false; 390 } 391 return typeof node === 'object' && typeof node.type === 'string'; 392 } 393 394 function isProperty(nodeType, key) { 395 return (nodeType === Syntax.ObjectExpression || nodeType === Syntax.ObjectPattern) && 'properties' === key; 396 } 397 398 function candidateExistsInLeaveList(leavelist, candidate) { 399 for (var i = leavelist.length - 1; i >= 0; --i) { 400 if (leavelist[i].node === candidate) { 401 return true; 402 } 403 } 404 return false; 405 } 406 407 Controller.prototype.traverse = function traverse(root, visitor) { 408 var worklist, 409 leavelist, 410 element, 411 node, 412 nodeType, 413 ret, 414 key, 415 current, 416 current2, 417 candidates, 418 candidate, 419 sentinel; 420 421 this.__initialize(root, visitor); 422 423 sentinel = {}; 424 425 // reference 426 worklist = this.__worklist; 427 leavelist = this.__leavelist; 428 429 // initialize 430 worklist.push(new Element(root, null, null, null)); 431 leavelist.push(new Element(null, null, null, null)); 432 433 while (worklist.length) { 434 element = worklist.pop(); 435 436 if (element === sentinel) { 437 element = leavelist.pop(); 438 439 ret = this.__execute(visitor.leave, element); 440 441 if (this.__state === BREAK || ret === BREAK) { 442 return; 443 } 444 continue; 445 } 446 447 if (element.node) { 448 449 ret = this.__execute(visitor.enter, element); 450 451 if (this.__state === BREAK || ret === BREAK) { 452 return; 453 } 454 455 worklist.push(sentinel); 456 leavelist.push(element); 457 458 if (this.__state === SKIP || ret === SKIP) { 459 continue; 460 } 461 462 node = element.node; 463 nodeType = node.type || element.wrap; 464 candidates = this.__keys[nodeType]; 465 if (!candidates) { 466 if (this.__fallback) { 467 candidates = this.__fallback(node); 468 } else { 469 throw new Error('Unknown node type ' + nodeType + '.'); 470 } 471 } 472 473 current = candidates.length; 474 while ((current -= 1) >= 0) { 475 key = candidates[current]; 476 candidate = node[key]; 477 if (!candidate) { 478 continue; 479 } 480 481 if (Array.isArray(candidate)) { 482 current2 = candidate.length; 483 while ((current2 -= 1) >= 0) { 484 if (!candidate[current2]) { 485 continue; 486 } 487 488 if (candidateExistsInLeaveList(leavelist, candidate[current2])) { 489 continue; 490 } 491 492 if (isProperty(nodeType, candidates[current])) { 493 element = new Element(candidate[current2], [key, current2], 'Property', null); 494 } else if (isNode(candidate[current2])) { 495 element = new Element(candidate[current2], [key, current2], null, null); 496 } else { 497 continue; 498 } 499 worklist.push(element); 500 } 501 } else if (isNode(candidate)) { 502 if (candidateExistsInLeaveList(leavelist, candidate)) { 503 continue; 504 } 505 506 worklist.push(new Element(candidate, key, null, null)); 507 } 508 } 509 } 510 } 511 }; 512 513 Controller.prototype.replace = function replace(root, visitor) { 514 var worklist, 515 leavelist, 516 node, 517 nodeType, 518 target, 519 element, 520 current, 521 current2, 522 candidates, 523 candidate, 524 sentinel, 525 outer, 526 key; 527 528 function removeElem(element) { 529 var i, 530 key, 531 nextElem, 532 parent; 533 534 if (element.ref.remove()) { 535 // When the reference is an element of an array. 536 key = element.ref.key; 537 parent = element.ref.parent; 538 539 // If removed from array, then decrease following items' keys. 540 i = worklist.length; 541 while (i--) { 542 nextElem = worklist[i]; 543 if (nextElem.ref && nextElem.ref.parent === parent) { 544 if (nextElem.ref.key < key) { 545 break; 546 } 547 --nextElem.ref.key; 548 } 549 } 550 } 551 } 552 553 this.__initialize(root, visitor); 554 555 sentinel = {}; 556 557 // reference 558 worklist = this.__worklist; 559 leavelist = this.__leavelist; 560 561 // initialize 562 outer = { 563 root: root 564 }; 565 element = new Element(root, null, null, new Reference(outer, 'root')); 566 worklist.push(element); 567 leavelist.push(element); 568 569 while (worklist.length) { 570 element = worklist.pop(); 571 572 if (element === sentinel) { 573 element = leavelist.pop(); 574 575 target = this.__execute(visitor.leave, element); 576 577 // node may be replaced with null, 578 // so distinguish between undefined and null in this place 579 if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) { 580 // replace 581 element.ref.replace(target); 582 } 583 584 if (this.__state === REMOVE || target === REMOVE) { 585 removeElem(element); 586 } 587 588 if (this.__state === BREAK || target === BREAK) { 589 return outer.root; 590 } 591 continue; 592 } 593 594 target = this.__execute(visitor.enter, element); 595 596 // node may be replaced with null, 597 // so distinguish between undefined and null in this place 598 if (target !== undefined && target !== BREAK && target !== SKIP && target !== REMOVE) { 599 // replace 600 element.ref.replace(target); 601 element.node = target; 602 } 603 604 if (this.__state === REMOVE || target === REMOVE) { 605 removeElem(element); 606 element.node = null; 607 } 608 609 if (this.__state === BREAK || target === BREAK) { 610 return outer.root; 611 } 612 613 // node may be null 614 node = element.node; 615 if (!node) { 616 continue; 617 } 618 619 worklist.push(sentinel); 620 leavelist.push(element); 621 622 if (this.__state === SKIP || target === SKIP) { 623 continue; 624 } 625 626 nodeType = node.type || element.wrap; 627 candidates = this.__keys[nodeType]; 628 if (!candidates) { 629 if (this.__fallback) { 630 candidates = this.__fallback(node); 631 } else { 632 throw new Error('Unknown node type ' + nodeType + '.'); 633 } 634 } 635 636 current = candidates.length; 637 while ((current -= 1) >= 0) { 638 key = candidates[current]; 639 candidate = node[key]; 640 if (!candidate) { 641 continue; 642 } 643 644 if (Array.isArray(candidate)) { 645 current2 = candidate.length; 646 while ((current2 -= 1) >= 0) { 647 if (!candidate[current2]) { 648 continue; 649 } 650 if (isProperty(nodeType, candidates[current])) { 651 element = new Element(candidate[current2], [key, current2], 'Property', new Reference(candidate, current2)); 652 } else if (isNode(candidate[current2])) { 653 element = new Element(candidate[current2], [key, current2], null, new Reference(candidate, current2)); 654 } else { 655 continue; 656 } 657 worklist.push(element); 658 } 659 } else if (isNode(candidate)) { 660 worklist.push(new Element(candidate, key, null, new Reference(node, key))); 661 } 662 } 663 } 664 665 return outer.root; 666 }; 667 668 function traverse(root, visitor) { 669 var controller = new Controller(); 670 return controller.traverse(root, visitor); 671 } 672 673 function replace(root, visitor) { 674 var controller = new Controller(); 675 return controller.replace(root, visitor); 676 } 677 678 function extendCommentRange(comment, tokens) { 679 var target; 680 681 target = upperBound(tokens, function search(token) { 682 return token.range[0] > comment.range[0]; 683 }); 684 685 comment.extendedRange = [comment.range[0], comment.range[1]]; 686 687 if (target !== tokens.length) { 688 comment.extendedRange[1] = tokens[target].range[0]; 689 } 690 691 target -= 1; 692 if (target >= 0) { 693 comment.extendedRange[0] = tokens[target].range[1]; 694 } 695 696 return comment; 697 } 698 699 function attachComments(tree, providedComments, tokens) { 700 // At first, we should calculate extended comment ranges. 701 var comments = [], comment, len, i, cursor; 702 703 if (!tree.range) { 704 throw new Error('attachComments needs range information'); 705 } 706 707 // tokens array is empty, we attach comments to tree as 'leadingComments' 708 if (!tokens.length) { 709 if (providedComments.length) { 710 for (i = 0, len = providedComments.length; i < len; i += 1) { 711 comment = deepCopy(providedComments[i]); 712 comment.extendedRange = [0, tree.range[0]]; 713 comments.push(comment); 714 } 715 tree.leadingComments = comments; 716 } 717 return tree; 718 } 719 720 for (i = 0, len = providedComments.length; i < len; i += 1) { 721 comments.push(extendCommentRange(deepCopy(providedComments[i]), tokens)); 722 } 723 724 // This is based on John Freeman's implementation. 725 cursor = 0; 726 traverse(tree, { 727 enter: function (node) { 728 var comment; 729 730 while (cursor < comments.length) { 731 comment = comments[cursor]; 732 if (comment.extendedRange[1] > node.range[0]) { 733 break; 734 } 735 736 if (comment.extendedRange[1] === node.range[0]) { 737 if (!node.leadingComments) { 738 node.leadingComments = []; 739 } 740 node.leadingComments.push(comment); 741 comments.splice(cursor, 1); 742 } else { 743 cursor += 1; 744 } 745 } 746 747 // already out of owned node 748 if (cursor === comments.length) { 749 return VisitorOption.Break; 750 } 751 752 if (comments[cursor].extendedRange[0] > node.range[1]) { 753 return VisitorOption.Skip; 754 } 755 } 756 }); 757 758 cursor = 0; 759 traverse(tree, { 760 leave: function (node) { 761 var comment; 762 763 while (cursor < comments.length) { 764 comment = comments[cursor]; 765 if (node.range[1] < comment.extendedRange[0]) { 766 break; 767 } 768 769 if (node.range[1] === comment.extendedRange[0]) { 770 if (!node.trailingComments) { 771 node.trailingComments = []; 772 } 773 node.trailingComments.push(comment); 774 comments.splice(cursor, 1); 775 } else { 776 cursor += 1; 777 } 778 } 779 780 // already out of owned node 781 if (cursor === comments.length) { 782 return VisitorOption.Break; 783 } 784 785 if (comments[cursor].extendedRange[0] > node.range[1]) { 786 return VisitorOption.Skip; 787 } 788 } 789 }); 790 791 return tree; 792 } 793 794 exports.Syntax = Syntax; 795 exports.traverse = traverse; 796 exports.replace = replace; 797 exports.attachComments = attachComments; 798 exports.VisitorKeys = VisitorKeys; 799 exports.VisitorOption = VisitorOption; 800 exports.Controller = Controller; 801 exports.cloneEnvironment = function () { return clone({}); }; 802 803 return exports; 804 }(exports)); 805 /* vim: set sw=4 ts=4 et tw=80 : */