001    // Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
002    // for details. All rights reserved. Use of this source code is governed by a
003    // BSD-style license that can be found in the LICENSE file.
004    
005    package com.google.dart.compiler.backend.js;
006    
007    import com.google.dart.compiler.backend.js.ast.*;
008    import com.google.dart.compiler.backend.js.ast.JsVars.JsVar;
009    import com.google.dart.compiler.util.TextOutput;
010    import gnu.trove.THashSet;
011    
012    import java.util.Iterator;
013    import java.util.List;
014    import java.util.Map;
015    import java.util.Set;
016    
017    import static com.google.dart.compiler.backend.js.ast.JsNumberLiteral.JsDoubleLiteral;
018    import static com.google.dart.compiler.backend.js.ast.JsNumberLiteral.JsIntLiteral;
019    
020    /**
021     * Produces text output from a JavaScript AST.
022     */
023    public class JsToStringGenerationVisitor extends JsVisitor {
024        private static final char[] CHARS_BREAK = "break".toCharArray();
025        private static final char[] CHARS_CASE = "case".toCharArray();
026        private static final char[] CHARS_CATCH = "catch".toCharArray();
027        private static final char[] CHARS_CONTINUE = "continue".toCharArray();
028        private static final char[] CHARS_DEBUGGER = "debugger".toCharArray();
029        private static final char[] CHARS_DEFAULT = "default".toCharArray();
030        private static final char[] CHARS_DO = "do".toCharArray();
031        private static final char[] CHARS_ELSE = "else".toCharArray();
032        private static final char[] CHARS_FALSE = "false".toCharArray();
033        private static final char[] CHARS_FINALLY = "finally".toCharArray();
034        private static final char[] CHARS_FOR = "for".toCharArray();
035        private static final char[] CHARS_FUNCTION = "function".toCharArray();
036        private static final char[] CHARS_IF = "if".toCharArray();
037        private static final char[] CHARS_IN = "in".toCharArray();
038        private static final char[] CHARS_NEW = "new".toCharArray();
039        private static final char[] CHARS_NULL = "null".toCharArray();
040        private static final char[] CHARS_RETURN = "return".toCharArray();
041        private static final char[] CHARS_SWITCH = "switch".toCharArray();
042        private static final char[] CHARS_THIS = "this".toCharArray();
043        private static final char[] CHARS_THROW = "throw".toCharArray();
044        private static final char[] CHARS_TRUE = "true".toCharArray();
045        private static final char[] CHARS_TRY = "try".toCharArray();
046        private static final char[] CHARS_VAR = "var".toCharArray();
047        private static final char[] CHARS_WHILE = "while".toCharArray();
048        private static final char[] HEX_DIGITS = {
049                '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
050    
051        /**
052         * How many lines of code to print inside of a JsBlock when printing terse.
053         */
054        private static final int JSBLOCK_LINES_TO_PRINT = 3;
055    
056        public static CharSequence javaScriptString(String value) {
057            return javaScriptString(value, false);
058        }
059    
060        /**
061         * Generate JavaScript code that evaluates to the supplied string. Adapted
062         * from {@link org.mozilla.javascript.ScriptRuntime#escapeString(String)}
063         * . The difference is that we quote with either " or ' depending on
064         * which one is used less inside the string.
065         */
066        @SuppressWarnings({"ConstantConditions", "UnnecessaryFullyQualifiedName", "JavadocReference"})
067        public static CharSequence javaScriptString(CharSequence chars, boolean forceDoubleQuote) {
068            final int n = chars.length();
069            int quoteCount = 0;
070            int aposCount = 0;
071    
072            for (int i = 0; i < n; i++) {
073                switch (chars.charAt(i)) {
074                    case '"':
075                        ++quoteCount;
076                        break;
077                    case '\'':
078                        ++aposCount;
079                        break;
080                }
081            }
082    
083            StringBuilder result = new StringBuilder(n + 16);
084    
085            char quoteChar = (quoteCount < aposCount || forceDoubleQuote) ? '"' : '\'';
086            result.append(quoteChar);
087    
088            for (int i = 0; i < n; i++) {
089                char c = chars.charAt(i);
090    
091                if (' ' <= c && c <= '~' && c != quoteChar && c != '\\') {
092                    // an ordinary print character (like C isprint())
093                    result.append(c);
094                    continue;
095                }
096    
097                int escape = -1;
098                switch (c) {
099                    case '\b':
100                        escape = 'b';
101                        break;
102                    case '\f':
103                        escape = 'f';
104                        break;
105                    case '\n':
106                        escape = 'n';
107                        break;
108                    case '\r':
109                        escape = 'r';
110                        break;
111                    case '\t':
112                        escape = 't';
113                        break;
114                    case '"':
115                        escape = '"';
116                        break; // only reach here if == quoteChar
117                    case '\'':
118                        escape = '\'';
119                        break; // only reach here if == quoteChar
120                    case '\\':
121                        escape = '\\';
122                        break;
123                }
124    
125                if (escape >= 0) {
126                    // an \escaped sort of character
127                    result.append('\\');
128                    result.append((char) escape);
129                }
130                else {
131                    /*
132                    * Emit characters from 0 to 31 that don't have a single character
133                    * escape sequence in octal where possible. This saves one or two
134                    * characters compared to the hexadecimal format '\xXX'.
135                    *
136                    * These short octal sequences may only be used at the end of the string
137                    * or where the following character is a non-digit. Otherwise, the
138                    * following character would be incorrectly interpreted as belonging to
139                    * the sequence.
140                    */
141                    if (c < ' ' && (i == n - 1 || chars.charAt(i + 1) < '0' || chars.charAt(i + 1) > '9')) {
142                        result.append('\\');
143                        if (c > 0x7) {
144                            result.append((char) ('0' + (0x7 & (c >> 3))));
145                        }
146                        result.append((char) ('0' + (0x7 & c)));
147                    }
148                    else {
149                        int hexSize;
150                        if (c < 256) {
151                            // 2-digit hex
152                            result.append("\\x");
153                            hexSize = 2;
154                        }
155                        else {
156                            // Unicode.
157                            result.append("\\u");
158                            hexSize = 4;
159                        }
160                        // append hexadecimal form of ch left-padded with 0
161                        for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) {
162                            int digit = 0xf & (c >> shift);
163                            result.append(HEX_DIGITS[digit]);
164                        }
165                    }
166                }
167            }
168            result.append(quoteChar);
169            escapeClosingTags(result);
170            return result;
171        }
172    
173        /**
174         * Escapes any closing XML tags embedded in <code>str</code>, which could
175         * potentially cause a parse failure in a browser, for example, embedding a
176         * closing <code>&lt;script&gt;</code> tag.
177         *
178         * @param str an unescaped literal; May be null
179         */
180        private static void escapeClosingTags(StringBuilder str) {
181            if (str == null) {
182                return;
183            }
184    
185            int index = 0;
186            while ((index = str.indexOf("</", index)) != -1) {
187                str.insert(index + 1, '\\');
188            }
189        }
190    
191        protected boolean needSemi = true;
192        private boolean lineBreakAfterBlock = true;
193    
194        /**
195         * "Global" blocks are either the global block of a fragment, or a block
196         * nested directly within some other global block. This definition matters
197         * because the statements designated by statementEnds and statementStarts are
198         * those that appear directly within these global blocks.
199         */
200        private Set<JsBlock> globalBlocks = new THashSet<JsBlock>();
201        protected final TextOutput p;
202    
203        public JsToStringGenerationVisitor(TextOutput out) {
204            p = out;
205        }
206    
207        @Override
208        public void visitArrayAccess(JsArrayAccess x) {
209            printPair(x, x.getArrayExpression());
210            leftSquare();
211            accept(x.getIndexExpression());
212            rightSquare();
213        }
214    
215        @Override
216        public void visitArray(JsArrayLiteral x) {
217            leftSquare();
218            printExpressions(x.getExpressions());
219            rightSquare();
220        }
221    
222        private void printExpressions(List<JsExpression> expressions) {
223            boolean notFirst = false;
224            for (JsExpression expression : expressions) {
225                notFirst = sepCommaOptSpace(notFirst) && !(expression instanceof JsDocComment);
226                boolean isEnclosed = parenPushIfCommaExpression(expression);
227                accept(expression);
228                if (isEnclosed) {
229                    rightParen();
230                }
231            }
232        }
233    
234        @Override
235        public void visitBinaryExpression(JsBinaryOperation binaryOperation) {
236            JsBinaryOperator operator = binaryOperation.getOperator();
237            JsExpression arg1 = binaryOperation.getArg1();
238            boolean isExpressionEnclosed = parenPush(binaryOperation, arg1, !operator.isLeftAssociative());
239    
240            accept(arg1);
241            if (operator.isKeyword()) {
242                _parenPopOrSpace(binaryOperation, arg1, !operator.isLeftAssociative());
243            }
244            else if (operator != JsBinaryOperator.COMMA) {
245                if (isExpressionEnclosed) {
246                    rightParen();
247                }
248                spaceOpt();
249            }
250    
251            p.print(operator.getSymbol());
252    
253            JsExpression arg2 = binaryOperation.getArg2();
254            boolean isParenOpened;
255            if (operator == JsBinaryOperator.COMMA) {
256                isParenOpened = false;
257                spaceOpt();
258            }
259            else if (arg2 instanceof JsBinaryOperation && ((JsBinaryOperation) arg2).getOperator() == JsBinaryOperator.AND) {
260                spaceOpt();
261                leftParen();
262                isParenOpened = true;
263            }
264            else {
265                if (spaceCalc(operator, arg2)) {
266                    isParenOpened = _parenPushOrSpace(binaryOperation, arg2, operator.isLeftAssociative());
267                }
268                else {
269                    spaceOpt();
270                    isParenOpened = parenPush(binaryOperation, arg2, operator.isLeftAssociative());
271                }
272            }
273            accept(arg2);
274            if (isParenOpened) {
275                rightParen();
276            }
277        }
278    
279        @Override
280        public void visitBlock(JsBlock x) {
281            printJsBlock(x, true, true);
282        }
283    
284        @Override
285        public void visitBoolean(JsLiteral.JsBooleanLiteral x) {
286            if (x.getValue()) {
287                p.print(CHARS_TRUE);
288            }
289            else {
290                p.print(CHARS_FALSE);
291            }
292        }
293    
294        @Override
295        public void visitBreak(JsBreak x) {
296            p.print(CHARS_BREAK);
297            continueOrBreakLabel(x);
298        }
299    
300        @Override
301        public void visitContinue(JsContinue x) {
302            p.print(CHARS_CONTINUE);
303            continueOrBreakLabel(x);
304        }
305    
306        private void continueOrBreakLabel(JsContinue x) {
307            String label = x.getLabel();
308            if (label != null) {
309                space();
310                p.print(label);
311            }
312        }
313    
314        @Override
315        public void visitCase(JsCase x) {
316            p.print(CHARS_CASE);
317            space();
318            accept(x.getCaseExpression());
319            _colon();
320            newlineOpt();
321    
322            printSwitchMemberStatements(x);
323        }
324    
325        private void printSwitchMemberStatements(JsSwitchMember x) {
326            p.indentIn();
327            for (JsStatement stmt : x.getStatements()) {
328                needSemi = true;
329                accept(stmt);
330                if (needSemi) {
331                    semi();
332                }
333                newlineOpt();
334            }
335            p.indentOut();
336            needSemi = false;
337        }
338    
339        @Override
340        public void visitCatch(JsCatch x) {
341            spaceOpt();
342            p.print(CHARS_CATCH);
343            spaceOpt();
344            leftParen();
345            nameDef(x.getParameter().getName());
346    
347            // Optional catch condition.
348            //
349            JsExpression catchCond = x.getCondition();
350            if (catchCond != null) {
351                space();
352                _if();
353                space();
354                accept(catchCond);
355            }
356    
357            rightParen();
358            spaceOpt();
359            accept(x.getBody());
360        }
361    
362        @Override
363        public void visitConditional(JsConditional x) {
364            // Associativity: for the then and else branches, it is safe to insert
365            // another
366            // ternary expression, but if the test expression is a ternary, it should
367            // get parentheses around it.
368            printPair(x, x.getTestExpression(), true);
369            spaceOpt();
370            p.print('?');
371            spaceOpt();
372            printPair(x, x.getThenExpression());
373            spaceOpt();
374            _colon();
375            spaceOpt();
376            printPair(x, x.getElseExpression());
377        }
378    
379        private void printPair(JsExpression parent, JsExpression expression, boolean wrongAssoc) {
380            boolean isNeedParen = parenCalc(parent, expression, wrongAssoc);
381            if (isNeedParen) {
382                leftParen();
383            }
384            accept(expression);
385            if (isNeedParen) {
386                rightParen();
387            }
388        }
389    
390        private void printPair(JsExpression parent, JsExpression expression) {
391            printPair(parent, expression, false);
392        }
393    
394        @Override
395        public void visitDebugger(JsDebugger x) {
396            p.print(CHARS_DEBUGGER);
397        }
398    
399        @Override
400        public void visitDefault(JsDefault x) {
401            p.print(CHARS_DEFAULT);
402            _colon();
403    
404            printSwitchMemberStatements(x);
405        }
406    
407        @Override
408        public void visitWhile(JsWhile x) {
409            _while();
410            spaceOpt();
411            leftParen();
412            accept(x.getCondition());
413            rightParen();
414            nestedPush(x.getBody());
415            accept(x.getBody());
416            nestedPop(x.getBody());
417        }
418    
419        @Override
420        public void visitDoWhile(JsDoWhile x) {
421            p.print(CHARS_DO);
422            nestedPush(x.getBody());
423            accept(x.getBody());
424            nestedPop(x.getBody());
425            if (needSemi) {
426                semi();
427                newlineOpt();
428            }
429            else {
430                spaceOpt();
431                needSemi = true;
432            }
433            _while();
434            spaceOpt();
435            leftParen();
436            accept(x.getCondition());
437            rightParen();
438        }
439    
440        @Override
441        public void visitEmpty(JsEmpty x) {
442        }
443    
444        @Override
445        public void visitExpressionStatement(JsExpressionStatement x) {
446            boolean surroundWithParentheses = JsFirstExpressionVisitor.exec(x);
447            if (surroundWithParentheses) {
448                leftParen();
449            }
450            accept(x.getExpression());
451            if (surroundWithParentheses) {
452                rightParen();
453            }
454        }
455    
456        @Override
457        public void visitFor(JsFor x) {
458            _for();
459            spaceOpt();
460            leftParen();
461    
462            // The init expressions or var decl.
463            //
464            if (x.getInitExpression() != null) {
465                accept(x.getInitExpression());
466            }
467            else if (x.getInitVars() != null) {
468                accept(x.getInitVars());
469            }
470    
471            semi();
472    
473            // The loop test.
474            //
475            if (x.getCondition() != null) {
476                spaceOpt();
477                accept(x.getCondition());
478            }
479    
480            semi();
481    
482            // The incr expression.
483            //
484            if (x.getIncrementExpression() != null) {
485                spaceOpt();
486                accept(x.getIncrementExpression());
487            }
488    
489            rightParen();
490            nestedPush(x.getBody());
491            accept(x.getBody());
492            nestedPop(x.getBody());
493        }
494    
495        @Override
496        public void visitForIn(JsForIn x) {
497            _for();
498            spaceOpt();
499            leftParen();
500    
501            if (x.getIterVarName() != null) {
502                var();
503                space();
504                nameDef(x.getIterVarName());
505    
506                if (x.getIterExpression() != null) {
507                    spaceOpt();
508                    assignment();
509                    spaceOpt();
510                    accept(x.getIterExpression());
511                }
512            }
513            else {
514                // Just a name ref.
515                //
516                accept(x.getIterExpression());
517            }
518    
519            space();
520            p.print(CHARS_IN);
521            space();
522            accept(x.getObjectExpression());
523    
524            rightParen();
525            nestedPush(x.getBody());
526            accept(x.getBody());
527            nestedPop(x.getBody());
528        }
529    
530        @Override
531        public void visitFunction(JsFunction x) {
532            p.print(CHARS_FUNCTION);
533            space();
534            if (x.getName() != null) {
535                nameOf(x);
536            }
537    
538            leftParen();
539            boolean notFirst = false;
540            for (Object element : x.getParameters()) {
541                JsParameter param = (JsParameter) element;
542                notFirst = sepCommaOptSpace(notFirst);
543                accept(param);
544            }
545            rightParen();
546            space();
547    
548            lineBreakAfterBlock = false;
549            accept(x.getBody());
550            needSemi = true;
551        }
552    
553        @Override
554        public void visitIf(JsIf x) {
555            _if();
556            spaceOpt();
557            leftParen();
558            accept(x.getIfExpression());
559            rightParen();
560            JsStatement thenStmt = x.getThenStatement();
561            JsStatement elseStatement = x.getElseStatement();
562            if (elseStatement != null && thenStmt instanceof JsIf && ((JsIf)thenStmt).getElseStatement() == null) {
563                thenStmt = new JsBlock(thenStmt);
564            }
565            nestedPush(thenStmt);
566            accept(thenStmt);
567            nestedPop(thenStmt);
568            if (elseStatement != null) {
569                if (needSemi) {
570                    semi();
571                    newlineOpt();
572                }
573                else {
574                    spaceOpt();
575                    needSemi = true;
576                }
577                p.print(CHARS_ELSE);
578                boolean elseIf = elseStatement instanceof JsIf;
579                if (!elseIf) {
580                    nestedPush(elseStatement);
581                }
582                else {
583                    space();
584                }
585                accept(elseStatement);
586                if (!elseIf) {
587                    nestedPop(elseStatement);
588                }
589            }
590        }
591    
592        @Override
593        public void visitInvocation(JsInvocation invocation) {
594            printPair(invocation, invocation.getQualifier());
595    
596            leftParen();
597            printExpressions(invocation.getArguments());
598            rightParen();
599        }
600    
601        @Override
602        public void visitLabel(JsLabel x) {
603            nameOf(x);
604            _colon();
605            spaceOpt();
606            accept(x.getStatement());
607        }
608    
609        @Override
610        public void visitNameRef(JsNameRef nameRef) {
611            JsExpression qualifier = nameRef.getQualifier();
612            if (qualifier != null) {
613                final boolean enclose;
614                if (qualifier instanceof JsLiteral.JsValueLiteral) {
615                    // "42.foo" is not allowed, but "(42).foo" is.
616                    enclose = qualifier instanceof JsNumberLiteral;
617                }
618                else {
619                    enclose = parenCalc(nameRef, qualifier, false);
620                }
621    
622                if (enclose) {
623                    leftParen();
624                }
625                accept(qualifier);
626                if (enclose) {
627                    rightParen();
628                }
629                p.print('.');
630            }
631    
632            p.maybeIndent();
633            beforeNodePrinted(nameRef);
634            p.print(nameRef.getIdent());
635        }
636    
637        protected void beforeNodePrinted(JsNode node) {
638        }
639    
640        @Override
641        public void visitNew(JsNew x) {
642            p.print(CHARS_NEW);
643            space();
644    
645            JsExpression constructorExpression = x.getConstructorExpression();
646            boolean needsParens = JsConstructExpressionVisitor.exec(constructorExpression);
647            if (needsParens) {
648                leftParen();
649            }
650            accept(constructorExpression);
651            if (needsParens) {
652                rightParen();
653            }
654    
655            leftParen();
656            printExpressions(x.getArguments());
657            rightParen();
658        }
659    
660        @Override
661        public void visitNull(JsNullLiteral x) {
662            p.print(CHARS_NULL);
663        }
664    
665        @Override
666        public void visitInt(JsIntLiteral x) {
667            p.print(x.value);
668        }
669    
670        @Override
671        public void visitDouble(JsDoubleLiteral x) {
672            p.print(x.value);
673        }
674    
675        @Override
676        public void visitObjectLiteral(JsObjectLiteral objectLiteral) {
677            p.print('{');
678            if (objectLiteral.isMultiline()) {
679                p.indentIn();
680            }
681    
682            boolean notFirst = false;
683            for (JsPropertyInitializer item : objectLiteral.getPropertyInitializers()) {
684                if (notFirst) {
685                    p.print(',');
686                }
687    
688                if (objectLiteral.isMultiline()) {
689                    newlineOpt();
690                }
691                else if (notFirst) {
692                    spaceOpt();
693                }
694    
695                notFirst = true;
696    
697                JsExpression labelExpr = item.getLabelExpr();
698                // labels can be either string, integral, or decimal literals
699                if (labelExpr instanceof JsNameRef) {
700                    p.print(((JsNameRef) labelExpr).getIdent());
701                }
702                else if (labelExpr instanceof JsStringLiteral) {
703                    p.print(((JsStringLiteral) labelExpr).getValue());
704                }
705                else {
706                    accept(labelExpr);
707                }
708    
709                _colon();
710                space();
711                JsExpression valueExpr = item.getValueExpr();
712                boolean wasEnclosed = parenPushIfCommaExpression(valueExpr);
713                accept(valueExpr);
714                if (wasEnclosed) {
715                    rightParen();
716                }
717            }
718    
719            if (objectLiteral.isMultiline()) {
720                p.indentOut();
721                newlineOpt();
722            }
723    
724            p.print('}');
725        }
726    
727        @Override
728        public void visitParameter(JsParameter x) {
729            nameOf(x);
730        }
731    
732        @Override
733        public void visitPostfixOperation(JsPostfixOperation x) {
734            JsUnaryOperator op = x.getOperator();
735            JsExpression arg = x.getArg();
736            // unary operators always associate correctly (I think)
737            printPair(x, arg);
738            p.print(op.getSymbol());
739        }
740    
741        @Override
742        public void visitPrefixOperation(JsPrefixOperation x) {
743            JsUnaryOperator op = x.getOperator();
744            p.print(op.getSymbol());
745            JsExpression arg = x.getArg();
746            if (spaceCalc(op, arg)) {
747                space();
748            }
749            // unary operators always associate correctly (I think)
750            printPair(x, arg);
751        }
752    
753        @Override
754        public void visitProgram(JsProgram x) {
755            p.print("<JsProgram>");
756        }
757    
758        @Override
759        public void visitProgramFragment(JsProgramFragment x) {
760            p.print("<JsProgramFragment>");
761        }
762    
763        @Override
764        public void visitRegExp(JsRegExp x) {
765            slash();
766            p.print(x.getPattern());
767            slash();
768            String flags = x.getFlags();
769            if (flags != null) {
770                p.print(flags);
771            }
772        }
773    
774        @Override
775        public void visitReturn(JsReturn x) {
776            p.print(CHARS_RETURN);
777            JsExpression expr = x.getExpression();
778            if (expr != null) {
779                space();
780                accept(expr);
781            }
782        }
783    
784        @Override
785        public void visitString(JsStringLiteral x) {
786            p.print(javaScriptString(x.getValue()));
787        }
788    
789        @Override
790        public void visit(JsSwitch x) {
791            p.print(CHARS_SWITCH);
792            spaceOpt();
793            leftParen();
794            accept(x.getExpression());
795            rightParen();
796            spaceOpt();
797            blockOpen();
798            acceptList(x.getCases());
799            blockClose();
800        }
801    
802        @Override
803        public void visitThis(JsLiteral.JsThisRef x) {
804            p.print(CHARS_THIS);
805        }
806    
807        @Override
808        public void visitThrow(JsThrow x) {
809            p.print(CHARS_THROW);
810            space();
811            accept(x.getExpression());
812        }
813    
814        @Override
815        public void visitTry(JsTry x) {
816            p.print(CHARS_TRY);
817            spaceOpt();
818            accept(x.getTryBlock());
819    
820            acceptList(x.getCatches());
821    
822            JsBlock finallyBlock = x.getFinallyBlock();
823            if (finallyBlock != null) {
824                p.print(CHARS_FINALLY);
825                spaceOpt();
826                accept(finallyBlock);
827            }
828        }
829    
830        @Override
831        public void visit(JsVar var) {
832            nameOf(var);
833            JsExpression initExpr = var.getInitExpression();
834            if (initExpr != null) {
835                spaceOpt();
836                assignment();
837                spaceOpt();
838                boolean isEnclosed = parenPushIfCommaExpression(initExpr);
839                accept(initExpr);
840                if (isEnclosed) {
841                    rightParen();
842                }
843            }
844        }
845    
846        @Override
847        public void visitVars(JsVars vars) {
848            var();
849            space();
850            boolean sep = false;
851            for (JsVar var : vars) {
852                if (sep) {
853                    if (vars.isMultiline()) {
854                        newlineOpt();
855                    }
856                    p.print(',');
857                    spaceOpt();
858                }
859                else {
860                    sep = true;
861                }
862    
863                accept(var);
864            }
865        }
866    
867        @Override
868        public void visitDocComment(JsDocComment comment) {
869            boolean asSingleLine = comment.getTags().size() == 1;
870            if (!asSingleLine) {
871                newlineOpt();
872            }
873            p.print("/**");
874            if (asSingleLine) {
875                space();
876            }
877            else {
878                p.newline();
879            }
880    
881            boolean notFirst = false;
882            for (Map.Entry<String, Object> entry : comment.getTags().entrySet()) {
883                if (notFirst) {
884                    p.newline();
885                    p.print(' ');
886                    p.print('*');
887                }
888                else {
889                    notFirst = true;
890                }
891    
892                p.print('@');
893                p.print(entry.getKey());
894                Object value = entry.getValue();
895                if (value != null) {
896                    space();
897                    if (value instanceof CharSequence) {
898                        p.print((CharSequence) value);
899                    }
900                    else {
901                        visitNameRef((JsNameRef) value);
902                    }
903                }
904    
905                if (!asSingleLine) {
906                    p.newline();
907                }
908            }
909    
910            if (asSingleLine) {
911                space();
912            }
913            else {
914                newlineOpt();
915            }
916    
917            p.print('*');
918            p.print('/');
919            if (asSingleLine) {
920                spaceOpt();
921            }
922        }
923    
924        protected final void newlineOpt() {
925            if (!p.isCompact()) {
926                p.newline();
927            }
928        }
929    
930        protected void printJsBlock(JsBlock x, boolean truncate, boolean finalNewline) {
931            if (!lineBreakAfterBlock) {
932                finalNewline = false;
933                lineBreakAfterBlock = true;
934            }
935    
936            boolean needBraces = !x.isGlobalBlock();
937            if (needBraces) {
938                blockOpen();
939            }
940    
941            int count = 0;
942            Iterator<JsStatement> iterator = x.getStatements().iterator();
943            while (iterator.hasNext()) {
944                boolean isGlobal = x.isGlobalBlock() || globalBlocks.contains(x);
945    
946                if (truncate && count > JSBLOCK_LINES_TO_PRINT) {
947                    p.print("[...]");
948                    newlineOpt();
949                    break;
950                }
951                JsStatement statement = iterator.next();
952                if (statement instanceof JsEmpty) {
953                    continue;
954                }
955    
956                needSemi = true;
957                boolean stmtIsGlobalBlock = false;
958                if (isGlobal) {
959                    if (statement instanceof JsBlock) {
960                        // A block inside a global block is still considered global
961                        stmtIsGlobalBlock = true;
962                        globalBlocks.add((JsBlock) statement);
963                    }
964                }
965    
966                accept(statement);
967                if (stmtIsGlobalBlock) {
968                    //noinspection SuspiciousMethodCalls
969                    globalBlocks.remove(statement);
970                }
971                if (needSemi) {
972                    /*
973                    * Special treatment of function declarations: If they are the only item in a
974                    * statement (i.e. not part of an assignment operation), just give them
975                    * a newline instead of a semi.
976                    */
977                    boolean functionStmt =
978                            statement instanceof JsExpressionStatement && ((JsExpressionStatement) statement).getExpression() instanceof JsFunction;
979                    /*
980                    * Special treatment of the last statement in a block: only a few
981                    * statements at the end of a block require semicolons.
982                    */
983                    boolean lastStatement = !iterator.hasNext() && needBraces && !JsRequiresSemiVisitor.exec(statement);
984                    if (functionStmt) {
985                        if (lastStatement) {
986                            newlineOpt();
987                        }
988                        else {
989                            p.newline();
990                        }
991                    }
992                    else {
993                        if (lastStatement) {
994                            p.printOpt(';');
995                        }
996                        else {
997                            semi();
998                        }
999                        newlineOpt();
1000                    }
1001                }
1002                ++count;
1003            }
1004    
1005            if (needBraces) {
1006                // _blockClose() modified
1007                p.indentOut();
1008                p.print('}');
1009                if (finalNewline) {
1010                    newlineOpt();
1011                }
1012            }
1013            needSemi = false;
1014        }
1015    
1016        private void assignment() {
1017            p.print('=');
1018        }
1019    
1020        private void blockClose() {
1021            p.indentOut();
1022            p.print('}');
1023            newlineOpt();
1024        }
1025    
1026        private void blockOpen() {
1027            p.print('{');
1028            p.indentIn();
1029            newlineOpt();
1030        }
1031    
1032        private void _colon() {
1033            p.print(':');
1034        }
1035    
1036        private void _for() {
1037            p.print(CHARS_FOR);
1038        }
1039    
1040        private void _if() {
1041            p.print(CHARS_IF);
1042        }
1043    
1044        private void leftParen() {
1045            p.print('(');
1046        }
1047    
1048        private void leftSquare() {
1049            p.print('[');
1050        }
1051    
1052        private void nameDef(JsName name) {
1053            p.print(name.getIdent());
1054        }
1055    
1056        private void nameOf(HasName hasName) {
1057            nameDef(hasName.getName());
1058        }
1059    
1060        private boolean nestedPop(JsStatement statement) {
1061            boolean pop = !(statement instanceof JsBlock);
1062            if (pop) {
1063                p.indentOut();
1064            }
1065            return pop;
1066        }
1067    
1068        private boolean nestedPush(JsStatement statement) {
1069            boolean push = !(statement instanceof JsBlock);
1070            if (push) {
1071                newlineOpt();
1072                p.indentIn();
1073            }
1074            else {
1075                spaceOpt();
1076            }
1077            return push;
1078        }
1079    
1080        private static boolean parenCalc(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1081            int parentPrec = JsPrecedenceVisitor.exec(parent);
1082            int childPrec = JsPrecedenceVisitor.exec(child);
1083            return parentPrec > childPrec || parentPrec == childPrec && wrongAssoc;
1084        }
1085    
1086        private boolean _parenPopOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1087            boolean doPop = parenCalc(parent, child, wrongAssoc);
1088            if (doPop) {
1089                rightParen();
1090            }
1091            else {
1092                space();
1093            }
1094            return doPop;
1095        }
1096    
1097        private boolean parenPush(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1098            boolean doPush = parenCalc(parent, child, wrongAssoc);
1099            if (doPush) {
1100                leftParen();
1101            }
1102            return doPush;
1103        }
1104    
1105        private boolean parenPushIfCommaExpression(JsExpression x) {
1106            boolean doPush = x instanceof JsBinaryOperation && ((JsBinaryOperation) x).getOperator() == JsBinaryOperator.COMMA;
1107            if (doPush) {
1108                leftParen();
1109            }
1110            return doPush;
1111        }
1112    
1113        private boolean _parenPushOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1114            boolean doPush = parenCalc(parent, child, wrongAssoc);
1115            if (doPush) {
1116                leftParen();
1117            }
1118            else {
1119                space();
1120            }
1121            return doPush;
1122        }
1123    
1124        private void rightParen() {
1125            p.print(')');
1126        }
1127    
1128        private void rightSquare() {
1129            p.print(']');
1130        }
1131    
1132        private void semi() {
1133            p.print(';');
1134        }
1135    
1136        private boolean sepCommaOptSpace(boolean sep) {
1137            if (sep) {
1138                p.print(',');
1139                spaceOpt();
1140            }
1141            return true;
1142        }
1143    
1144        private void slash() {
1145            p.print('/');
1146        }
1147    
1148        private void space() {
1149            p.print(' ');
1150        }
1151    
1152        /**
1153         * Decide whether, if <code>op</code> is printed followed by <code>arg</code>,
1154         * there needs to be a space between the operator and expression.
1155         *
1156         * @return <code>true</code> if a space needs to be printed
1157         */
1158        private static boolean spaceCalc(JsOperator op, JsExpression arg) {
1159            if (op.isKeyword()) {
1160                return true;
1161            }
1162            if (arg instanceof JsBinaryOperation) {
1163                JsBinaryOperation binary = (JsBinaryOperation) arg;
1164                /*
1165                * If the binary operation has a higher precedence than op, then it won't
1166                * be parenthesized, so check the first argument of the binary operation.
1167                */
1168                return binary.getOperator().getPrecedence() > op.getPrecedence() && spaceCalc(op, binary.getArg1());
1169            }
1170            if (arg instanceof JsPrefixOperation) {
1171                JsOperator op2 = ((JsPrefixOperation) arg).getOperator();
1172                return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
1173                       && (op2 == JsUnaryOperator.DEC || op2 == JsUnaryOperator.NEG)
1174                       || (op == JsBinaryOperator.ADD && op2 == JsUnaryOperator.INC);
1175            }
1176            if (arg instanceof JsNumberLiteral && (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)) {
1177                if (arg instanceof JsIntLiteral) {
1178                    return ((JsIntLiteral) arg).value < 0;
1179                }
1180                else {
1181                    assert arg instanceof JsDoubleLiteral;
1182                    //noinspection CastConflictsWithInstanceof
1183                    return ((JsDoubleLiteral) arg).value < 0;
1184                }
1185            }
1186            return false;
1187        }
1188    
1189        private void spaceOpt() {
1190            p.printOpt(' ');
1191        }
1192    
1193        private void var() {
1194            p.print(CHARS_VAR);
1195        }
1196    
1197        private void _while() {
1198            p.print(CHARS_WHILE);
1199        }
1200    }