001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018//////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.indentation; 021 022import java.util.ArrayDeque; 023import java.util.Deque; 024import java.util.Locale; 025 026import com.puppycrawl.tools.checkstyle.StatelessCheck; 027import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 028import com.puppycrawl.tools.checkstyle.api.DetailAST; 029import com.puppycrawl.tools.checkstyle.api.TokenTypes; 030import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 031import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 032 033/** 034 * <p> 035 * Controls the indentation between comments and surrounding code. 036 * Comments are indented at the same level as the surrounding code. 037 * Detailed info about such convention can be found 038 * <a href="https://checkstyle.org/styleguides/google-java-style-20180523/javaguide.html#s4.8.6.1-block-comment-style"> 039 * here</a> 040 * </p> 041 * <p> 042 * Please take a look at the following examples to understand how the check works: 043 * </p> 044 * <p> 045 * Example #1: Block comments. 046 * </p> 047 * <pre> 048 * 1 /* 049 * 2 * it is Ok 050 * 3 */ 051 * 4 boolean bool = true; 052 * 5 053 * 6 /* violation 054 * 7 * (block comment should have the same indentation level as line 9) 055 * 8 */ 056 * 9 double d = 3.14; 057 * </pre> 058 * <p> 059 * Example #2: Comment is placed at the end of the block and has previous statement. 060 * </p> 061 * <pre> 062 * 1 public void foo1() { 063 * 2 foo2(); 064 * 3 // it is OK 065 * 4 } 066 * 5 067 * 6 public void foo2() { 068 * 7 foo3(); 069 * 8 // violation (comment should have the same indentation level as line 7) 070 * 9 } 071 * </pre> 072 * <p> 073 * Example #3: Comment is used as a single line border to separate groups of methods. 074 * </p> 075 * <pre> 076 * 1 /////////////////////////////// it is OK 077 * 2 078 * 3 public void foo7() { 079 * 4 int a = 0; 080 * 5 } 081 * 6 082 * 7 ///////////////////////////// violation (should have the same indentation level as line 9) 083 * 8 084 * 9 public void foo8() {} 085 * </pre> 086 * <p> 087 * Example #4: Comment has distributed previous statement. 088 * </p> 089 * <pre> 090 * 1 public void foo11() { 091 * 2 CheckUtil 092 * 3 .getFirstNode(new DetailAST()) 093 * 4 .getFirstChild() 094 * 5 .getNextSibling(); 095 * 6 // it is OK 096 * 7 } 097 * 8 098 * 9 public void foo12() { 099 * 10 CheckUtil 100 * 11 .getFirstNode(new DetailAST()) 101 * 12 .getFirstChild() 102 * 13 .getNextSibling(); 103 * 14 // violation (should have the same indentation level as line 10) 104 * 15 } 105 * </pre> 106 * <p> 107 * Example #5: Single line block comment is placed within an empty code block. 108 * Note, if comment is placed at the end of the empty code block, we have 109 * Checkstyle's limitations to clearly detect user intention of explanation 110 * target - above or below. The only case we can assume as a violation is when 111 * a single line comment within the empty code block has indentation level that 112 * is lower than the indentation level of the closing right curly brace. 113 * </p> 114 * <pre> 115 * 1 public void foo46() { 116 * 2 // comment 117 * 3 // block 118 * 4 // it is OK (we cannot clearly detect user intention of explanation target) 119 * 5 } 120 * 6 121 * 7 public void foo46() { 122 * 8 // comment 123 * 9 // block 124 * 10 // violation (comment should have the same indentation level as line 11) 125 * 11 } 126 * </pre> 127 * <p> 128 * Example #6: 'fallthrough' comments and similar. 129 * </p> 130 * <pre> 131 * 0 switch(a) { 132 * 1 case "1": 133 * 2 int k = 7; 134 * 3 // it is OK 135 * 4 case "2": 136 * 5 int k = 7; 137 * 6 // it is OK 138 * 7 case "3": 139 * 8 if (true) {} 140 * 9 // violation (should have the same indentation level as line 8 or 10) 141 * 10 case "4": 142 * 11 case "5": { 143 * 12 int a; 144 * 13 } 145 * 14 // fall through (it is OK) 146 * 15 case "12": { 147 * 16 int a; 148 * 17 } 149 * 18 default: 150 * 19 // it is OK 151 * 20 } 152 * </pre> 153 * <p> 154 * Example #7: Comment is placed within a distributed statement. 155 * </p> 156 * <pre> 157 * 1 String breaks = "J" 158 * 2 // violation (comment should have the same indentation level as line 3) 159 * 3 + "A" 160 * 4 // it is OK 161 * 5 + "V" 162 * 6 + "A" 163 * 7 // it is OK 164 * 8 ; 165 * </pre> 166 * <p> 167 * Example #8: Comment is placed within an empty case block. 168 * Note, if comment is placed at the end of the empty case block, we have 169 * Checkstyle's limitations to clearly detect user intention of explanation 170 * target - above or below. The only case we can assume as a violation is when 171 * a single line comment within the empty case block has indentation level that 172 * is lower than the indentation level of the next case token. 173 * </p> 174 * <pre> 175 * 1 case 4: 176 * 2 // it is OK 177 * 3 case 5: 178 * 4 // violation (should have the same indentation level as line 3 or 5) 179 * 5 case 6: 180 * </pre> 181 * <p> 182 * Example #9: Single line block comment has previous and next statement. 183 * </p> 184 * <pre> 185 * 1 String s1 = "Clean code!"; 186 * 2 s.toString().toString().toString(); 187 * 3 // single line 188 * 4 // block 189 * 5 // comment (it is OK) 190 * 6 int a = 5; 191 * 7 192 * 8 String s2 = "Code complete!"; 193 * 9 s.toString().toString().toString(); 194 * 10 // violation (should have the same indentation level as line 11) 195 * 11 // violation (should have the same indentation level as line 12) 196 * 12 // violation (should have the same indentation level as line 13) 197 * 13 int b = 18; 198 * </pre> 199 * <p> 200 * Example #10: Comment within the block tries to describe the next code block. 201 * </p> 202 * <pre> 203 * 1 public void foo42() { 204 * 2 int a = 5; 205 * 3 if (a == 5) { 206 * 4 int b; 207 * 5 // it is OK 208 * 6 } else if (a ==6) { ... } 209 * 7 } 210 * 8 211 * 9 public void foo43() { 212 * 10 try { 213 * 11 int a; 214 * 12 // Why do we catch exception here? - violation (not the same indentation as line 11) 215 * 13 } catch (Exception e) { ... } 216 * 14 } 217 * </pre> 218 * <ul> 219 * <li> 220 * Property {@code tokens} - tokens to check 221 * Type is {@code int[]}. 222 * Default value is: 223 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#SINGLE_LINE_COMMENT"> 224 * SINGLE_LINE_COMMENT</a>, 225 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BLOCK_COMMENT_BEGIN"> 226 * BLOCK_COMMENT_BEGIN</a>. 227 * </li> 228 * </ul> 229 * <p> 230 * To configure the Check: 231 * </p> 232 * <pre> 233 * <module name="CommentsIndentation"/> 234 * </pre> 235 * <p> 236 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker} 237 * </p> 238 * <p> 239 * Violation Message Keys: 240 * </p> 241 * <ul> 242 * <li> 243 * {@code comments.indentation.block} 244 * </li> 245 * <li> 246 * {@code comments.indentation.single} 247 * </li> 248 * </ul> 249 * 250 * @since 6.10 251 */ 252@StatelessCheck 253public class CommentsIndentationCheck extends AbstractCheck { 254 255 /** 256 * A key is pointing to the warning message text in "messages.properties" file. 257 */ 258 public static final String MSG_KEY_SINGLE = "comments.indentation.single"; 259 260 /** 261 * A key is pointing to the warning message text in "messages.properties" file. 262 */ 263 public static final String MSG_KEY_BLOCK = "comments.indentation.block"; 264 265 @Override 266 public int[] getDefaultTokens() { 267 return new int[] { 268 TokenTypes.SINGLE_LINE_COMMENT, 269 TokenTypes.BLOCK_COMMENT_BEGIN, 270 }; 271 } 272 273 @Override 274 public int[] getAcceptableTokens() { 275 return new int[] { 276 TokenTypes.SINGLE_LINE_COMMENT, 277 TokenTypes.BLOCK_COMMENT_BEGIN, 278 }; 279 } 280 281 @Override 282 public int[] getRequiredTokens() { 283 return CommonUtil.EMPTY_INT_ARRAY; 284 } 285 286 @Override 287 public boolean isCommentNodesRequired() { 288 return true; 289 } 290 291 @Override 292 public void visitToken(DetailAST commentAst) { 293 switch (commentAst.getType()) { 294 case TokenTypes.SINGLE_LINE_COMMENT: 295 case TokenTypes.BLOCK_COMMENT_BEGIN: 296 visitComment(commentAst); 297 break; 298 default: 299 final String exceptionMsg = "Unexpected token type: " + commentAst.getText(); 300 throw new IllegalArgumentException(exceptionMsg); 301 } 302 } 303 304 /** 305 * Checks comment indentations over surrounding code, e.g.: 306 * <p> 307 * {@code 308 * // some comment - this is ok 309 * double d = 3.14; 310 * // some comment - this is <b>not</b> ok. 311 * double d1 = 5.0; 312 * } 313 * </p> 314 * 315 * @param comment comment to check. 316 */ 317 private void visitComment(DetailAST comment) { 318 if (!isTrailingComment(comment)) { 319 final DetailAST prevStmt = getPreviousStatement(comment); 320 final DetailAST nextStmt = getNextStmt(comment); 321 322 if (isInEmptyCaseBlock(prevStmt, nextStmt)) { 323 handleCommentInEmptyCaseBlock(prevStmt, comment, nextStmt); 324 } 325 else if (isFallThroughComment(prevStmt, nextStmt)) { 326 handleFallThroughComment(prevStmt, comment, nextStmt); 327 } 328 else if (isInEmptyCodeBlock(prevStmt, nextStmt)) { 329 handleCommentInEmptyCodeBlock(comment, nextStmt); 330 } 331 else if (isCommentAtTheEndOfTheCodeBlock(nextStmt)) { 332 handleCommentAtTheEndOfTheCodeBlock(prevStmt, comment, nextStmt); 333 } 334 else if (nextStmt != null && !areSameLevelIndented(comment, nextStmt, nextStmt)) { 335 log(comment, getMessageKey(comment), nextStmt.getLineNo(), 336 comment.getColumnNo(), nextStmt.getColumnNo()); 337 } 338 } 339 } 340 341 /** 342 * Returns the next statement of a comment. 343 * 344 * @param comment comment. 345 * @return the next statement of a comment. 346 */ 347 private static DetailAST getNextStmt(DetailAST comment) { 348 DetailAST nextStmt = comment.getNextSibling(); 349 while (nextStmt != null 350 && isComment(nextStmt) 351 && comment.getColumnNo() != nextStmt.getColumnNo()) { 352 nextStmt = nextStmt.getNextSibling(); 353 } 354 return nextStmt; 355 } 356 357 /** 358 * Returns the previous statement of a comment. 359 * 360 * @param comment comment. 361 * @return the previous statement of a comment. 362 */ 363 private DetailAST getPreviousStatement(DetailAST comment) { 364 final DetailAST prevStatement; 365 if (isDistributedPreviousStatement(comment)) { 366 prevStatement = getDistributedPreviousStatement(comment); 367 } 368 else { 369 prevStatement = getOneLinePreviousStatement(comment); 370 } 371 return prevStatement; 372 } 373 374 /** 375 * Checks whether the previous statement of a comment is distributed over two or more lines. 376 * 377 * @param comment comment to check. 378 * @return true if the previous statement of a comment is distributed over two or more lines. 379 */ 380 private boolean isDistributedPreviousStatement(DetailAST comment) { 381 final DetailAST previousSibling = comment.getPreviousSibling(); 382 return isDistributedExpression(comment) 383 || isDistributedReturnStatement(previousSibling) 384 || isDistributedThrowStatement(previousSibling); 385 } 386 387 /** 388 * Checks whether the previous statement of a comment is a method call chain or 389 * string concatenation statement distributed over two ore more lines. 390 * 391 * @param comment comment to check. 392 * @return true if the previous statement is a distributed expression. 393 */ 394 private boolean isDistributedExpression(DetailAST comment) { 395 DetailAST previousSibling = comment.getPreviousSibling(); 396 while (previousSibling != null && isComment(previousSibling)) { 397 previousSibling = previousSibling.getPreviousSibling(); 398 } 399 boolean isDistributed = false; 400 if (previousSibling != null) { 401 if (previousSibling.getType() == TokenTypes.SEMI 402 && isOnPreviousLineIgnoringComments(comment, previousSibling)) { 403 DetailAST currentToken = previousSibling.getPreviousSibling(); 404 while (currentToken.getFirstChild() != null) { 405 currentToken = currentToken.getFirstChild(); 406 } 407 if (currentToken.getType() == TokenTypes.COMMENT_CONTENT) { 408 currentToken = currentToken.getParent(); 409 while (isComment(currentToken)) { 410 currentToken = currentToken.getNextSibling(); 411 } 412 } 413 if (!TokenUtil.areOnSameLine(previousSibling, currentToken)) { 414 isDistributed = true; 415 } 416 } 417 else { 418 isDistributed = isStatementWithPossibleCurlies(previousSibling); 419 } 420 } 421 return isDistributed; 422 } 423 424 /** 425 * Whether the statement can have or always have curly brackets. 426 * 427 * @param previousSibling the statement to check. 428 * @return true if the statement can have or always have curly brackets. 429 */ 430 private static boolean isStatementWithPossibleCurlies(DetailAST previousSibling) { 431 return previousSibling.getType() == TokenTypes.LITERAL_IF 432 || previousSibling.getType() == TokenTypes.LITERAL_TRY 433 || previousSibling.getType() == TokenTypes.LITERAL_FOR 434 || previousSibling.getType() == TokenTypes.LITERAL_DO 435 || previousSibling.getType() == TokenTypes.LITERAL_WHILE 436 || previousSibling.getType() == TokenTypes.LITERAL_SWITCH 437 || isDefinition(previousSibling); 438 } 439 440 /** 441 * Whether the statement is a kind of definition (method, class etc.). 442 * 443 * @param previousSibling the statement to check. 444 * @return true if the statement is a kind of definition. 445 */ 446 private static boolean isDefinition(DetailAST previousSibling) { 447 return previousSibling.getType() == TokenTypes.METHOD_DEF 448 || previousSibling.getType() == TokenTypes.CLASS_DEF 449 || previousSibling.getType() == TokenTypes.INTERFACE_DEF 450 || previousSibling.getType() == TokenTypes.ENUM_DEF 451 || previousSibling.getType() == TokenTypes.ANNOTATION_DEF; 452 } 453 454 /** 455 * Checks whether the previous statement of a comment is a distributed return statement. 456 * 457 * @param commentPreviousSibling previous sibling of the comment. 458 * @return true if the previous statement of a comment is a distributed return statement. 459 */ 460 private static boolean isDistributedReturnStatement(DetailAST commentPreviousSibling) { 461 boolean isDistributed = false; 462 if (commentPreviousSibling != null 463 && commentPreviousSibling.getType() == TokenTypes.LITERAL_RETURN) { 464 final DetailAST firstChild = commentPreviousSibling.getFirstChild(); 465 final DetailAST nextSibling = firstChild.getNextSibling(); 466 if (nextSibling != null) { 467 isDistributed = true; 468 } 469 } 470 return isDistributed; 471 } 472 473 /** 474 * Checks whether the previous statement of a comment is a distributed throw statement. 475 * 476 * @param commentPreviousSibling previous sibling of the comment. 477 * @return true if the previous statement of a comment is a distributed throw statement. 478 */ 479 private static boolean isDistributedThrowStatement(DetailAST commentPreviousSibling) { 480 boolean isDistributed = false; 481 if (commentPreviousSibling != null 482 && commentPreviousSibling.getType() == TokenTypes.LITERAL_THROW) { 483 final DetailAST firstChild = commentPreviousSibling.getFirstChild(); 484 final DetailAST nextSibling = firstChild.getNextSibling(); 485 if (!TokenUtil.areOnSameLine(nextSibling, commentPreviousSibling)) { 486 isDistributed = true; 487 } 488 } 489 return isDistributed; 490 } 491 492 /** 493 * Returns the first token of the distributed previous statement of comment. 494 * 495 * @param comment comment to check. 496 * @return the first token of the distributed previous statement of comment. 497 */ 498 private static DetailAST getDistributedPreviousStatement(DetailAST comment) { 499 DetailAST currentToken = comment.getPreviousSibling(); 500 while (isComment(currentToken)) { 501 currentToken = currentToken.getPreviousSibling(); 502 } 503 final DetailAST previousStatement; 504 if (currentToken.getType() == TokenTypes.SEMI) { 505 currentToken = currentToken.getPreviousSibling(); 506 while (currentToken.getFirstChild() != null) { 507 currentToken = currentToken.getFirstChild(); 508 } 509 previousStatement = currentToken; 510 } 511 else { 512 previousStatement = currentToken; 513 } 514 return previousStatement; 515 } 516 517 /** 518 * Checks whether case block is empty. 519 * 520 * @param nextStmt previous statement. 521 * @param prevStmt next statement. 522 * @return true if case block is empty. 523 */ 524 private static boolean isInEmptyCaseBlock(DetailAST prevStmt, DetailAST nextStmt) { 525 return prevStmt != null 526 && nextStmt != null 527 && (prevStmt.getType() == TokenTypes.LITERAL_CASE 528 || prevStmt.getType() == TokenTypes.CASE_GROUP) 529 && (nextStmt.getType() == TokenTypes.LITERAL_CASE 530 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT); 531 } 532 533 /** 534 * Checks whether comment is a 'fall through' comment. 535 * For example: 536 * <p> 537 * {@code 538 * ... 539 * case OPTION_ONE: 540 * int someVariable = 1; 541 * // fall through 542 * case OPTION_TWO: 543 * int a = 5; 544 * break; 545 * ... 546 * } 547 * </p> 548 * 549 * @param prevStmt previous statement. 550 * @param nextStmt next statement. 551 * @return true if a comment is a 'fall through' comment. 552 */ 553 private static boolean isFallThroughComment(DetailAST prevStmt, DetailAST nextStmt) { 554 return prevStmt != null 555 && nextStmt != null 556 && prevStmt.getType() != TokenTypes.LITERAL_CASE 557 && (nextStmt.getType() == TokenTypes.LITERAL_CASE 558 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT); 559 } 560 561 /** 562 * Checks whether a comment is placed at the end of the code block. 563 * 564 * @param nextStmt next statement. 565 * @return true if a comment is placed at the end of the block. 566 */ 567 private static boolean isCommentAtTheEndOfTheCodeBlock(DetailAST nextStmt) { 568 return nextStmt != null 569 && nextStmt.getType() == TokenTypes.RCURLY; 570 } 571 572 /** 573 * Checks whether comment is placed in the empty code block. 574 * For example: 575 * <p> 576 * ... 577 * {@code 578 * // empty code block 579 * } 580 * ... 581 * </p> 582 * Note, the method does not treat empty case blocks. 583 * 584 * @param prevStmt previous statement. 585 * @param nextStmt next statement. 586 * @return true if comment is placed in the empty code block. 587 */ 588 private static boolean isInEmptyCodeBlock(DetailAST prevStmt, DetailAST nextStmt) { 589 return prevStmt != null 590 && nextStmt != null 591 && (prevStmt.getType() == TokenTypes.SLIST 592 || prevStmt.getType() == TokenTypes.LCURLY 593 || prevStmt.getType() == TokenTypes.ARRAY_INIT 594 || prevStmt.getType() == TokenTypes.OBJBLOCK) 595 && nextStmt.getType() == TokenTypes.RCURLY; 596 } 597 598 /** 599 * Handles a comment which is placed within empty case block. 600 * Note, if comment is placed at the end of the empty case block, we have Checkstyle's 601 * limitations to clearly detect user intention of explanation target - above or below. The 602 * only case we can assume as a violation is when a single line comment within the empty case 603 * block has indentation level that is lower than the indentation level of the next case 604 * token. For example: 605 * <p> 606 * {@code 607 * ... 608 * case OPTION_ONE: 609 * // violation 610 * case OPTION_TWO: 611 * ... 612 * } 613 * </p> 614 * 615 * @param prevStmt previous statement. 616 * @param comment single line comment. 617 * @param nextStmt next statement. 618 */ 619 private void handleCommentInEmptyCaseBlock(DetailAST prevStmt, DetailAST comment, 620 DetailAST nextStmt) { 621 if (comment.getColumnNo() < prevStmt.getColumnNo() 622 || comment.getColumnNo() < nextStmt.getColumnNo()) { 623 logMultilineIndentation(prevStmt, comment, nextStmt); 624 } 625 } 626 627 /** 628 * Handles 'fall through' single line comment. 629 * Note, 'fall through' and similar comments can have indentation level as next or previous 630 * statement. 631 * For example: 632 * <p> 633 * {@code 634 * ... 635 * case OPTION_ONE: 636 * int someVariable = 1; 637 * // fall through - OK 638 * case OPTION_TWO: 639 * int a = 5; 640 * break; 641 * ... 642 * } 643 * </p> 644 * <p> 645 * {@code 646 * ... 647 * case OPTION_ONE: 648 * int someVariable = 1; 649 * // then init variable a - OK 650 * case OPTION_TWO: 651 * int a = 5; 652 * break; 653 * ... 654 * } 655 * </p> 656 * 657 * @param prevStmt previous statement. 658 * @param comment single line comment. 659 * @param nextStmt next statement. 660 */ 661 private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment, 662 DetailAST nextStmt) { 663 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) { 664 logMultilineIndentation(prevStmt, comment, nextStmt); 665 } 666 } 667 668 /** 669 * Handles a comment which is placed at the end of non empty code block. 670 * Note, if single line comment is placed at the end of non empty block the comment should have 671 * the same indentation level as the previous statement. For example: 672 * <p> 673 * {@code 674 * if (a == true) { 675 * int b = 1; 676 * // comment 677 * } 678 * } 679 * </p> 680 * 681 * @param prevStmt previous statement. 682 * @param comment comment to check. 683 * @param nextStmt next statement. 684 */ 685 private void handleCommentAtTheEndOfTheCodeBlock(DetailAST prevStmt, DetailAST comment, 686 DetailAST nextStmt) { 687 if (prevStmt != null) { 688 if (prevStmt.getType() == TokenTypes.LITERAL_CASE 689 || prevStmt.getType() == TokenTypes.CASE_GROUP 690 || prevStmt.getType() == TokenTypes.LITERAL_DEFAULT) { 691 if (comment.getColumnNo() < nextStmt.getColumnNo()) { 692 log(comment, getMessageKey(comment), nextStmt.getLineNo(), 693 comment.getColumnNo(), nextStmt.getColumnNo()); 694 } 695 } 696 else if (isCommentForMultiblock(nextStmt)) { 697 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) { 698 logMultilineIndentation(prevStmt, comment, nextStmt); 699 } 700 } 701 else if (!areSameLevelIndented(comment, prevStmt, prevStmt)) { 702 final int prevStmtLineNo = prevStmt.getLineNo(); 703 log(comment, getMessageKey(comment), prevStmtLineNo, 704 comment.getColumnNo(), getLineStart(prevStmtLineNo)); 705 } 706 } 707 } 708 709 /** 710 * Whether the comment might have been used for the next block in a multi-block structure. 711 * 712 * @param endBlockStmt the end of the current block. 713 * @return true, if the comment might have been used for the next 714 * block in a multi-block structure. 715 */ 716 private static boolean isCommentForMultiblock(DetailAST endBlockStmt) { 717 final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling(); 718 final int endBlockLineNo = endBlockStmt.getLineNo(); 719 final DetailAST catchAst = endBlockStmt.getParent().getParent(); 720 final DetailAST finallyAst = catchAst.getNextSibling(); 721 return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo 722 || finallyAst != null 723 && catchAst.getType() == TokenTypes.LITERAL_CATCH 724 && finallyAst.getLineNo() == endBlockLineNo; 725 } 726 727 /** 728 * Handles a comment which is placed within the empty code block. 729 * Note, if comment is placed at the end of the empty code block, we have Checkstyle's 730 * limitations to clearly detect user intention of explanation target - above or below. The 731 * only case we can assume as a violation is when a single line comment within the empty 732 * code block has indentation level that is lower than the indentation level of the closing 733 * right curly brace. For example: 734 * <p> 735 * {@code 736 * if (a == true) { 737 * // violation 738 * } 739 * } 740 * </p> 741 * 742 * @param comment comment to check. 743 * @param nextStmt next statement. 744 */ 745 private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) { 746 if (comment.getColumnNo() < nextStmt.getColumnNo()) { 747 log(comment, getMessageKey(comment), nextStmt.getLineNo(), 748 comment.getColumnNo(), nextStmt.getColumnNo()); 749 } 750 } 751 752 /** 753 * Does pre-order traverse of abstract syntax tree to find the previous statement of the 754 * comment. If previous statement of the comment is found, then the traverse will 755 * be finished. 756 * 757 * @param comment current statement. 758 * @return previous statement of the comment or null if the comment does not have previous 759 * statement. 760 */ 761 private DetailAST getOneLinePreviousStatement(DetailAST comment) { 762 DetailAST root = comment.getParent(); 763 while (root != null && !isBlockStart(root)) { 764 root = root.getParent(); 765 } 766 767 final Deque<DetailAST> stack = new ArrayDeque<>(); 768 DetailAST previousStatement = null; 769 while (root != null || !stack.isEmpty()) { 770 if (!stack.isEmpty()) { 771 root = stack.pop(); 772 } 773 while (root != null) { 774 previousStatement = findPreviousStatement(comment, root); 775 if (previousStatement != null) { 776 root = null; 777 stack.clear(); 778 break; 779 } 780 if (root.getNextSibling() != null) { 781 stack.push(root.getNextSibling()); 782 } 783 root = root.getFirstChild(); 784 } 785 } 786 return previousStatement; 787 } 788 789 /** 790 * Whether the ast is a comment. 791 * 792 * @param ast the ast to check. 793 * @return true if the ast is a comment. 794 */ 795 private static boolean isComment(DetailAST ast) { 796 final int astType = ast.getType(); 797 return astType == TokenTypes.SINGLE_LINE_COMMENT 798 || astType == TokenTypes.BLOCK_COMMENT_BEGIN 799 || astType == TokenTypes.COMMENT_CONTENT 800 || astType == TokenTypes.BLOCK_COMMENT_END; 801 } 802 803 /** 804 * Whether the AST node starts a block. 805 * 806 * @param root the AST node to check. 807 * @return true if the AST node starts a block. 808 */ 809 private static boolean isBlockStart(DetailAST root) { 810 return root.getType() == TokenTypes.SLIST 811 || root.getType() == TokenTypes.OBJBLOCK 812 || root.getType() == TokenTypes.ARRAY_INIT 813 || root.getType() == TokenTypes.CASE_GROUP; 814 } 815 816 /** 817 * Finds a previous statement of the comment. 818 * Uses root token of the line while searching. 819 * 820 * @param comment comment. 821 * @param root root token of the line. 822 * @return previous statement of the comment or null if previous statement was not found. 823 */ 824 private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) { 825 DetailAST previousStatement = null; 826 if (root.getLineNo() >= comment.getLineNo()) { 827 // ATTENTION: parent of the comment is below the comment in case block 828 // See https://github.com/checkstyle/checkstyle/issues/851 829 previousStatement = getPrevStatementFromSwitchBlock(comment); 830 } 831 final DetailAST tokenWhichBeginsTheLine; 832 if (root.getType() == TokenTypes.EXPR 833 && root.getFirstChild().getFirstChild() != null) { 834 if (root.getFirstChild().getType() == TokenTypes.LITERAL_NEW) { 835 tokenWhichBeginsTheLine = root.getFirstChild(); 836 } 837 else { 838 tokenWhichBeginsTheLine = findTokenWhichBeginsTheLine(root); 839 } 840 } 841 else if (root.getType() == TokenTypes.PLUS) { 842 tokenWhichBeginsTheLine = root.getFirstChild(); 843 } 844 else { 845 tokenWhichBeginsTheLine = root; 846 } 847 if (tokenWhichBeginsTheLine != null 848 && !isComment(tokenWhichBeginsTheLine) 849 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) { 850 previousStatement = tokenWhichBeginsTheLine; 851 } 852 return previousStatement; 853 } 854 855 /** 856 * Finds a token which begins the line. 857 * 858 * @param root root token of the line. 859 * @return token which begins the line. 860 */ 861 private static DetailAST findTokenWhichBeginsTheLine(DetailAST root) { 862 final DetailAST tokenWhichBeginsTheLine; 863 if (isUsingOfObjectReferenceToInvokeMethod(root)) { 864 tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root); 865 } 866 else { 867 tokenWhichBeginsTheLine = root.getFirstChild().findFirstToken(TokenTypes.IDENT); 868 } 869 return tokenWhichBeginsTheLine; 870 } 871 872 /** 873 * Checks whether there is a use of an object reference to invoke an object's method on line. 874 * 875 * @param root root token of the line. 876 * @return true if there is a use of an object reference to invoke an object's method on line. 877 */ 878 private static boolean isUsingOfObjectReferenceToInvokeMethod(DetailAST root) { 879 return root.getFirstChild().getFirstChild().getFirstChild() != null 880 && root.getFirstChild().getFirstChild().getFirstChild().getNextSibling() != null; 881 } 882 883 /** 884 * Finds the start token of method call chain. 885 * 886 * @param root root token of the line. 887 * @return the start token of method call chain. 888 */ 889 private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) { 890 DetailAST startOfMethodCallChain = root; 891 while (startOfMethodCallChain.getFirstChild() != null 892 && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) { 893 startOfMethodCallChain = startOfMethodCallChain.getFirstChild(); 894 } 895 if (startOfMethodCallChain.getFirstChild() != null) { 896 startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling(); 897 } 898 return startOfMethodCallChain; 899 } 900 901 /** 902 * Checks whether the checked statement is on the previous line ignoring empty lines 903 * and lines which contain only comments. 904 * 905 * @param currentStatement current statement. 906 * @param checkedStatement checked statement. 907 * @return true if checked statement is on the line which is previous to current statement 908 * ignoring empty lines and lines which contain only comments. 909 */ 910 private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement, 911 DetailAST checkedStatement) { 912 DetailAST nextToken = getNextToken(checkedStatement); 913 int distanceAim = 1; 914 if (nextToken != null && isComment(nextToken)) { 915 distanceAim += countEmptyLines(checkedStatement, currentStatement); 916 } 917 918 while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) { 919 if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 920 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo(); 921 } 922 distanceAim++; 923 nextToken = nextToken.getNextSibling(); 924 } 925 return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim; 926 } 927 928 /** 929 * Get the token to start counting the number of lines to add to the distance aim from. 930 * 931 * @param checkedStatement the checked statement. 932 * @return the token to start counting the number of lines to add to the distance aim from. 933 */ 934 private DetailAST getNextToken(DetailAST checkedStatement) { 935 DetailAST nextToken; 936 if (checkedStatement.getType() == TokenTypes.SLIST 937 || checkedStatement.getType() == TokenTypes.ARRAY_INIT 938 || checkedStatement.getType() == TokenTypes.CASE_GROUP) { 939 nextToken = checkedStatement.getFirstChild(); 940 } 941 else { 942 nextToken = checkedStatement.getNextSibling(); 943 } 944 if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) { 945 nextToken = nextToken.getNextSibling(); 946 } 947 return nextToken; 948 } 949 950 /** 951 * Count the number of empty lines between statements. 952 * 953 * @param startStatement start statement. 954 * @param endStatement end statement. 955 * @return the number of empty lines between statements. 956 */ 957 private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) { 958 int emptyLinesNumber = 0; 959 final String[] lines = getLines(); 960 final int endLineNo = endStatement.getLineNo(); 961 for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) { 962 if (CommonUtil.isBlank(lines[lineNo])) { 963 emptyLinesNumber++; 964 } 965 } 966 return emptyLinesNumber; 967 } 968 969 /** 970 * Logs comment which can have the same indentation level as next or previous statement. 971 * 972 * @param comment comment. 973 * @param nextStmt next statement. 974 * @param prevStmt previous statement. 975 */ 976 private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment, 977 DetailAST nextStmt) { 978 final String multilineNoTemplate = "%d, %d"; 979 log(comment, getMessageKey(comment), 980 String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(), 981 nextStmt.getLineNo()), comment.getColumnNo(), 982 String.format(Locale.getDefault(), multilineNoTemplate, 983 getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo()))); 984 } 985 986 /** 987 * Get a message key depending on a comment type. 988 * 989 * @param comment the comment to process. 990 * @return a message key. 991 */ 992 private static String getMessageKey(DetailAST comment) { 993 final String msgKey; 994 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 995 msgKey = MSG_KEY_SINGLE; 996 } 997 else { 998 msgKey = MSG_KEY_BLOCK; 999 } 1000 return msgKey; 1001 } 1002 1003 /** 1004 * Gets comment's previous statement from switch block. 1005 * 1006 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 1007 * @return comment's previous statement or null if previous statement is absent. 1008 */ 1009 private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) { 1010 final DetailAST prevStmt; 1011 final DetailAST parentStatement = comment.getParent(); 1012 if (parentStatement.getType() == TokenTypes.CASE_GROUP) { 1013 prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement); 1014 } 1015 else { 1016 prevStmt = getPrevCaseToken(parentStatement); 1017 } 1018 return prevStmt; 1019 } 1020 1021 /** 1022 * Gets previous statement for comment which is placed immediately under case. 1023 * 1024 * @param parentStatement comment's parent statement. 1025 * @return comment's previous statement or null if previous statement is absent. 1026 */ 1027 private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) { 1028 DetailAST prevStmt = null; 1029 final DetailAST prevBlock = parentStatement.getPreviousSibling(); 1030 if (prevBlock.getLastChild() != null) { 1031 DetailAST blockBody = prevBlock.getLastChild().getLastChild(); 1032 if (blockBody.getType() == TokenTypes.SEMI) { 1033 blockBody = blockBody.getPreviousSibling(); 1034 } 1035 if (blockBody.getType() == TokenTypes.EXPR) { 1036 if (isUsingOfObjectReferenceToInvokeMethod(blockBody)) { 1037 prevStmt = findStartTokenOfMethodCallChain(blockBody); 1038 } 1039 else { 1040 prevStmt = blockBody.getFirstChild().getFirstChild(); 1041 } 1042 } 1043 else { 1044 if (blockBody.getType() == TokenTypes.SLIST) { 1045 prevStmt = blockBody.getParent().getParent(); 1046 } 1047 else { 1048 prevStmt = blockBody; 1049 } 1050 } 1051 if (isComment(prevStmt)) { 1052 prevStmt = prevStmt.getNextSibling(); 1053 } 1054 } 1055 return prevStmt; 1056 } 1057 1058 /** 1059 * Gets previous case-token for comment. 1060 * 1061 * @param parentStatement comment's parent statement. 1062 * @return previous case-token or null if previous case-token is absent. 1063 */ 1064 private static DetailAST getPrevCaseToken(DetailAST parentStatement) { 1065 final DetailAST prevCaseToken; 1066 final DetailAST parentBlock = parentStatement.getParent(); 1067 if (parentBlock.getParent() != null 1068 && parentBlock.getParent().getPreviousSibling() != null 1069 && parentBlock.getParent().getPreviousSibling().getType() 1070 == TokenTypes.LITERAL_CASE) { 1071 prevCaseToken = parentBlock.getParent().getPreviousSibling(); 1072 } 1073 else { 1074 prevCaseToken = null; 1075 } 1076 return prevCaseToken; 1077 } 1078 1079 /** 1080 * Checks if comment and next code statement 1081 * (or previous code stmt like <b>case</b> in switch block) are indented at the same level, 1082 * e.g.: 1083 * <p> 1084 * <pre> 1085 * {@code 1086 * // some comment - same indentation level 1087 * int x = 10; 1088 * // some comment - different indentation level 1089 * int x1 = 5; 1090 * /* 1091 * * 1092 * */ 1093 * boolean bool = true; - same indentation level 1094 * } 1095 * </pre> 1096 * </p> 1097 * 1098 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 1099 * @param prevStmt previous code statement. 1100 * @param nextStmt next code statement. 1101 * @return true if comment and next code statement are indented at the same level. 1102 */ 1103 private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt, 1104 DetailAST nextStmt) { 1105 return comment.getColumnNo() == getLineStart(nextStmt.getLineNo()) 1106 || comment.getColumnNo() == getLineStart(prevStmt.getLineNo()); 1107 } 1108 1109 /** 1110 * Get a column number where a code starts. 1111 * 1112 * @param lineNo the line number to get column number in. 1113 * @return the column number where a code starts. 1114 */ 1115 private int getLineStart(int lineNo) { 1116 final char[] line = getLines()[lineNo - 1].toCharArray(); 1117 int lineStart = 0; 1118 while (Character.isWhitespace(line[lineStart])) { 1119 lineStart++; 1120 } 1121 return lineStart; 1122 } 1123 1124 /** 1125 * Checks if current comment is a trailing comment. 1126 * 1127 * @param comment comment to check. 1128 * @return true if current comment is a trailing comment. 1129 */ 1130 private boolean isTrailingComment(DetailAST comment) { 1131 final boolean isTrailingComment; 1132 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 1133 isTrailingComment = isTrailingSingleLineComment(comment); 1134 } 1135 else { 1136 isTrailingComment = isTrailingBlockComment(comment); 1137 } 1138 return isTrailingComment; 1139 } 1140 1141 /** 1142 * Checks if current single line comment is trailing comment, e.g.: 1143 * <p> 1144 * {@code 1145 * double d = 3.14; // some comment 1146 * } 1147 * </p> 1148 * 1149 * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 1150 * @return true if current single line comment is trailing comment. 1151 */ 1152 private boolean isTrailingSingleLineComment(DetailAST singleLineComment) { 1153 final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1); 1154 final int commentColumnNo = singleLineComment.getColumnNo(); 1155 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine); 1156 } 1157 1158 /** 1159 * Checks if current comment block is trailing comment, e.g.: 1160 * <p> 1161 * {@code 1162 * double d = 3.14; /* some comment */ 1163 * /* some comment */ double d = 18.5; 1164 * } 1165 * </p> 1166 * 1167 * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}. 1168 * @return true if current comment block is trailing comment. 1169 */ 1170 private boolean isTrailingBlockComment(DetailAST blockComment) { 1171 final String commentLine = getLine(blockComment.getLineNo() - 1); 1172 final int commentColumnNo = blockComment.getColumnNo(); 1173 final DetailAST nextSibling = blockComment.getNextSibling(); 1174 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine) 1175 || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment); 1176 } 1177 1178}