1 /*
   2  * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.nashorn.internal.ir.debug;
  27 
  28 import java.util.Arrays;
  29 import java.util.List;
  30 import java.util.ArrayList;
  31 import jdk.nashorn.internal.codegen.CompilerConstants;
  32 import jdk.nashorn.internal.ir.AccessNode;
  33 import jdk.nashorn.internal.ir.BinaryNode;
  34 import jdk.nashorn.internal.ir.Block;
  35 import jdk.nashorn.internal.ir.BlockStatement;
  36 import jdk.nashorn.internal.ir.BreakNode;
  37 import jdk.nashorn.internal.ir.CallNode;
  38 import jdk.nashorn.internal.ir.CaseNode;
  39 import jdk.nashorn.internal.ir.CatchNode;
  40 import jdk.nashorn.internal.ir.ContinueNode;
  41 import jdk.nashorn.internal.ir.EmptyNode;
  42 import jdk.nashorn.internal.ir.ExpressionStatement;
  43 import jdk.nashorn.internal.ir.ForNode;
  44 import jdk.nashorn.internal.ir.FunctionNode;
  45 import jdk.nashorn.internal.ir.IdentNode;
  46 import jdk.nashorn.internal.ir.IfNode;
  47 import jdk.nashorn.internal.ir.IndexNode;
  48 import jdk.nashorn.internal.ir.LabelNode;
  49 import jdk.nashorn.internal.ir.LexicalContext;
  50 import jdk.nashorn.internal.ir.LiteralNode;
  51 import jdk.nashorn.internal.ir.Node;
  52 import jdk.nashorn.internal.ir.ObjectNode;
  53 import jdk.nashorn.internal.ir.PropertyNode;
  54 import jdk.nashorn.internal.ir.ReturnNode;
  55 import jdk.nashorn.internal.ir.RuntimeNode;
  56 import jdk.nashorn.internal.ir.SplitNode;
  57 import jdk.nashorn.internal.ir.Statement;
  58 import jdk.nashorn.internal.ir.SwitchNode;
  59 import jdk.nashorn.internal.ir.TernaryNode;
  60 import jdk.nashorn.internal.ir.ThrowNode;
  61 import jdk.nashorn.internal.ir.TryNode;
  62 import jdk.nashorn.internal.ir.UnaryNode;
  63 import jdk.nashorn.internal.ir.VarNode;
  64 import jdk.nashorn.internal.ir.WhileNode;
  65 import jdk.nashorn.internal.ir.WithNode;
  66 import jdk.nashorn.internal.ir.visitor.NodeVisitor;
  67 import jdk.nashorn.internal.parser.JSONParser;
  68 import jdk.nashorn.internal.parser.Lexer.RegexToken;
  69 import jdk.nashorn.internal.parser.Parser;
  70 import jdk.nashorn.internal.parser.TokenType;
  71 import jdk.nashorn.internal.runtime.Context;
  72 import jdk.nashorn.internal.runtime.ParserException;
  73 import jdk.nashorn.internal.runtime.ScriptEnvironment;
  74 import jdk.nashorn.internal.runtime.Source;
  75 
  76 /**
  77  * This IR writer produces a JSON string that represents AST as a JSON string.
  78  */
  79 public final class JSONWriter extends NodeVisitor<LexicalContext> {
  80 
  81     /**
  82      * Returns AST as JSON compatible string.
  83      *
  84      * @param env  script environment to use
  85      * @param code code to be parsed
  86      * @param name name of the code source (used for location)
  87      * @param includeLoc tells whether to include location information for nodes or not
  88      * @return JSON string representation of AST of the supplied code
  89      */
  90     public static String parse(final ScriptEnvironment env, final String code, final String name, final boolean includeLoc) {
  91         final Parser       parser     = new Parser(env, new Source(name, code), new Context.ThrowErrorManager(), env._strict);
  92         final JSONWriter   jsonWriter = new JSONWriter(includeLoc);
  93         try {
  94             final FunctionNode functionNode = parser.parse(CompilerConstants.RUN_SCRIPT.symbolName());
  95             functionNode.accept(jsonWriter);
  96             return jsonWriter.getString();
  97         } catch (final ParserException e) {
  98             e.throwAsEcmaException();
  99             return null;
 100         }
 101     }
 102 
 103     @Override
 104     protected boolean enterDefault(final Node node) {
 105         objectStart();
 106         location(node);
 107 
 108         return true;
 109     }
 110 
 111     private boolean leave() {
 112         objectEnd();
 113         return false;
 114     }
 115 
 116     @Override
 117     protected Node leaveDefault(final Node node) {
 118         objectEnd();
 119         return null;
 120     }
 121 
 122     @Override
 123     public boolean enterAccessNode(final AccessNode accessNode) {
 124         enterDefault(accessNode);
 125 
 126         type("MemberExpression");
 127         comma();
 128 
 129         property("object");
 130         accessNode.getBase().accept(this);
 131         comma();
 132 
 133         property("property");
 134         accessNode.getProperty().accept(this);
 135         comma();
 136 
 137         property("computed", false);
 138 
 139         return leave();
 140     }
 141 
 142     @Override
 143     public boolean enterBlock(final Block block) {
 144         enterDefault(block);
 145 
 146         type("BlockStatement");
 147         comma();
 148 
 149         array("body", block.getStatements());
 150 
 151         return leave();
 152     }
 153 
 154     private static boolean isLogical(final TokenType tt) {
 155         switch (tt) {
 156         case AND:
 157         case OR:
 158             return true;
 159         default:
 160             return false;
 161         }
 162     }
 163 
 164     @Override
 165     public boolean enterBinaryNode(final BinaryNode binaryNode) {
 166         enterDefault(binaryNode);
 167 
 168         final String name;
 169         if (binaryNode.isAssignment()) {
 170             name = "AssignmentExpression";
 171         } else if (isLogical(binaryNode.tokenType())) {
 172             name = "LogicalExpression";
 173         } else {
 174             name = "BinaryExpression";
 175         }
 176 
 177         type(name);
 178         comma();
 179 
 180         property("operator", binaryNode.tokenType().getName());
 181         comma();
 182 
 183         property("left");
 184         binaryNode.lhs().accept(this);
 185         comma();
 186 
 187         property("right");
 188         binaryNode.rhs().accept(this);
 189 
 190         return leave();
 191     }
 192 
 193     @Override
 194     public boolean enterBreakNode(final BreakNode breakNode) {
 195         enterDefault(breakNode);
 196 
 197         type("BreakStatement");
 198         comma();
 199 
 200         final IdentNode label = breakNode.getLabel();
 201         property("label");
 202         if (label != null) {
 203             label.accept(this);
 204         } else {
 205             nullValue();
 206         }
 207 
 208         return leave();
 209     }
 210 
 211     @Override
 212     public boolean enterCallNode(final CallNode callNode) {
 213         enterDefault(callNode);
 214 
 215         type("CallExpression");
 216         comma();
 217 
 218         property("callee");
 219         callNode.getFunction().accept(this);
 220         comma();
 221 
 222         array("arguments", callNode.getArgs());
 223 
 224         return leave();
 225     }
 226 
 227     @Override
 228     public boolean enterCaseNode(final CaseNode caseNode) {
 229         enterDefault(caseNode);
 230 
 231         type("SwitchCase");
 232         comma();
 233 
 234         final Node test = caseNode.getTest();
 235         property("test");
 236         if (test != null) {
 237             test.accept(this);
 238         } else {
 239             nullValue();
 240         }
 241         comma();
 242 
 243         array("consequent", caseNode.getBody().getStatements());
 244 
 245         return leave();
 246     }
 247 
 248     @Override
 249     public boolean enterCatchNode(final CatchNode catchNode) {
 250         enterDefault(catchNode);
 251 
 252         type("CatchClause");
 253         comma();
 254 
 255         property("param");
 256         catchNode.getException().accept(this);
 257         comma();
 258 
 259         final Node guard = catchNode.getExceptionCondition();
 260         if (guard != null) {
 261             property("guard");
 262             guard.accept(this);
 263             comma();
 264         }
 265 
 266         property("body");
 267         catchNode.getBody().accept(this);
 268 
 269         return leave();
 270     }
 271 
 272     @Override
 273     public boolean enterContinueNode(final ContinueNode continueNode) {
 274         enterDefault(continueNode);
 275 
 276         type("ContinueStatement");
 277         comma();
 278 
 279         final IdentNode label = continueNode.getLabel();
 280         property("label");
 281         if (label != null) {
 282             label.accept(this);
 283         } else {
 284             nullValue();
 285         }
 286 
 287         return leave();
 288     }
 289 
 290     @Override
 291     public boolean enterEmptyNode(final EmptyNode emptyNode) {
 292         enterDefault(emptyNode);
 293 
 294         type("EmptyStatement");
 295 
 296         return leave();
 297     }
 298 
 299     @Override
 300     public boolean enterExpressionStatement(final ExpressionStatement expressionStatement) {
 301         // handle debugger statement
 302         final Node expression = expressionStatement.getExpression();
 303         if (expression instanceof RuntimeNode) {
 304             expression.accept(this);
 305             return false;
 306         }
 307 
 308         enterDefault(expressionStatement);
 309 
 310         type("ExpressionStatement");
 311         comma();
 312 
 313         property("expression");
 314         expression.accept(this);
 315 
 316         return leave();
 317     }
 318 
 319     @Override
 320     public boolean enterBlockStatement(BlockStatement blockStatement) {
 321         enterDefault(blockStatement);
 322 
 323         type("BlockStatement");
 324         comma();
 325 
 326         property("block");
 327         blockStatement.getBlock().accept(this);
 328 
 329         return leave();
 330     }
 331 
 332     @Override
 333     public boolean enterForNode(final ForNode forNode) {
 334         enterDefault(forNode);
 335 
 336         if (forNode.isForIn() || (forNode.isForEach() && forNode.getInit() != null)) {
 337             type("ForInStatement");
 338             comma();
 339 
 340             Node init = forNode.getInit();
 341             assert init != null;
 342             property("left");
 343             init.accept(this);
 344             comma();
 345 
 346             Node modify = forNode.getModify();
 347             assert modify != null;
 348             property("right");
 349             modify.accept(this);
 350             comma();
 351 
 352             property("body");
 353             forNode.getBody().accept(this);
 354             comma();
 355 
 356             property("each", forNode.isForEach());
 357         } else {
 358             type("ForStatement");
 359             comma();
 360 
 361             final Node init = forNode.getInit();
 362             property("init");
 363             if (init != null) {
 364                 init.accept(this);
 365             } else {
 366                 nullValue();
 367             }
 368             comma();
 369 
 370             final Node test = forNode.getTest();
 371             property("test");
 372             if (test != null) {
 373                 test.accept(this);
 374             } else {
 375                 nullValue();
 376             }
 377             comma();
 378 
 379             final Node update = forNode.getModify();
 380             property("update");
 381             if (update != null) {
 382                 update.accept(this);
 383             } else {
 384                 nullValue();
 385             }
 386             comma();
 387 
 388             property("body");
 389             forNode.getBody().accept(this);
 390         }
 391 
 392         return leave();
 393     }
 394 
 395     @Override
 396     public boolean enterFunctionNode(final FunctionNode functionNode) {
 397         final boolean program = functionNode.isProgram();
 398         if (program) {
 399             return emitProgram(functionNode);
 400         }
 401 
 402         enterDefault(functionNode);
 403         final String name;
 404         if (functionNode.isDeclared()) {
 405             name = "FunctionDeclaration";
 406         } else {
 407             name = "FunctionExpression";
 408         }
 409         type(name);
 410         comma();
 411 
 412         property("id");
 413         final FunctionNode.Kind kind = functionNode.getKind();
 414         if (functionNode.isAnonymous() || kind == FunctionNode.Kind.GETTER || kind == FunctionNode.Kind.SETTER) {
 415             nullValue();
 416         } else {
 417             functionNode.getIdent().accept(this);
 418         }
 419         comma();
 420 
 421         array("params", functionNode.getParameters());
 422         comma();
 423 
 424         arrayStart("defaults");
 425         arrayEnd();
 426         comma();
 427 
 428         property("rest");
 429         nullValue();
 430         comma();
 431 
 432         property("body");
 433         functionNode.getBody().accept(this);
 434         comma();
 435 
 436         property("generator", false);
 437         comma();
 438 
 439         property("expression", false);
 440 
 441         return leave();
 442     }
 443 
 444     private boolean emitProgram(final FunctionNode functionNode) {
 445         enterDefault(functionNode);
 446         type("Program");
 447         comma();
 448 
 449         // body consists of nested functions and statements
 450         final List<Statement> stats = functionNode.getBody().getStatements();
 451         final int size = stats.size();
 452         int idx = 0;
 453         arrayStart("body");
 454 
 455         for (final Node stat : stats) {
 456             stat.accept(this);
 457             if (idx != (size - 1)) {
 458                 comma();
 459             }
 460             idx++;
 461         }
 462         arrayEnd();
 463 
 464         return leave();
 465     }
 466 
 467     @Override
 468     public boolean enterIdentNode(final IdentNode identNode) {
 469         enterDefault(identNode);
 470 
 471         final String name = identNode.getName();
 472         if ("this".equals(name)) {
 473             type("ThisExpression");
 474         } else {
 475             type("Identifier");
 476             comma();
 477             property("name", identNode.getName());
 478         }
 479 
 480         return leave();
 481     }
 482 
 483     @Override
 484     public boolean enterIfNode(final IfNode ifNode) {
 485         enterDefault(ifNode);
 486 
 487         type("IfStatement");
 488         comma();
 489 
 490         property("test");
 491         ifNode.getTest().accept(this);
 492         comma();
 493 
 494         property("consequent");
 495         ifNode.getPass().accept(this);
 496         final Node elsePart = ifNode.getFail();
 497         comma();
 498 
 499         property("alternate");
 500         if (elsePart != null) {
 501             elsePart.accept(this);
 502         } else {
 503             nullValue();
 504         }
 505 
 506         return leave();
 507     }
 508 
 509     @Override
 510     public boolean enterIndexNode(final IndexNode indexNode) {
 511         enterDefault(indexNode);
 512 
 513         type("MemberExpression");
 514         comma();
 515 
 516         property("object");
 517         indexNode.getBase().accept(this);
 518         comma();
 519 
 520         property("property");
 521         indexNode.getIndex().accept(this);
 522         comma();
 523 
 524         property("computed", true);
 525 
 526         return leave();
 527     }
 528 
 529     @Override
 530     public boolean enterLabelNode(final LabelNode labelNode) {
 531         enterDefault(labelNode);
 532 
 533         type("LabeledStatement");
 534         comma();
 535 
 536         property("label");
 537         labelNode.getLabel().accept(this);
 538         comma();
 539 
 540         property("body");
 541         labelNode.getBody().accept(this);
 542 
 543         return leave();
 544     }
 545 
 546     @SuppressWarnings("rawtypes")
 547     @Override
 548     public boolean enterLiteralNode(final LiteralNode literalNode) {
 549         enterDefault(literalNode);
 550 
 551         if (literalNode instanceof LiteralNode.ArrayLiteralNode) {
 552             type("ArrayExpression");
 553             comma();
 554 
 555             final Node[] value = literalNode.getArray();
 556             array("elements", Arrays.asList(value));
 557         } else {
 558             type("Literal");
 559             comma();
 560 
 561             property("value");
 562             final Object value = literalNode.getValue();
 563             if (value instanceof RegexToken) {
 564                 // encode RegExp literals as Strings of the form /.../<flags>
 565                 final RegexToken regex = (RegexToken)value;
 566                 final StringBuilder regexBuf = new StringBuilder();
 567                 regexBuf.append('/');
 568                 regexBuf.append(regex.getExpression());
 569                 regexBuf.append('/');
 570                 regexBuf.append(regex.getOptions());
 571                 buf.append(quote(regexBuf.toString()));
 572             } else {
 573                 final String str = literalNode.getString();
 574                 // encode every String literal with prefix '$' so that script
 575                 // can differentiate b/w RegExps as Strings and Strings.
 576                 buf.append(literalNode.isString()? quote("$" + str) : str);
 577             }
 578         }
 579 
 580         return leave();
 581     }
 582 
 583     @Override
 584     public boolean enterObjectNode(final ObjectNode objectNode) {
 585         enterDefault(objectNode);
 586 
 587         type("ObjectExpression");
 588         comma();
 589 
 590         array("properties", objectNode.getElements());
 591 
 592         return leave();
 593     }
 594 
 595     @Override
 596     public boolean enterPropertyNode(final PropertyNode propertyNode) {
 597         final Node key = propertyNode.getKey();
 598 
 599         final Node value = propertyNode.getValue();
 600         if (value != null) {
 601             objectStart();
 602             location(propertyNode);
 603 
 604             property("key");
 605             key.accept(this);
 606             comma();
 607 
 608             property("value");
 609             value.accept(this);
 610             comma();
 611 
 612             property("kind", "init");
 613 
 614             objectEnd();
 615         } else {
 616             // getter
 617             final Node getter = propertyNode.getGetter();
 618             if (getter != null) {
 619                 objectStart();
 620                 location(propertyNode);
 621 
 622                 property("key");
 623                 key.accept(this);
 624                 comma();
 625 
 626                 property("value");
 627                 getter.accept(this);
 628                 comma();
 629 
 630                 property("kind", "get");
 631 
 632                 objectEnd();
 633             }
 634 
 635             // setter
 636             final Node setter = propertyNode.getSetter();
 637             if (setter != null) {
 638                 if (getter != null) {
 639                     comma();
 640                 }
 641                 objectStart();
 642                 location(propertyNode);
 643 
 644                 property("key");
 645                 key.accept(this);
 646                 comma();
 647 
 648                 property("value");
 649                 setter.accept(this);
 650                 comma();
 651 
 652                 property("kind", "set");
 653 
 654                 objectEnd();
 655             }
 656         }
 657 
 658         return false;
 659     }
 660 
 661     @Override
 662     public boolean enterReturnNode(final ReturnNode returnNode) {
 663         enterDefault(returnNode);
 664 
 665         type("ReturnStatement");
 666         comma();
 667 
 668         final Node arg = returnNode.getExpression();
 669         property("argument");
 670         if (arg != null) {
 671             arg.accept(this);
 672         } else {
 673             nullValue();
 674         }
 675 
 676         return leave();
 677     }
 678 
 679     @Override
 680     public boolean enterRuntimeNode(final RuntimeNode runtimeNode) {
 681         final RuntimeNode.Request req = runtimeNode.getRequest();
 682 
 683         if (req == RuntimeNode.Request.DEBUGGER) {
 684             enterDefault(runtimeNode);
 685             type("DebuggerStatement");
 686             return leave();
 687         }
 688 
 689         return false;
 690     }
 691 
 692     @Override
 693     public boolean enterSplitNode(final SplitNode splitNode) {
 694         return false;
 695     }
 696 
 697     @Override
 698     public boolean enterSwitchNode(final SwitchNode switchNode) {
 699         enterDefault(switchNode);
 700 
 701         type("SwitchStatement");
 702         comma();
 703 
 704         property("discriminant");
 705         switchNode.getExpression().accept(this);
 706         comma();
 707 
 708         array("cases", switchNode.getCases());
 709 
 710         return leave();
 711     }
 712 
 713     @Override
 714     public boolean enterTernaryNode(final TernaryNode ternaryNode) {
 715         enterDefault(ternaryNode);
 716 
 717         type("ConditionalExpression");
 718         comma();
 719 
 720         property("test");
 721         ternaryNode.getTest().accept(this);
 722         comma();
 723 
 724         property("consequent");
 725         ternaryNode.getTrueExpression().accept(this);
 726         comma();
 727 
 728         property("alternate");
 729         ternaryNode.getFalseExpression().accept(this);
 730 
 731         return leave();
 732     }
 733 
 734     @Override
 735     public boolean enterThrowNode(final ThrowNode throwNode) {
 736         enterDefault(throwNode);
 737 
 738         type("ThrowStatement");
 739         comma();
 740 
 741         property("argument");
 742         throwNode.getExpression().accept(this);
 743 
 744         return leave();
 745     }
 746 
 747     @Override
 748     public boolean enterTryNode(final TryNode tryNode) {
 749         enterDefault(tryNode);
 750 
 751         type("TryStatement");
 752         comma();
 753 
 754         property("block");
 755         tryNode.getBody().accept(this);
 756         comma();
 757 
 758 
 759         final List<? extends Node> catches = tryNode.getCatches();
 760         final List<CatchNode> guarded = new ArrayList<>();
 761         CatchNode unguarded = null;
 762         if (catches != null) {
 763             for (Node n : catches) {
 764                 CatchNode cn = (CatchNode)n;
 765                 if (cn.getExceptionCondition() != null) {
 766                     guarded.add(cn);
 767                 } else {
 768                     assert unguarded == null: "too many unguarded?";
 769                     unguarded = cn;
 770                 }
 771             }
 772         }
 773 
 774         array("guardedHandlers", guarded);
 775         comma();
 776 
 777         property("handler");
 778         if (unguarded != null) {
 779             unguarded.accept(this);
 780         } else {
 781             nullValue();
 782         }
 783         comma();
 784 
 785         property("finalizer");
 786         final Node finallyNode = tryNode.getFinallyBody();
 787         if (finallyNode != null) {
 788             finallyNode.accept(this);
 789         } else {
 790             nullValue();
 791         }
 792 
 793         return leave();
 794     }
 795 
 796     @Override
 797     public boolean enterUnaryNode(final UnaryNode unaryNode) {
 798         enterDefault(unaryNode);
 799 
 800         final TokenType tokenType = unaryNode.tokenType();
 801         if (tokenType == TokenType.NEW) {
 802             type("NewExpression");
 803             comma();
 804 
 805             final CallNode callNode = (CallNode)unaryNode.rhs();
 806             property("callee");
 807             callNode.getFunction().accept(this);
 808             comma();
 809 
 810             array("arguments", callNode.getArgs());
 811         } else {
 812             final String operator;
 813             final boolean prefix;
 814             switch (tokenType) {
 815             case INCPOSTFIX:
 816                 prefix = false;
 817                 operator = "++";
 818                 break;
 819             case DECPOSTFIX:
 820                 prefix = false;
 821                 operator = "--";
 822                 break;
 823             case INCPREFIX:
 824                 operator = "++";
 825                 prefix = true;
 826                 break;
 827             case DECPREFIX:
 828                 operator = "--";
 829                 prefix = true;
 830                 break;
 831             default:
 832                 prefix = true;
 833                 operator = tokenType.getName();
 834                 break;
 835             }
 836 
 837             type(unaryNode.isAssignment()? "UpdateExpression" : "UnaryExpression");
 838             comma();
 839 
 840             property("operator", operator);
 841             comma();
 842 
 843             property("prefix", prefix);
 844             comma();
 845 
 846             property("argument");
 847             unaryNode.rhs().accept(this);
 848         }
 849 
 850         return leave();
 851     }
 852 
 853     @Override
 854     public boolean enterVarNode(final VarNode varNode) {
 855         final Node init = varNode.getInit();
 856         if (init instanceof FunctionNode && ((FunctionNode)init).isDeclared()) {
 857             // function declaration - don't emit VariableDeclaration instead
 858             // just emit FunctionDeclaration using 'init' Node.
 859             init.accept(this);
 860             return false;
 861         }
 862 
 863         enterDefault(varNode);
 864 
 865         type("VariableDeclaration");
 866         comma();
 867 
 868         arrayStart("declarations");
 869 
 870         // VariableDeclarator
 871         objectStart();
 872         location(varNode.getName());
 873 
 874         type("VariableDeclarator");
 875         comma();
 876 
 877         property("id");
 878         varNode.getName().accept(this);
 879         comma();
 880 
 881         property("init");
 882         if (init != null) {
 883             init.accept(this);
 884         } else {
 885             nullValue();
 886         }
 887 
 888         // VariableDeclarator
 889         objectEnd();
 890 
 891         // declarations
 892         arrayEnd();
 893 
 894         return leave();
 895     }
 896 
 897     @Override
 898     public boolean enterWhileNode(final WhileNode whileNode) {
 899         enterDefault(whileNode);
 900 
 901         type(whileNode.isDoWhile() ? "DoWhileStatement" : "WhileStatement");
 902         comma();
 903 
 904         if (whileNode.isDoWhile()) {
 905             property("body");
 906             whileNode.getBody().accept(this);
 907             comma();
 908 
 909             property("test");
 910             whileNode.getTest().accept(this);
 911         } else {
 912             property("test");
 913             whileNode.getTest().accept(this);
 914             comma();
 915 
 916             property("body");
 917             whileNode.getBody().accept(this);
 918         }
 919 
 920         return leave();
 921     }
 922 
 923     @Override
 924     public boolean enterWithNode(final WithNode withNode) {
 925         enterDefault(withNode);
 926 
 927         type("WithStatement");
 928         comma();
 929 
 930         property("object");
 931         withNode.getExpression().accept(this);
 932         comma();
 933 
 934         property("body");
 935         withNode.getBody().accept(this);
 936 
 937         return leave();
 938    }
 939 
 940     // Internals below
 941 
 942     private JSONWriter(final boolean includeLocation) {
 943         super(new LexicalContext());
 944         this.buf             = new StringBuilder();
 945         this.includeLocation = includeLocation;
 946     }
 947 
 948     private final StringBuilder buf;
 949     private final boolean includeLocation;
 950 
 951     private String getString() {
 952         return buf.toString();
 953     }
 954 
 955     private void property(final String key, final String value, final boolean escape) {
 956         buf.append('"');
 957         buf.append(key);
 958         buf.append("\":");
 959         if (value != null) {
 960             if (escape) buf.append('"');
 961             buf.append(value);
 962             if (escape) buf.append('"');
 963         }
 964     }
 965 
 966     private void property(final String key, final String value) {
 967         property(key, value, true);
 968     }
 969 
 970     private void property(final String key, final boolean value) {
 971         property(key, Boolean.toString(value), false);
 972     }
 973 
 974     private void property(final String key, final int value) {
 975         property(key, Integer.toString(value), false);
 976     }
 977 
 978     private void property(final String key) {
 979         property(key, null);
 980     }
 981 
 982     private void type(final String value) {
 983         property("type", value);
 984     }
 985 
 986     private void objectStart(final String name) {
 987         buf.append('"');
 988         buf.append(name);
 989         buf.append("\":{");
 990     }
 991 
 992     private void objectStart() {
 993         buf.append('{');
 994     }
 995 
 996     private void objectEnd() {
 997         buf.append('}');
 998     }
 999 
1000     private void array(final String name, final List<? extends Node> nodes) {
1001         // The size, idx comparison is just to avoid trailing comma..
1002         final int size = nodes.size();
1003         int idx = 0;
1004         arrayStart(name);
1005         for (final Node node : nodes) {
1006             if (node != null) {
1007                 node.accept(this);
1008             } else {
1009                 nullValue();
1010             }
1011             if (idx != (size - 1)) {
1012                 comma();
1013             }
1014             idx++;
1015         }
1016         arrayEnd();
1017     }
1018 
1019     private void arrayStart(final String name) {
1020         buf.append('"');
1021         buf.append(name);
1022         buf.append('"');
1023         buf.append(':');
1024         buf.append('[');
1025     }
1026 
1027     private void arrayEnd() {
1028         buf.append(']');
1029     }
1030 
1031     private void comma() {
1032         buf.append(',');
1033     }
1034 
1035     private void nullValue() {
1036         buf.append("null");
1037     }
1038 
1039     private void location(final Node node) {
1040         if (includeLocation) {
1041             objectStart("loc");
1042 
1043             // source name
1044             final Source src = lc.getCurrentFunction().getSource();
1045             property("source", src.getName());
1046             comma();
1047 
1048             // start position
1049             objectStart("start");
1050             final int start = node.getStart();
1051             property("line", src.getLine(start));
1052             comma();
1053             property("column", src.getColumn(start));
1054             objectEnd();
1055             comma();
1056 
1057             // end position
1058             objectStart("end");
1059             final int end = node.getFinish();
1060             property("line", src.getLine(end));
1061             comma();
1062             property("column", src.getColumn(end));
1063             objectEnd();
1064 
1065             // end 'loc'
1066             objectEnd();
1067 
1068             comma();
1069         }
1070     }
1071 
1072     private static String quote(final String str) {
1073         return JSONParser.quote(str);
1074     }
1075 }