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 }