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