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