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