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