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