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