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