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