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