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());
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) {
380 boolean isNeedParen = parenCalc(parent, expression, false);
381 if (isNeedParen) {
382 leftParen();
383 }
384 accept(expression);
385 if (isNeedParen) {
386 rightParen();
387 }
388 }
389
390 @Override
391 public void visitDebugger(JsDebugger x) {
392 p.print(CHARS_DEBUGGER);
393 }
394
395 @Override
396 public void visitDefault(JsDefault x) {
397 p.print(CHARS_DEFAULT);
398 _colon();
399
400 printSwitchMemberStatements(x);
401 }
402
403 @Override
404 public void visitWhile(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(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(JsEmpty x) {
438 }
439
440 @Override
441 public void visitExpressionStatement(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(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(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(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(JsIf x) {
551 _if();
552 spaceOpt();
553 leftParen();
554 accept(x.getIfExpression());
555 rightParen();
556 JsStatement thenStmt = x.getThenStatement();
557 nestedPush(thenStmt);
558 accept(thenStmt);
559 nestedPop(thenStmt);
560 JsStatement elseStatement = x.getElseStatement();
561 if (elseStatement != null) {
562 if (needSemi) {
563 semi();
564 newlineOpt();
565 }
566 else {
567 spaceOpt();
568 needSemi = true;
569 }
570 p.print(CHARS_ELSE);
571 boolean elseIf = elseStatement instanceof JsIf;
572 if (!elseIf) {
573 nestedPush(elseStatement);
574 }
575 else {
576 space();
577 }
578 accept(elseStatement);
579 if (!elseIf) {
580 nestedPop(elseStatement);
581 }
582 }
583 }
584
585 @Override
586 public void visitInvocation(JsInvocation invocation) {
587 printPair(invocation, invocation.getQualifier());
588
589 leftParen();
590 printExpressions(invocation.getArguments());
591 rightParen();
592 }
593
594 @Override
595 public void visitLabel(JsLabel x) {
596 nameOf(x);
597 _colon();
598 spaceOpt();
599 accept(x.getStatement());
600 }
601
602 @Override
603 public void visitNameRef(JsNameRef nameRef) {
604 JsExpression qualifier = nameRef.getQualifier();
605 if (qualifier != null) {
606 final boolean enclose;
607 if (qualifier instanceof JsLiteral.JsValueLiteral) {
608 // "42.foo" is not allowed, but "(42).foo" is.
609 enclose = qualifier instanceof JsNumberLiteral;
610 }
611 else {
612 enclose = parenCalc(nameRef, qualifier, false);
613 }
614
615 if (enclose) {
616 leftParen();
617 }
618 accept(qualifier);
619 if (enclose) {
620 rightParen();
621 }
622 p.print('.');
623 }
624
625 p.maybeIndent();
626 beforeNodePrinted(nameRef);
627 p.print(nameRef.getIdent());
628 }
629
630 protected void beforeNodePrinted(JsNode node) {
631 }
632
633 @Override
634 public void visitNew(JsNew x) {
635 p.print(CHARS_NEW);
636 space();
637
638 JsExpression constructorExpression = x.getConstructorExpression();
639 boolean needsParens = JsConstructExpressionVisitor.exec(constructorExpression);
640 if (needsParens) {
641 leftParen();
642 }
643 accept(constructorExpression);
644 if (needsParens) {
645 rightParen();
646 }
647
648 leftParen();
649 printExpressions(x.getArguments());
650 rightParen();
651 }
652
653 @Override
654 public void visitNull(JsNullLiteral x) {
655 p.print(CHARS_NULL);
656 }
657
658 @Override
659 public void visitInt(JsIntLiteral x) {
660 p.print(x.value);
661 }
662
663 @Override
664 public void visitDouble(JsDoubleLiteral x) {
665 p.print(x.value);
666 }
667
668 @Override
669 public void visitObjectLiteral(JsObjectLiteral objectLiteral) {
670 p.print('{');
671 if (objectLiteral.isMultiline()) {
672 p.indentIn();
673 }
674
675 boolean notFirst = false;
676 for (JsPropertyInitializer item : objectLiteral.getPropertyInitializers()) {
677 if (notFirst) {
678 p.print(',');
679 }
680
681 if (objectLiteral.isMultiline()) {
682 newlineOpt();
683 }
684 else if (notFirst) {
685 spaceOpt();
686 }
687
688 notFirst = true;
689
690 JsExpression labelExpr = item.getLabelExpr();
691 // labels can be either string, integral, or decimal literals
692 if (labelExpr instanceof JsNameRef) {
693 p.print(((JsNameRef) labelExpr).getIdent());
694 }
695 else if (labelExpr instanceof JsStringLiteral) {
696 p.print(((JsStringLiteral) labelExpr).getValue());
697 }
698 else {
699 accept(labelExpr);
700 }
701
702 _colon();
703 space();
704 JsExpression valueExpr = item.getValueExpr();
705 boolean wasEnclosed = parenPushIfCommaExpression(valueExpr);
706 accept(valueExpr);
707 if (wasEnclosed) {
708 rightParen();
709 }
710 }
711
712 if (objectLiteral.isMultiline()) {
713 p.indentOut();
714 newlineOpt();
715 }
716
717 p.print('}');
718 }
719
720 @Override
721 public void visitParameter(JsParameter x) {
722 nameOf(x);
723 }
724
725 @Override
726 public void visitPostfixOperation(JsPostfixOperation x) {
727 JsUnaryOperator op = x.getOperator();
728 JsExpression arg = x.getArg();
729 // unary operators always associate correctly (I think)
730 printPair(x, arg);
731 p.print(op.getSymbol());
732 }
733
734 @Override
735 public void visitPrefixOperation(JsPrefixOperation x) {
736 JsUnaryOperator op = x.getOperator();
737 p.print(op.getSymbol());
738 JsExpression arg = x.getArg();
739 if (spaceCalc(op, arg)) {
740 space();
741 }
742 // unary operators always associate correctly (I think)
743 printPair(x, arg);
744 }
745
746 @Override
747 public void visitProgram(JsProgram x) {
748 p.print("<JsProgram>");
749 }
750
751 @Override
752 public void visitProgramFragment(JsProgramFragment x) {
753 p.print("<JsProgramFragment>");
754 }
755
756 @Override
757 public void visitRegExp(JsRegExp x) {
758 slash();
759 p.print(x.getPattern());
760 slash();
761 String flags = x.getFlags();
762 if (flags != null) {
763 p.print(flags);
764 }
765 }
766
767 @Override
768 public void visitReturn(JsReturn x) {
769 p.print(CHARS_RETURN);
770 JsExpression expr = x.getExpression();
771 if (expr != null) {
772 space();
773 accept(expr);
774 }
775 }
776
777 @Override
778 public void visitString(JsStringLiteral x) {
779 p.print(javaScriptString(x.getValue()));
780 }
781
782 @Override
783 public void visit(JsSwitch x) {
784 p.print(CHARS_SWITCH);
785 spaceOpt();
786 leftParen();
787 accept(x.getExpression());
788 rightParen();
789 spaceOpt();
790 blockOpen();
791 acceptList(x.getCases());
792 blockClose();
793 }
794
795 @Override
796 public void visitThis(JsLiteral.JsThisRef x) {
797 p.print(CHARS_THIS);
798 }
799
800 @Override
801 public void visitThrow(JsThrow x) {
802 p.print(CHARS_THROW);
803 space();
804 accept(x.getExpression());
805 }
806
807 @Override
808 public void visitTry(JsTry x) {
809 p.print(CHARS_TRY);
810 spaceOpt();
811 accept(x.getTryBlock());
812
813 acceptList(x.getCatches());
814
815 JsBlock finallyBlock = x.getFinallyBlock();
816 if (finallyBlock != null) {
817 p.print(CHARS_FINALLY);
818 spaceOpt();
819 accept(finallyBlock);
820 }
821 }
822
823 @Override
824 public void visit(JsVar var) {
825 nameOf(var);
826 JsExpression initExpr = var.getInitExpression();
827 if (initExpr != null) {
828 spaceOpt();
829 assignment();
830 spaceOpt();
831 boolean isEnclosed = parenPushIfCommaExpression(initExpr);
832 accept(initExpr);
833 if (isEnclosed) {
834 rightParen();
835 }
836 }
837 }
838
839 @Override
840 public void visitVars(JsVars vars) {
841 var();
842 space();
843 boolean sep = false;
844 for (JsVar var : vars) {
845 if (sep) {
846 if (vars.isMultiline()) {
847 newlineOpt();
848 }
849 p.print(',');
850 spaceOpt();
851 }
852 else {
853 sep = true;
854 }
855
856 accept(var);
857 }
858 }
859
860 @Override
861 public void visitDocComment(JsDocComment comment) {
862 boolean asSingleLine = comment.getTags().size() == 1;
863 if (!asSingleLine) {
864 newlineOpt();
865 }
866 p.print("/**");
867 if (asSingleLine) {
868 space();
869 }
870 else {
871 p.newline();
872 }
873
874 boolean notFirst = false;
875 for (Map.Entry<String, Object> entry : comment.getTags().entrySet()) {
876 if (notFirst) {
877 p.newline();
878 p.print(' ');
879 p.print('*');
880 }
881 else {
882 notFirst = true;
883 }
884
885 p.print('@');
886 p.print(entry.getKey());
887 Object value = entry.getValue();
888 if (value != null) {
889 space();
890 if (value instanceof CharSequence) {
891 p.print((CharSequence) value);
892 }
893 else {
894 visitNameRef((JsNameRef) value);
895 }
896 }
897
898 if (!asSingleLine) {
899 p.newline();
900 }
901 }
902
903 if (asSingleLine) {
904 space();
905 }
906 else {
907 newlineOpt();
908 }
909
910 p.print('*');
911 p.print('/');
912 if (asSingleLine) {
913 spaceOpt();
914 }
915 }
916
917 protected final void newlineOpt() {
918 if (!p.isCompact()) {
919 p.newline();
920 }
921 }
922
923 protected void printJsBlock(JsBlock x, boolean truncate, boolean finalNewline) {
924 if (!lineBreakAfterBlock) {
925 finalNewline = false;
926 lineBreakAfterBlock = true;
927 }
928
929 boolean needBraces = !x.isGlobalBlock();
930 if (needBraces) {
931 blockOpen();
932 }
933
934 int count = 0;
935 Iterator<JsStatement> iterator = x.getStatements().iterator();
936 while (iterator.hasNext()) {
937 boolean isGlobal = x.isGlobalBlock() || globalBlocks.contains(x);
938
939 if (truncate && count > JSBLOCK_LINES_TO_PRINT) {
940 p.print("[...]");
941 newlineOpt();
942 break;
943 }
944 JsStatement statement = iterator.next();
945 if (statement instanceof JsEmpty) {
946 continue;
947 }
948
949 needSemi = true;
950 boolean stmtIsGlobalBlock = false;
951 if (isGlobal) {
952 if (statement instanceof JsBlock) {
953 // A block inside a global block is still considered global
954 stmtIsGlobalBlock = true;
955 globalBlocks.add((JsBlock) statement);
956 }
957 }
958
959 accept(statement);
960 if (stmtIsGlobalBlock) {
961 //noinspection SuspiciousMethodCalls
962 globalBlocks.remove(statement);
963 }
964 if (needSemi) {
965 /*
966 * Special treatment of function declarations: If they are the only item in a
967 * statement (i.e. not part of an assignment operation), just give them
968 * a newline instead of a semi.
969 */
970 boolean functionStmt =
971 statement instanceof JsExpressionStatement && ((JsExpressionStatement) statement).getExpression() instanceof JsFunction;
972 /*
973 * Special treatment of the last statement in a block: only a few
974 * statements at the end of a block require semicolons.
975 */
976 boolean lastStatement = !iterator.hasNext() && needBraces && !JsRequiresSemiVisitor.exec(statement);
977 if (functionStmt) {
978 if (lastStatement) {
979 newlineOpt();
980 }
981 else {
982 p.newline();
983 }
984 }
985 else {
986 if (lastStatement) {
987 p.printOpt(';');
988 }
989 else {
990 semi();
991 }
992 newlineOpt();
993 }
994 }
995 ++count;
996 }
997
998 if (needBraces) {
999 // _blockClose() modified
1000 p.indentOut();
1001 p.print('}');
1002 if (finalNewline) {
1003 newlineOpt();
1004 }
1005 }
1006 needSemi = false;
1007 }
1008
1009 private void assignment() {
1010 p.print('=');
1011 }
1012
1013 private void blockClose() {
1014 p.indentOut();
1015 p.print('}');
1016 newlineOpt();
1017 }
1018
1019 private void blockOpen() {
1020 p.print('{');
1021 p.indentIn();
1022 newlineOpt();
1023 }
1024
1025 private void _colon() {
1026 p.print(':');
1027 }
1028
1029 private void _for() {
1030 p.print(CHARS_FOR);
1031 }
1032
1033 private void _if() {
1034 p.print(CHARS_IF);
1035 }
1036
1037 private void leftParen() {
1038 p.print('(');
1039 }
1040
1041 private void leftSquare() {
1042 p.print('[');
1043 }
1044
1045 private void nameDef(JsName name) {
1046 p.print(name.getIdent());
1047 }
1048
1049 private void nameOf(HasName hasName) {
1050 nameDef(hasName.getName());
1051 }
1052
1053 private boolean nestedPop(JsStatement statement) {
1054 boolean pop = !(statement instanceof JsBlock);
1055 if (pop) {
1056 p.indentOut();
1057 }
1058 return pop;
1059 }
1060
1061 private boolean nestedPush(JsStatement statement) {
1062 boolean push = !(statement instanceof JsBlock);
1063 if (push) {
1064 newlineOpt();
1065 p.indentIn();
1066 }
1067 else {
1068 spaceOpt();
1069 }
1070 return push;
1071 }
1072
1073 private static boolean parenCalc(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1074 int parentPrec = JsPrecedenceVisitor.exec(parent);
1075 int childPrec = JsPrecedenceVisitor.exec(child);
1076 return parentPrec > childPrec || parentPrec == childPrec && wrongAssoc;
1077 }
1078
1079 private boolean _parenPopOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1080 boolean doPop = parenCalc(parent, child, wrongAssoc);
1081 if (doPop) {
1082 rightParen();
1083 }
1084 else {
1085 space();
1086 }
1087 return doPop;
1088 }
1089
1090 private boolean parenPush(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1091 boolean doPush = parenCalc(parent, child, wrongAssoc);
1092 if (doPush) {
1093 leftParen();
1094 }
1095 return doPush;
1096 }
1097
1098 private boolean parenPushIfCommaExpression(JsExpression x) {
1099 boolean doPush = x instanceof JsBinaryOperation && ((JsBinaryOperation) x).getOperator() == JsBinaryOperator.COMMA;
1100 if (doPush) {
1101 leftParen();
1102 }
1103 return doPush;
1104 }
1105
1106 private boolean _parenPushOrSpace(JsExpression parent, JsExpression child, boolean wrongAssoc) {
1107 boolean doPush = parenCalc(parent, child, wrongAssoc);
1108 if (doPush) {
1109 leftParen();
1110 }
1111 else {
1112 space();
1113 }
1114 return doPush;
1115 }
1116
1117 private void rightParen() {
1118 p.print(')');
1119 }
1120
1121 private void rightSquare() {
1122 p.print(']');
1123 }
1124
1125 private void semi() {
1126 p.print(';');
1127 }
1128
1129 private boolean sepCommaOptSpace(boolean sep) {
1130 if (sep) {
1131 p.print(',');
1132 spaceOpt();
1133 }
1134 return true;
1135 }
1136
1137 private void slash() {
1138 p.print('/');
1139 }
1140
1141 private void space() {
1142 p.print(' ');
1143 }
1144
1145 /**
1146 * Decide whether, if <code>op</code> is printed followed by <code>arg</code>,
1147 * there needs to be a space between the operator and expression.
1148 *
1149 * @return <code>true</code> if a space needs to be printed
1150 */
1151 private static boolean spaceCalc(JsOperator op, JsExpression arg) {
1152 if (op.isKeyword()) {
1153 return true;
1154 }
1155 if (arg instanceof JsBinaryOperation) {
1156 JsBinaryOperation binary = (JsBinaryOperation) arg;
1157 /*
1158 * If the binary operation has a higher precedence than op, then it won't
1159 * be parenthesized, so check the first argument of the binary operation.
1160 */
1161 return binary.getOperator().getPrecedence() > op.getPrecedence() && spaceCalc(op, binary.getArg1());
1162 }
1163 if (arg instanceof JsPrefixOperation) {
1164 JsOperator op2 = ((JsPrefixOperation) arg).getOperator();
1165 return (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)
1166 && (op2 == JsUnaryOperator.DEC || op2 == JsUnaryOperator.NEG)
1167 || (op == JsBinaryOperator.ADD && op2 == JsUnaryOperator.INC);
1168 }
1169 if (arg instanceof JsNumberLiteral && (op == JsBinaryOperator.SUB || op == JsUnaryOperator.NEG)) {
1170 if (arg instanceof JsIntLiteral) {
1171 return ((JsIntLiteral) arg).value < 0;
1172 }
1173 else {
1174 assert arg instanceof JsDoubleLiteral;
1175 //noinspection CastConflictsWithInstanceof
1176 return ((JsDoubleLiteral) arg).value < 0;
1177 }
1178 }
1179 return false;
1180 }
1181
1182 private void spaceOpt() {
1183 p.printOpt(' ');
1184 }
1185
1186 private void var() {
1187 p.print(CHARS_VAR);
1188 }
1189
1190 private void _while() {
1191 p.print(CHARS_WHILE);
1192 }
1193 }