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><script></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 }