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