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