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