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 }