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