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