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