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