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            nestedPush(thenStmt);
562            accept(thenStmt);
563            nestedPop(thenStmt);
564            JsStatement elseStatement = x.getElseStatement();
565            if (elseStatement != null) {
566                if (needSemi) {
567                    semi();
568                    newlineOpt();
569                }
570                else {
571                    spaceOpt();
572                    needSemi = true;
573                }
574                p.print(CHARS_ELSE);
575                boolean elseIf = elseStatement instanceof JsIf;
576                if (!elseIf) {
577                    nestedPush(elseStatement);
578                }
579                else {
580                    space();
581                }
582                accept(elseStatement);
583                if (!elseIf) {
584                    nestedPop(elseStatement);
585                }
586            }
587        }
588    
589        @Override
590        public void visitInvocation(JsInvocation invocation) {
591            printPair(invocation, invocation.getQualifier());
592    
593            leftParen();
594            printExpressions(invocation.getArguments());
595            rightParen();
596        }
597    
598        @Override
599        public void visitLabel(JsLabel x) {
600            nameOf(x);
601            _colon();
602            spaceOpt();
603            accept(x.getStatement());
604        }
605    
606        @Override
607        public void visitNameRef(JsNameRef nameRef) {
608            JsExpression qualifier = nameRef.getQualifier();
609            if (qualifier != null) {
610                final boolean enclose;
611                if (qualifier instanceof JsLiteral.JsValueLiteral) {
612                    // "42.foo" is not allowed, but "(42).foo" is.
613                    enclose = qualifier instanceof JsNumberLiteral;
614                }
615                else {
616                    enclose = parenCalc(nameRef, qualifier, false);
617                }
618    
619                if (enclose) {
620                    leftParen();
621                }
622                accept(qualifier);
623                if (enclose) {
624                    rightParen();
625                }
626                p.print('.');
627            }
628    
629            p.maybeIndent();
630            beforeNodePrinted(nameRef);
631            p.print(nameRef.getIdent());
632        }
633    
634        protected void beforeNodePrinted(JsNode node) {
635        }
636    
637        @Override
638        public void visitNew(JsNew x) {
639            p.print(CHARS_NEW);
640            space();
641    
642            JsExpression constructorExpression = x.getConstructorExpression();
643            boolean needsParens = JsConstructExpressionVisitor.exec(constructorExpression);
644            if (needsParens) {
645                leftParen();
646            }
647            accept(constructorExpression);
648            if (needsParens) {
649                rightParen();
650            }
651    
652            leftParen();
653            printExpressions(x.getArguments());
654            rightParen();
655        }
656    
657        @Override
658        public void visitNull(JsNullLiteral x) {
659            p.print(CHARS_NULL);
660        }
661    
662        @Override
663        public void visitInt(JsIntLiteral x) {
664            p.print(x.value);
665        }
666    
667        @Override
668        public void visitDouble(JsDoubleLiteral x) {
669            p.print(x.value);
670        }
671    
672        @Override
673        public void visitObjectLiteral(JsObjectLiteral objectLiteral) {
674            p.print('{');
675            if (objectLiteral.isMultiline()) {
676                p.indentIn();
677            }
678    
679            boolean notFirst = false;
680            for (JsPropertyInitializer item : objectLiteral.getPropertyInitializers()) {
681                if (notFirst) {
682                    p.print(',');
683                }
684    
685                if (objectLiteral.isMultiline()) {
686                    newlineOpt();
687                }
688                else if (notFirst) {
689                    spaceOpt();
690                }
691    
692                notFirst = true;
693    
694                JsExpression labelExpr = item.getLabelExpr();
695                // labels can be either string, integral, or decimal literals
696                if (labelExpr instanceof JsNameRef) {
697                    p.print(((JsNameRef) labelExpr).getIdent());
698                }
699                else if (labelExpr instanceof JsStringLiteral) {
700                    p.print(((JsStringLiteral) labelExpr).getValue());
701                }
702                else {
703                    accept(labelExpr);
704                }
705    
706                _colon();
707                space();
708                JsExpression valueExpr = item.getValueExpr();
709                boolean wasEnclosed = parenPushIfCommaExpression(valueExpr);
710                accept(valueExpr);
711                if (wasEnclosed) {
712                    rightParen();
713                }
714            }
715    
716            if (objectLiteral.isMultiline()) {
717                p.indentOut();
718                newlineOpt();
719            }
720    
721            p.print('}');
722        }
723    
724        @Override
725        public void visitParameter(JsParameter x) {
726            nameOf(x);
727        }
728    
729        @Override
730        public void visitPostfixOperation(JsPostfixOperation x) {
731            JsUnaryOperator op = x.getOperator();
732            JsExpression arg = x.getArg();
733            // unary operators always associate correctly (I think)
734            printPair(x, arg);
735            p.print(op.getSymbol());
736        }
737    
738        @Override
739        public void visitPrefixOperation(JsPrefixOperation x) {
740            JsUnaryOperator op = x.getOperator();
741            p.print(op.getSymbol());
742            JsExpression arg = x.getArg();
743            if (spaceCalc(op, arg)) {
744                space();
745            }
746            // unary operators always associate correctly (I think)
747            printPair(x, arg);
748        }
749    
750        @Override
751        public void visitProgram(JsProgram x) {
752            p.print("<JsProgram>");
753        }
754    
755        @Override
756        public void visitProgramFragment(JsProgramFragment x) {
757            p.print("<JsProgramFragment>");
758        }
759    
760        @Override
761        public void visitRegExp(JsRegExp x) {
762            slash();
763            p.print(x.getPattern());
764            slash();
765            String flags = x.getFlags();
766            if (flags != null) {
767                p.print(flags);
768            }
769        }
770    
771        @Override
772        public void visitReturn(JsReturn x) {
773            p.print(CHARS_RETURN);
774            JsExpression expr = x.getExpression();
775            if (expr != null) {
776                space();
777                accept(expr);
778            }
779        }
780    
781        @Override
782        public void visitString(JsStringLiteral x) {
783            p.print(javaScriptString(x.getValue()));
784        }
785    
786        @Override
787        public void visit(JsSwitch x) {
788            p.print(CHARS_SWITCH);
789            spaceOpt();
790            leftParen();
791            accept(x.getExpression());
792            rightParen();
793            spaceOpt();
794            blockOpen();
795            acceptList(x.getCases());
796            blockClose();
797        }
798    
799        @Override
800        public void visitThis(JsLiteral.JsThisRef x) {
801            p.print(CHARS_THIS);
802        }
803    
804        @Override
805        public void visitThrow(JsThrow x) {
806            p.print(CHARS_THROW);
807            space();
808            accept(x.getExpression());
809        }
810    
811        @Override
812        public void visitTry(JsTry x) {
813            p.print(CHARS_TRY);
814            spaceOpt();
815            accept(x.getTryBlock());
816    
817            acceptList(x.getCatches());
818    
819            JsBlock finallyBlock = x.getFinallyBlock();
820            if (finallyBlock != null) {
821                p.print(CHARS_FINALLY);
822                spaceOpt();
823                accept(finallyBlock);
824            }
825        }
826    
827        @Override
828        public void visit(JsVar var) {
829            nameOf(var);
830            JsExpression initExpr = var.getInitExpression();
831            if (initExpr != null) {
832                spaceOpt();
833                assignment();
834                spaceOpt();
835                boolean isEnclosed = parenPushIfCommaExpression(initExpr);
836                accept(initExpr);
837                if (isEnclosed) {
838                    rightParen();
839                }
840            }
841        }
842    
843        @Override
844        public void visitVars(JsVars vars) {
845            var();
846            space();
847            boolean sep = false;
848            for (JsVar var : vars) {
849                if (sep) {
850                    if (vars.isMultiline()) {
851                        newlineOpt();
852                    }
853                    p.print(',');
854                    spaceOpt();
855                }
856                else {
857                    sep = true;
858                }
859    
860                accept(var);
861            }
862        }
863    
864        @Override
865        public void visitDocComment(JsDocComment comment) {
866            boolean asSingleLine = comment.getTags().size() == 1;
867            if (!asSingleLine) {
868                newlineOpt();
869            }
870            p.print("/**");
871            if (asSingleLine) {
872                space();
873            }
874            else {
875                p.newline();
876            }
877    
878            boolean notFirst = false;
879            for (Map.Entry<String, Object> entry : comment.getTags().entrySet()) {
880                if (notFirst) {
881                    p.newline();
882                    p.print(' ');
883                    p.print('*');
884                }
885                else {
886                    notFirst = true;
887                }
888    
889                p.print('@');
890                p.print(entry.getKey());
891                Object value = entry.getValue();
892                if (value != null) {
893                    space();
894                    if (value instanceof CharSequence) {
895                        p.print((CharSequence) value);
896                    }
897                    else {
898                        visitNameRef((JsNameRef) value);
899                    }
900                }
901    
902                if (!asSingleLine) {
903                    p.newline();
904                }
905            }
906    
907            if (asSingleLine) {
908                space();
909            }
910            else {
911                newlineOpt();
912            }
913    
914            p.print('*');
915            p.print('/');
916            if (asSingleLine) {
917                spaceOpt();
918            }
919        }
920    
921        protected final void newlineOpt() {
922            if (!p.isCompact()) {
923                p.newline();
924            }
925        }
926    
927        protected void printJsBlock(JsBlock x, boolean truncate, boolean finalNewline) {
928            if (!lineBreakAfterBlock) {
929                finalNewline = false;
930                lineBreakAfterBlock = true;
931            }
932    
933            boolean needBraces = !x.isGlobalBlock();
934            if (needBraces) {
935                blockOpen();
936            }
937    
938            int count = 0;
939            Iterator<JsStatement> iterator = x.getStatements().iterator();
940            while (iterator.hasNext()) {
941                boolean isGlobal = x.isGlobalBlock() || globalBlocks.contains(x);
942    
943                if (truncate && count > JSBLOCK_LINES_TO_PRINT) {
944                    p.print("[...]");
945                    newlineOpt();
946                    break;
947                }
948                JsStatement statement = iterator.next();
949                if (statement instanceof JsEmpty) {
950                    continue;
951                }
952    
953                needSemi = true;
954                boolean stmtIsGlobalBlock = false;
955                if (isGlobal) {
956                    if (statement instanceof JsBlock) {
957                        // A block inside a global block is still considered global
958                        stmtIsGlobalBlock = true;
959                        globalBlocks.add((JsBlock) statement);
960                    }
961                }
962    
963                accept(statement);
964                if (stmtIsGlobalBlock) {
965                    //noinspection SuspiciousMethodCalls
966                    globalBlocks.remove(statement);
967                }
968                if (needSemi) {
969                    /*
970                    * Special treatment of function declarations: If they are the only item in a
971                    * statement (i.e. not part of an assignment operation), just give them
972                    * a newline instead of a semi.
973                    */
974                    boolean functionStmt =
975                            statement instanceof JsExpressionStatement && ((JsExpressionStatement) statement).getExpression() instanceof JsFunction;
976                    /*
977                    * Special treatment of the last statement in a block: only a few
978                    * statements at the end of a block require semicolons.
979                    */
980                    boolean lastStatement = !iterator.hasNext() && needBraces && !JsRequiresSemiVisitor.exec(statement);
981                    if (functionStmt) {
982                        if (lastStatement) {
983                            newlineOpt();
984                        }
985                        else {
986                            p.newline();
987                        }
988                    }
989                    else {
990                        if (lastStatement) {
991                            p.printOpt(';');
992                        }
993                        else {
994                            semi();
995                        }
996                        newlineOpt();
997                    }
998                }
999                ++count;
1000            }
1001    
1002            if (needBraces) {
1003                // _blockClose() modified
1004                p.indentOut();
1005                p.print('}');
1006                if (finalNewline) {
1007                    newlineOpt();
1008                }
1009            }
1010            needSemi = false;
1011        }
1012    
1013        private void assignment() {
1014            p.print('=');
1015        }
1016    
1017        private void blockClose() {
1018            p.indentOut();
1019            p.print('}');
1020            newlineOpt();
1021        }
1022    
1023        private void blockOpen() {
1024            p.print('{');
1025            p.indentIn();
1026            newlineOpt();
1027        }
1028    
1029        private void _colon() {
1030            p.print(':');
1031        }
1032    
1033        private void _for() {
1034            p.print(CHARS_FOR);
1035        }
1036    
1037        private void _if() {
1038            p.print(CHARS_IF);
1039        }
1040    
1041        private void leftParen() {
1042            p.print('(');
1043        }
1044    
1045        private void leftSquare() {
1046            p.print('[');
1047        }
1048    
1049        private void nameDef(JsName name) {
1050            p.print(name.getIdent());
1051        }
1052    
1053        private void nameOf(HasName hasName) {
1054            nameDef(hasName.getName());
1055        }
1056    
1057        private boolean nestedPop(JsStatement statement) {
1058            boolean pop = !(statement instanceof JsBlock);
1059            if (pop) {
1060                p.indentOut();
1061            }
1062            return pop;
1063        }
1064    
1065        private boolean nestedPush(JsStatement statement) {
1066            boolean push = !(statement instanceof JsBlock);
1067            if (push) {
1068                newlineOpt();
1069                p.indentIn();
1070            }
1071            else {
1072                spaceOpt();
1073            }
1074            return push;
1075        }
1076    
1077        private static boolean parenCalc(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1078            int parentPrec = JsPrecedenceVisitor.exec(parent);
1079            int childPrec = JsPrecedenceVisitor.exec(child);
1080            return parentPrec > childPrec || parentPrec == childPrec && wrongAssoc;
1081        }
1082    
1083        private boolean _parenPopOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1084            boolean doPop = parenCalc(parent, child, wrongAssoc);
1085            if (doPop) {
1086                rightParen();
1087            }
1088            else {
1089                space();
1090            }
1091            return doPop;
1092        }
1093    
1094        private boolean parenPush(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1095            boolean doPush = parenCalc(parent, child, wrongAssoc);
1096            if (doPush) {
1097                leftParen();
1098            }
1099            return doPush;
1100        }
1101    
1102        private boolean parenPushIfCommaExpression(JsExpression x) {
1103            boolean doPush = x instanceof JsBinaryOperation && ((JsBinaryOperation) x).getOperator() == JsBinaryOperator.COMMA;
1104            if (doPush) {
1105                leftParen();
1106            }
1107            return doPush;
1108        }
1109    
1110        private boolean _parenPushOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1111            boolean doPush = parenCalc(parent, child, wrongAssoc);
1112            if (doPush) {
1113                leftParen();
1114            }
1115            else {
1116                space();
1117            }
1118            return doPush;
1119        }
1120    
1121        private void rightParen() {
1122            p.print(')');
1123        }
1124    
1125        private void rightSquare() {
1126            p.print(']');
1127        }
1128    
1129        private void semi() {
1130            p.print(';');
1131        }
1132    
1133        private boolean sepCommaOptSpace(boolean sep) {
1134            if (sep) {
1135                p.print(',');
1136                spaceOpt();
1137            }
1138            return true;
1139        }
1140    
1141        private void slash() {
1142            p.print('/');
1143        }
1144    
1145        private void space() {
1146            p.print(' ');
1147        }
1148    
1149        /**
1150         * Decide whether, if <code>op</code> is printed followed by <code>arg</code>,
1151         * there needs to be a space between the operator and expression.
1152         *
1153         * @return <code>true</code> if a space needs to be printed
1154         */
1155        private static boolean spaceCalc(JsOperator op, JsExpression arg) {
1156            if (op.isKeyword()) {
1157                return true;
1158            }
1159            if (arg instanceof JsBinaryOperation) {
1160                JsBinaryOperation binary = (JsBinaryOperation) arg;
1161                /*
1162                * If the binary operation has a higher precedence than op, then it won't
1163                * be parenthesized, so check the first argument of the binary operation.
1164                */
1165                return binary.getOperator().getPrecedence() > op.getPrecedence() && spaceCalc(op, binary.getArg1());
1166            }
1167            if (arg instanceof JsPrefixOperation) {
1168                JsOperator op2 = ((JsPrefixOperation) arg).getOperator();
1169                return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
1170                       && (op2 == JsUnaryOperator.DEC || op2 == JsUnaryOperator.NEG)
1171                       || (op == JsBinaryOperator.ADD && op2 == JsUnaryOperator.INC);
1172            }
1173            if (arg instanceof JsNumberLiteral && (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)) {
1174                if (arg instanceof JsIntLiteral) {
1175                    return ((JsIntLiteral) arg).value < 0;
1176                }
1177                else {
1178                    assert arg instanceof JsDoubleLiteral;
1179                    //noinspection CastConflictsWithInstanceof
1180                    return ((JsDoubleLiteral) arg).value < 0;
1181                }
1182            }
1183            return false;
1184        }
1185    
1186        private void spaceOpt() {
1187            p.printOpt(' ');
1188        }
1189    
1190        private void var() {
1191            p.print(CHARS_VAR);
1192        }
1193    
1194        private void _while() {
1195            p.print(CHARS_WHILE);
1196        }
1197    }