001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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 java.lang.String[]}. 222 * Validation type is {@code tokenSet}. 223 * Default value is: 224 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#SINGLE_LINE_COMMENT"> 225 * SINGLE_LINE_COMMENT</a>, 226 * <a href="https://checkstyle.org/apidocs/com/puppycrawl/tools/checkstyle/api/TokenTypes.html#BLOCK_COMMENT_BEGIN"> 227 * BLOCK_COMMENT_BEGIN</a>. 228 * </li> 229 * </ul> 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 (currentToken.getType() == TokenTypes.COMMENT_CONTENT) { 410 currentToken = currentToken.getParent(); 411 while (isComment(currentToken)) { 412 currentToken = currentToken.getNextSibling(); 413 } 414 } 415 if (!TokenUtil.areOnSameLine(previousSibling, currentToken)) { 416 isDistributed = true; 417 } 418 } 419 else { 420 isDistributed = isStatementWithPossibleCurlies(previousSibling); 421 } 422 } 423 return isDistributed; 424 } 425 426 /** 427 * Whether the statement can have or always have curly brackets. 428 * 429 * @param previousSibling the statement to check. 430 * @return true if the statement can have or always have curly brackets. 431 */ 432 private static boolean isStatementWithPossibleCurlies(DetailAST previousSibling) { 433 return previousSibling.getType() == TokenTypes.LITERAL_IF 434 || previousSibling.getType() == TokenTypes.LITERAL_TRY 435 || previousSibling.getType() == TokenTypes.LITERAL_FOR 436 || previousSibling.getType() == TokenTypes.LITERAL_DO 437 || previousSibling.getType() == TokenTypes.LITERAL_WHILE 438 || previousSibling.getType() == TokenTypes.LITERAL_SWITCH 439 || isDefinition(previousSibling); 440 } 441 442 /** 443 * Whether the statement is a kind of definition (method, class etc.). 444 * 445 * @param previousSibling the statement to check. 446 * @return true if the statement is a kind of definition. 447 */ 448 private static boolean isDefinition(DetailAST previousSibling) { 449 return TokenUtil.isTypeDeclaration(previousSibling.getType()) 450 || previousSibling.getType() == TokenTypes.METHOD_DEF; 451 } 452 453 /** 454 * Checks whether the previous statement of a comment is a distributed return statement. 455 * 456 * @param commentPreviousSibling previous sibling of the comment. 457 * @return true if the previous statement of a comment is a distributed return statement. 458 */ 459 private static boolean isDistributedReturnStatement(DetailAST commentPreviousSibling) { 460 boolean isDistributed = false; 461 if (commentPreviousSibling != null 462 && commentPreviousSibling.getType() == TokenTypes.LITERAL_RETURN) { 463 final DetailAST firstChild = commentPreviousSibling.getFirstChild(); 464 final DetailAST nextSibling = firstChild.getNextSibling(); 465 if (nextSibling != null) { 466 isDistributed = true; 467 } 468 } 469 return isDistributed; 470 } 471 472 /** 473 * Checks whether the previous statement of a comment is a distributed throw statement. 474 * 475 * @param commentPreviousSibling previous sibling of the comment. 476 * @return true if the previous statement of a comment is a distributed throw statement. 477 */ 478 private static boolean isDistributedThrowStatement(DetailAST commentPreviousSibling) { 479 boolean isDistributed = false; 480 if (commentPreviousSibling != null 481 && commentPreviousSibling.getType() == TokenTypes.LITERAL_THROW) { 482 final DetailAST firstChild = commentPreviousSibling.getFirstChild(); 483 final DetailAST nextSibling = firstChild.getNextSibling(); 484 if (!TokenUtil.areOnSameLine(nextSibling, commentPreviousSibling)) { 485 isDistributed = true; 486 } 487 } 488 return isDistributed; 489 } 490 491 /** 492 * Returns the first token of the distributed previous statement of comment. 493 * 494 * @param comment comment to check. 495 * @return the first token of the distributed previous statement of comment. 496 */ 497 private static DetailAST getDistributedPreviousStatement(DetailAST comment) { 498 DetailAST currentToken = comment.getPreviousSibling(); 499 while (isComment(currentToken)) { 500 currentToken = currentToken.getPreviousSibling(); 501 } 502 final DetailAST previousStatement; 503 if (currentToken.getType() == TokenTypes.SEMI) { 504 currentToken = currentToken.getPreviousSibling(); 505 while (currentToken.getFirstChild() != null) { 506 currentToken = currentToken.getFirstChild(); 507 } 508 previousStatement = currentToken; 509 } 510 else { 511 previousStatement = currentToken; 512 } 513 return previousStatement; 514 } 515 516 /** 517 * Checks whether case block is empty. 518 * 519 * @param nextStmt previous statement. 520 * @param prevStmt next statement. 521 * @return true if case block is empty. 522 */ 523 private static boolean isInEmptyCaseBlock(DetailAST prevStmt, DetailAST nextStmt) { 524 return prevStmt != null 525 && nextStmt != null 526 && (prevStmt.getType() == TokenTypes.LITERAL_CASE 527 || prevStmt.getType() == TokenTypes.CASE_GROUP) 528 && (nextStmt.getType() == TokenTypes.LITERAL_CASE 529 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT); 530 } 531 532 /** 533 * Checks whether comment is a 'fall through' comment. 534 * For example: 535 * <p> 536 * {@code 537 * ... 538 * case OPTION_ONE: 539 * int someVariable = 1; 540 * // fall through 541 * case OPTION_TWO: 542 * int a = 5; 543 * break; 544 * ... 545 * } 546 * </p> 547 * 548 * @param prevStmt previous statement. 549 * @param nextStmt next statement. 550 * @return true if a comment is a 'fall through' comment. 551 */ 552 private static boolean isFallThroughComment(DetailAST prevStmt, DetailAST nextStmt) { 553 return prevStmt != null 554 && nextStmt != null 555 && prevStmt.getType() != TokenTypes.LITERAL_CASE 556 && (nextStmt.getType() == TokenTypes.LITERAL_CASE 557 || nextStmt.getType() == TokenTypes.LITERAL_DEFAULT); 558 } 559 560 /** 561 * Checks whether a comment is placed at the end of the code block. 562 * 563 * @param nextStmt next statement. 564 * @return true if a comment is placed at the end of the block. 565 */ 566 private static boolean isCommentAtTheEndOfTheCodeBlock(DetailAST nextStmt) { 567 return nextStmt != null 568 && nextStmt.getType() == TokenTypes.RCURLY; 569 } 570 571 /** 572 * Checks whether comment is placed in the empty code block. 573 * For example: 574 * <p> 575 * ... 576 * {@code 577 * // empty code block 578 * } 579 * ... 580 * </p> 581 * Note, the method does not treat empty case blocks. 582 * 583 * @param prevStmt previous statement. 584 * @param nextStmt next statement. 585 * @return true if comment is placed in the empty code block. 586 */ 587 private static boolean isInEmptyCodeBlock(DetailAST prevStmt, DetailAST nextStmt) { 588 return prevStmt != null 589 && nextStmt != null 590 && (prevStmt.getType() == TokenTypes.SLIST 591 || prevStmt.getType() == TokenTypes.LCURLY 592 || prevStmt.getType() == TokenTypes.ARRAY_INIT 593 || prevStmt.getType() == TokenTypes.OBJBLOCK) 594 && nextStmt.getType() == TokenTypes.RCURLY; 595 } 596 597 /** 598 * Handles a comment which is placed within empty case block. 599 * Note, if comment is placed at the end of the empty case block, we have Checkstyle's 600 * limitations to clearly detect user intention of explanation target - above or below. The 601 * only case we can assume as a violation is when a single line comment within the empty case 602 * block has indentation level that is lower than the indentation level of the next case 603 * token. For example: 604 * <p> 605 * {@code 606 * ... 607 * case OPTION_ONE: 608 * // violation 609 * case OPTION_TWO: 610 * ... 611 * } 612 * </p> 613 * 614 * @param prevStmt previous statement. 615 * @param comment single line comment. 616 * @param nextStmt next statement. 617 */ 618 private void handleCommentInEmptyCaseBlock(DetailAST prevStmt, DetailAST comment, 619 DetailAST nextStmt) { 620 if (comment.getColumnNo() < prevStmt.getColumnNo() 621 || comment.getColumnNo() < nextStmt.getColumnNo()) { 622 logMultilineIndentation(prevStmt, comment, nextStmt); 623 } 624 } 625 626 /** 627 * Handles 'fall through' single line comment. 628 * Note, 'fall through' and similar comments can have indentation level as next or previous 629 * statement. 630 * For example: 631 * <p> 632 * {@code 633 * ... 634 * case OPTION_ONE: 635 * int someVariable = 1; 636 * // fall through - OK 637 * case OPTION_TWO: 638 * int a = 5; 639 * break; 640 * ... 641 * } 642 * </p> 643 * <p> 644 * {@code 645 * ... 646 * case OPTION_ONE: 647 * int someVariable = 1; 648 * // then init variable a - OK 649 * case OPTION_TWO: 650 * int a = 5; 651 * break; 652 * ... 653 * } 654 * </p> 655 * 656 * @param prevStmt previous statement. 657 * @param comment single line comment. 658 * @param nextStmt next statement. 659 */ 660 private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment, 661 DetailAST nextStmt) { 662 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) { 663 logMultilineIndentation(prevStmt, comment, nextStmt); 664 } 665 } 666 667 /** 668 * Handles a comment which is placed at the end of non empty code block. 669 * Note, if single line comment is placed at the end of non empty block the comment should have 670 * the same indentation level as the previous statement. For example: 671 * <p> 672 * {@code 673 * if (a == true) { 674 * int b = 1; 675 * // comment 676 * } 677 * } 678 * </p> 679 * 680 * @param prevStmt previous statement. 681 * @param comment comment to check. 682 * @param nextStmt next statement. 683 */ 684 private void handleCommentAtTheEndOfTheCodeBlock(DetailAST prevStmt, DetailAST comment, 685 DetailAST nextStmt) { 686 if (prevStmt != null) { 687 if (prevStmt.getType() == TokenTypes.LITERAL_CASE 688 || prevStmt.getType() == TokenTypes.CASE_GROUP 689 || prevStmt.getType() == TokenTypes.LITERAL_DEFAULT) { 690 if (comment.getColumnNo() < nextStmt.getColumnNo()) { 691 log(comment, getMessageKey(comment), nextStmt.getLineNo(), 692 comment.getColumnNo(), nextStmt.getColumnNo()); 693 } 694 } 695 else if (isCommentForMultiblock(nextStmt)) { 696 if (!areSameLevelIndented(comment, prevStmt, nextStmt)) { 697 logMultilineIndentation(prevStmt, comment, nextStmt); 698 } 699 } 700 else if (!areSameLevelIndented(comment, prevStmt, prevStmt)) { 701 final int prevStmtLineNo = prevStmt.getLineNo(); 702 log(comment, getMessageKey(comment), prevStmtLineNo, 703 comment.getColumnNo(), getLineStart(prevStmtLineNo)); 704 } 705 } 706 } 707 708 /** 709 * Whether the comment might have been used for the next block in a multi-block structure. 710 * 711 * @param endBlockStmt the end of the current block. 712 * @return true, if the comment might have been used for the next 713 * block in a multi-block structure. 714 */ 715 private static boolean isCommentForMultiblock(DetailAST endBlockStmt) { 716 final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling(); 717 final int endBlockLineNo = endBlockStmt.getLineNo(); 718 final DetailAST catchAst = endBlockStmt.getParent().getParent(); 719 final DetailAST finallyAst = catchAst.getNextSibling(); 720 return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo 721 || finallyAst != null 722 && catchAst.getType() == TokenTypes.LITERAL_CATCH 723 && finallyAst.getLineNo() == endBlockLineNo; 724 } 725 726 /** 727 * Handles a comment which is placed within the empty code block. 728 * Note, if comment is placed at the end of the empty code block, we have Checkstyle's 729 * limitations to clearly detect user intention of explanation target - above or below. The 730 * only case we can assume as a violation is when a single line comment within the empty 731 * code block has indentation level that is lower than the indentation level of the closing 732 * right curly brace. For example: 733 * <p> 734 * {@code 735 * if (a == true) { 736 * // violation 737 * } 738 * } 739 * </p> 740 * 741 * @param comment comment to check. 742 * @param nextStmt next statement. 743 */ 744 private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) { 745 if (comment.getColumnNo() < nextStmt.getColumnNo()) { 746 log(comment, getMessageKey(comment), nextStmt.getLineNo(), 747 comment.getColumnNo(), nextStmt.getColumnNo()); 748 } 749 } 750 751 /** 752 * Does pre-order traverse of abstract syntax tree to find the previous statement of the 753 * comment. If previous statement of the comment is found, then the traverse will 754 * be finished. 755 * 756 * @param comment current statement. 757 * @return previous statement of the comment or null if the comment does not have previous 758 * statement. 759 */ 760 private DetailAST getOneLinePreviousStatement(DetailAST comment) { 761 DetailAST root = comment.getParent(); 762 while (root != null && !isBlockStart(root)) { 763 root = root.getParent(); 764 } 765 766 final Deque<DetailAST> stack = new ArrayDeque<>(); 767 DetailAST previousStatement = null; 768 while (root != null || !stack.isEmpty()) { 769 if (!stack.isEmpty()) { 770 root = stack.pop(); 771 } 772 while (root != null) { 773 previousStatement = findPreviousStatement(comment, root); 774 if (previousStatement != null) { 775 root = null; 776 stack.clear(); 777 break; 778 } 779 if (root.getNextSibling() != null) { 780 stack.push(root.getNextSibling()); 781 } 782 root = root.getFirstChild(); 783 } 784 } 785 return previousStatement; 786 } 787 788 /** 789 * Whether the ast is a comment. 790 * 791 * @param ast the ast to check. 792 * @return true if the ast is a comment. 793 */ 794 private static boolean isComment(DetailAST ast) { 795 final int astType = ast.getType(); 796 return astType == TokenTypes.SINGLE_LINE_COMMENT 797 || astType == TokenTypes.BLOCK_COMMENT_BEGIN 798 || astType == TokenTypes.COMMENT_CONTENT 799 || astType == TokenTypes.BLOCK_COMMENT_END; 800 } 801 802 /** 803 * Whether the AST node starts a block. 804 * 805 * @param root the AST node to check. 806 * @return true if the AST node starts a block. 807 */ 808 private static boolean isBlockStart(DetailAST root) { 809 return root.getType() == TokenTypes.SLIST 810 || root.getType() == TokenTypes.OBJBLOCK 811 || root.getType() == TokenTypes.ARRAY_INIT 812 || root.getType() == TokenTypes.CASE_GROUP; 813 } 814 815 /** 816 * Finds a previous statement of the comment. 817 * Uses root token of the line while searching. 818 * 819 * @param comment comment. 820 * @param root root token of the line. 821 * @return previous statement of the comment or null if previous statement was not found. 822 */ 823 private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) { 824 DetailAST previousStatement = null; 825 if (root.getLineNo() >= comment.getLineNo()) { 826 // ATTENTION: parent of the comment is below the comment in case block 827 // See https://github.com/checkstyle/checkstyle/issues/851 828 previousStatement = getPrevStatementFromSwitchBlock(comment); 829 } 830 final DetailAST tokenWhichBeginsTheLine; 831 if (root.getType() == TokenTypes.EXPR 832 && root.getFirstChild().getFirstChild() != null) { 833 if (root.getFirstChild().getType() == TokenTypes.LITERAL_NEW) { 834 tokenWhichBeginsTheLine = root.getFirstChild(); 835 } 836 else { 837 tokenWhichBeginsTheLine = findTokenWhichBeginsTheLine(root); 838 } 839 } 840 else if (root.getType() == TokenTypes.PLUS) { 841 tokenWhichBeginsTheLine = root.getFirstChild(); 842 } 843 else { 844 tokenWhichBeginsTheLine = root; 845 } 846 if (tokenWhichBeginsTheLine != null 847 && !isComment(tokenWhichBeginsTheLine) 848 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) { 849 previousStatement = tokenWhichBeginsTheLine; 850 } 851 return previousStatement; 852 } 853 854 /** 855 * Finds a token which begins the line. 856 * 857 * @param root root token of the line. 858 * @return token which begins the line. 859 */ 860 private static DetailAST findTokenWhichBeginsTheLine(DetailAST root) { 861 final DetailAST tokenWhichBeginsTheLine; 862 if (isUsingOfObjectReferenceToInvokeMethod(root)) { 863 tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root); 864 } 865 else { 866 tokenWhichBeginsTheLine = root.getFirstChild().findFirstToken(TokenTypes.IDENT); 867 } 868 return tokenWhichBeginsTheLine; 869 } 870 871 /** 872 * Checks whether there is a use of an object reference to invoke an object's method on line. 873 * 874 * @param root root token of the line. 875 * @return true if there is a use of an object reference to invoke an object's method on line. 876 */ 877 private static boolean isUsingOfObjectReferenceToInvokeMethod(DetailAST root) { 878 return root.getFirstChild().getFirstChild().getFirstChild() != null 879 && root.getFirstChild().getFirstChild().getFirstChild().getNextSibling() != null; 880 } 881 882 /** 883 * Finds the start token of method call chain. 884 * 885 * @param root root token of the line. 886 * @return the start token of method call chain. 887 */ 888 private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) { 889 DetailAST startOfMethodCallChain = root; 890 while (startOfMethodCallChain.getFirstChild() != null 891 && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) { 892 startOfMethodCallChain = startOfMethodCallChain.getFirstChild(); 893 } 894 if (startOfMethodCallChain.getFirstChild() != null) { 895 startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling(); 896 } 897 return startOfMethodCallChain; 898 } 899 900 /** 901 * Checks whether the checked statement is on the previous line ignoring empty lines 902 * and lines which contain only comments. 903 * 904 * @param currentStatement current statement. 905 * @param checkedStatement checked statement. 906 * @return true if checked statement is on the line which is previous to current statement 907 * ignoring empty lines and lines which contain only comments. 908 */ 909 private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement, 910 DetailAST checkedStatement) { 911 DetailAST nextToken = getNextToken(checkedStatement); 912 int distanceAim = 1; 913 if (nextToken != null && isComment(nextToken)) { 914 distanceAim += countEmptyLines(checkedStatement, currentStatement); 915 } 916 917 while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) { 918 if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 919 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo(); 920 } 921 distanceAim++; 922 nextToken = nextToken.getNextSibling(); 923 } 924 return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim; 925 } 926 927 /** 928 * Get the token to start counting the number of lines to add to the distance aim from. 929 * 930 * @param checkedStatement the checked statement. 931 * @return the token to start counting the number of lines to add to the distance aim from. 932 */ 933 private DetailAST getNextToken(DetailAST checkedStatement) { 934 DetailAST nextToken; 935 if (checkedStatement.getType() == TokenTypes.SLIST 936 || checkedStatement.getType() == TokenTypes.ARRAY_INIT 937 || checkedStatement.getType() == TokenTypes.CASE_GROUP) { 938 nextToken = checkedStatement.getFirstChild(); 939 } 940 else { 941 nextToken = checkedStatement.getNextSibling(); 942 } 943 if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) { 944 nextToken = nextToken.getNextSibling(); 945 } 946 return nextToken; 947 } 948 949 /** 950 * Count the number of empty lines between statements. 951 * 952 * @param startStatement start statement. 953 * @param endStatement end statement. 954 * @return the number of empty lines between statements. 955 */ 956 private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) { 957 int emptyLinesNumber = 0; 958 final String[] lines = getLines(); 959 final int endLineNo = endStatement.getLineNo(); 960 for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) { 961 if (CommonUtil.isBlank(lines[lineNo])) { 962 emptyLinesNumber++; 963 } 964 } 965 return emptyLinesNumber; 966 } 967 968 /** 969 * Logs comment which can have the same indentation level as next or previous statement. 970 * 971 * @param comment comment. 972 * @param nextStmt next statement. 973 * @param prevStmt previous statement. 974 */ 975 private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment, 976 DetailAST nextStmt) { 977 final String multilineNoTemplate = "%d, %d"; 978 log(comment, getMessageKey(comment), 979 String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(), 980 nextStmt.getLineNo()), comment.getColumnNo(), 981 String.format(Locale.getDefault(), multilineNoTemplate, 982 getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo()))); 983 } 984 985 /** 986 * Get a message key depending on a comment type. 987 * 988 * @param comment the comment to process. 989 * @return a message key. 990 */ 991 private static String getMessageKey(DetailAST comment) { 992 final String msgKey; 993 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 994 msgKey = MSG_KEY_SINGLE; 995 } 996 else { 997 msgKey = MSG_KEY_BLOCK; 998 } 999 return msgKey; 1000 } 1001 1002 /** 1003 * Gets comment's previous statement from switch block. 1004 * 1005 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 1006 * @return comment's previous statement or null if previous statement is absent. 1007 */ 1008 private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) { 1009 final DetailAST prevStmt; 1010 final DetailAST parentStatement = comment.getParent(); 1011 if (parentStatement.getType() == TokenTypes.CASE_GROUP) { 1012 prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement); 1013 } 1014 else { 1015 prevStmt = getPrevCaseToken(parentStatement); 1016 } 1017 return prevStmt; 1018 } 1019 1020 /** 1021 * Gets previous statement for comment which is placed immediately under case. 1022 * 1023 * @param parentStatement comment's parent statement. 1024 * @return comment's previous statement or null if previous statement is absent. 1025 */ 1026 private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) { 1027 DetailAST prevStmt = null; 1028 final DetailAST prevBlock = parentStatement.getPreviousSibling(); 1029 if (prevBlock.getLastChild() != null) { 1030 DetailAST blockBody = prevBlock.getLastChild().getLastChild(); 1031 if (blockBody.getType() == TokenTypes.SEMI) { 1032 blockBody = blockBody.getPreviousSibling(); 1033 } 1034 if (blockBody.getType() == TokenTypes.EXPR) { 1035 if (isUsingOfObjectReferenceToInvokeMethod(blockBody)) { 1036 prevStmt = findStartTokenOfMethodCallChain(blockBody); 1037 } 1038 else { 1039 prevStmt = blockBody.getFirstChild().getFirstChild(); 1040 } 1041 } 1042 else { 1043 if (blockBody.getType() == TokenTypes.SLIST) { 1044 prevStmt = blockBody.getParent().getParent(); 1045 } 1046 else { 1047 prevStmt = blockBody; 1048 } 1049 } 1050 if (isComment(prevStmt)) { 1051 prevStmt = prevStmt.getNextSibling(); 1052 } 1053 } 1054 return prevStmt; 1055 } 1056 1057 /** 1058 * Gets previous case-token for comment. 1059 * 1060 * @param parentStatement comment's parent statement. 1061 * @return previous case-token or null if previous case-token is absent. 1062 */ 1063 private static DetailAST getPrevCaseToken(DetailAST parentStatement) { 1064 final DetailAST prevCaseToken; 1065 final DetailAST parentBlock = parentStatement.getParent(); 1066 if (parentBlock.getParent() != null 1067 && parentBlock.getParent().getPreviousSibling() != null 1068 && parentBlock.getParent().getPreviousSibling().getType() 1069 == TokenTypes.LITERAL_CASE) { 1070 prevCaseToken = parentBlock.getParent().getPreviousSibling(); 1071 } 1072 else { 1073 prevCaseToken = null; 1074 } 1075 return prevCaseToken; 1076 } 1077 1078 /** 1079 * Checks if comment and next code statement 1080 * (or previous code stmt like <b>case</b> in switch block) are indented at the same level, 1081 * e.g.: 1082 * <p> 1083 * <pre> 1084 * {@code 1085 * // some comment - same indentation level 1086 * int x = 10; 1087 * // some comment - different indentation level 1088 * int x1 = 5; 1089 * /* 1090 * * 1091 * */ 1092 * boolean bool = true; - same indentation level 1093 * } 1094 * </pre> 1095 * </p> 1096 * 1097 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 1098 * @param prevStmt previous code statement. 1099 * @param nextStmt next code statement. 1100 * @return true if comment and next code statement are indented at the same level. 1101 */ 1102 private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt, 1103 DetailAST nextStmt) { 1104 return comment.getColumnNo() == getLineStart(nextStmt.getLineNo()) 1105 || comment.getColumnNo() == getLineStart(prevStmt.getLineNo()); 1106 } 1107 1108 /** 1109 * Get a column number where a code starts. 1110 * 1111 * @param lineNo the line number to get column number in. 1112 * @return the column number where a code starts. 1113 */ 1114 private int getLineStart(int lineNo) { 1115 final char[] line = getLines()[lineNo - 1].toCharArray(); 1116 int lineStart = 0; 1117 while (Character.isWhitespace(line[lineStart])) { 1118 lineStart++; 1119 } 1120 return lineStart; 1121 } 1122 1123 /** 1124 * Checks if current comment is a trailing comment. 1125 * 1126 * @param comment comment to check. 1127 * @return true if current comment is a trailing comment. 1128 */ 1129 private boolean isTrailingComment(DetailAST comment) { 1130 final boolean isTrailingComment; 1131 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 1132 isTrailingComment = isTrailingSingleLineComment(comment); 1133 } 1134 else { 1135 isTrailingComment = isTrailingBlockComment(comment); 1136 } 1137 return isTrailingComment; 1138 } 1139 1140 /** 1141 * Checks if current single line comment is trailing comment, e.g.: 1142 * <p> 1143 * {@code 1144 * double d = 3.14; // some comment 1145 * } 1146 * </p> 1147 * 1148 * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 1149 * @return true if current single line comment is trailing comment. 1150 */ 1151 private boolean isTrailingSingleLineComment(DetailAST singleLineComment) { 1152 final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1); 1153 final int commentColumnNo = singleLineComment.getColumnNo(); 1154 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine); 1155 } 1156 1157 /** 1158 * Checks if current comment block is trailing comment, e.g.: 1159 * <p> 1160 * {@code 1161 * double d = 3.14; /* some comment */ 1162 * /* some comment */ double d = 18.5; 1163 * } 1164 * </p> 1165 * 1166 * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}. 1167 * @return true if current comment block is trailing comment. 1168 */ 1169 private boolean isTrailingBlockComment(DetailAST blockComment) { 1170 final String commentLine = getLine(blockComment.getLineNo() - 1); 1171 final int commentColumnNo = blockComment.getColumnNo(); 1172 final DetailAST nextSibling = blockComment.getNextSibling(); 1173 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine) 1174 || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment); 1175 } 1176 1177 /** 1178 * Checks if the comment is inside a method call with same indentation of 1179 * first expression. e.g: 1180 * <p> 1181 * {@code 1182 * private final boolean myList = someMethod( 1183 * // Some comment here 1184 * s1, 1185 * s2, 1186 * s3 1187 * // ok 1188 * ); 1189 * } 1190 * </p> 1191 * 1192 * @param comment comment to check. 1193 * @return true, if comment is inside inside a method call with same indentation. 1194 */ 1195 private static boolean areInSameMethodCallWithSameIndent(DetailAST comment) { 1196 return comment.getParent() != null 1197 && comment.getParent().getType() == TokenTypes.METHOD_CALL 1198 && comment.getColumnNo() 1199 == getFirstExpressionNodeFromMethodCall(comment.getParent()).getColumnNo(); 1200 } 1201 1202 /** 1203 * Returns the first EXPR DetailAST child from parent of comment. 1204 * 1205 * @param methodCall methodCall DetailAst from which node to be extracted. 1206 * @return first EXPR DetailAST child from parent of comment. 1207 */ 1208 private static DetailAST getFirstExpressionNodeFromMethodCall(DetailAST methodCall) { 1209 // Method call always has ELIST 1210 return methodCall.findFirstToken(TokenTypes.ELIST); 1211 } 1212 1213}