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