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