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