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