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-20260409/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 be finished. 559 * 560 * @param comment current statement. 561 * @return previous statement of the comment or null if the comment does not have previous 562 * statement. 563 */ 564 private DetailAST getOneLinePreviousStatement(DetailAST comment) { 565 DetailAST root = comment.getParent(); 566 while (root != null && !isBlockStart(root)) { 567 root = root.getParent(); 568 } 569 570 final Deque<DetailAST> stack = new ArrayDeque<>(); 571 DetailAST previousStatement = null; 572 while (root != null || !stack.isEmpty()) { 573 if (!stack.isEmpty()) { 574 root = stack.pop(); 575 } 576 while (root != null) { 577 previousStatement = findPreviousStatement(comment, root); 578 if (previousStatement != null) { 579 root = null; 580 stack.clear(); 581 break; 582 } 583 if (root.getNextSibling() != null) { 584 stack.push(root.getNextSibling()); 585 } 586 root = root.getFirstChild(); 587 } 588 } 589 return previousStatement; 590 } 591 592 /** 593 * Whether the ast is a comment. 594 * 595 * @param ast the ast to check. 596 * @return true if the ast is a comment. 597 */ 598 private static boolean isComment(DetailAST ast) { 599 final int astType = ast.getType(); 600 return astType == TokenTypes.SINGLE_LINE_COMMENT 601 || astType == TokenTypes.BLOCK_COMMENT_BEGIN 602 || astType == TokenTypes.COMMENT_CONTENT 603 || astType == TokenTypes.BLOCK_COMMENT_END; 604 } 605 606 /** 607 * Whether the AST node starts a block. 608 * 609 * @param root the AST node to check. 610 * @return true if the AST node starts a block. 611 */ 612 private static boolean isBlockStart(DetailAST root) { 613 return root.getType() == TokenTypes.SLIST 614 || root.getType() == TokenTypes.OBJBLOCK 615 || root.getType() == TokenTypes.ARRAY_INIT 616 || root.getType() == TokenTypes.CASE_GROUP; 617 } 618 619 /** 620 * Finds a previous statement of the comment. 621 * Uses root token of the line while searching. 622 * 623 * @param comment comment. 624 * @param root root token of the line. 625 * @return previous statement of the comment or null if previous statement was not found. 626 */ 627 private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) { 628 DetailAST previousStatement = null; 629 if (Math.max(root.getLineNo(), comment.getLineNo()) == root.getLineNo()) { 630 // ATTENTION: parent of the comment is below the comment in case block 631 // See https://github.com/checkstyle/checkstyle/issues/851 632 previousStatement = getPrevStatementFromSwitchBlock(comment); 633 } 634 final DetailAST tokenWhichBeginsTheLine; 635 if (root.getType() == TokenTypes.EXPR) { 636 tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root); 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 the start token of method call chain. 654 * 655 * @param root root token of the line. 656 * @return the start token of method call chain. 657 */ 658 private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) { 659 DetailAST startOfMethodCallChain = root; 660 while (startOfMethodCallChain.getFirstChild() != null 661 && TokenUtil.areOnSameLine(startOfMethodCallChain.getFirstChild(), root)) { 662 startOfMethodCallChain = startOfMethodCallChain.getFirstChild(); 663 } 664 if (startOfMethodCallChain.getFirstChild() != null) { 665 startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling(); 666 } 667 return startOfMethodCallChain; 668 } 669 670 /** 671 * Checks whether the checked statement is on the previous line ignoring empty lines 672 * and lines which contain only comments. 673 * 674 * @param currentStatement current statement. 675 * @param checkedStatement checked statement. 676 * @return true if checked statement is on the line which is previous to current statement 677 * ignoring empty lines and lines which contain only comments. 678 */ 679 private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement, 680 DetailAST checkedStatement) { 681 DetailAST nextToken = getNextToken(checkedStatement); 682 int distanceAim = 1; 683 if (nextToken != null && isComment(nextToken)) { 684 distanceAim += countEmptyLines(checkedStatement, currentStatement); 685 } 686 687 while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) { 688 if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 689 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo(); 690 } 691 distanceAim++; 692 nextToken = nextToken.getNextSibling(); 693 } 694 return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim; 695 } 696 697 /** 698 * Get the token to start counting the number of lines to add to the distance aim from. 699 * 700 * @param checkedStatement the checked statement. 701 * @return the token to start counting the number of lines to add to the distance aim from. 702 */ 703 private DetailAST getNextToken(DetailAST checkedStatement) { 704 DetailAST nextToken; 705 if (checkedStatement.getType() == TokenTypes.SLIST 706 || checkedStatement.getType() == TokenTypes.ARRAY_INIT 707 || checkedStatement.getType() == TokenTypes.CASE_GROUP) { 708 nextToken = checkedStatement.getFirstChild(); 709 } 710 else { 711 nextToken = checkedStatement.getNextSibling(); 712 } 713 if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) { 714 nextToken = nextToken.getNextSibling(); 715 } 716 return nextToken; 717 } 718 719 /** 720 * Count the number of empty lines between statements. 721 * 722 * @param startStatement start statement. 723 * @param endStatement end statement. 724 * @return the number of empty lines between statements. 725 */ 726 private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) { 727 int emptyLinesNumber = 0; 728 final String[] lines = getLines(); 729 final int endLineNo = endStatement.getLineNo(); 730 for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) { 731 if (CommonUtil.isBlank(lines[lineNo])) { 732 emptyLinesNumber++; 733 } 734 } 735 return emptyLinesNumber; 736 } 737 738 /** 739 * Logs comment which can have the same indentation level as next or previous statement. 740 * 741 * @param prevStmt previous statement. 742 * @param comment comment. 743 * @param nextStmt next statement. 744 */ 745 private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment, 746 DetailAST nextStmt) { 747 final String multilineNoTemplate = "%d, %d"; 748 log(comment, getMessageKey(comment), 749 String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(), 750 nextStmt.getLineNo()), comment.getColumnNo(), 751 String.format(Locale.getDefault(), multilineNoTemplate, 752 getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo()))); 753 } 754 755 /** 756 * Get a message key depending on a comment type. 757 * 758 * @param comment the comment to process. 759 * @return a message key. 760 */ 761 private static String getMessageKey(DetailAST comment) { 762 final String msgKey; 763 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 764 msgKey = MSG_KEY_SINGLE; 765 } 766 else { 767 msgKey = MSG_KEY_BLOCK; 768 } 769 return msgKey; 770 } 771 772 /** 773 * Gets comment's previous statement from switch block. 774 * 775 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 776 * @return comment's previous statement or null if previous statement is absent. 777 */ 778 private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) { 779 final DetailAST prevStmt; 780 final DetailAST parentStatement = comment.getParent(); 781 if (parentStatement.getType() == TokenTypes.CASE_GROUP) { 782 prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement); 783 } 784 else { 785 prevStmt = getPrevCaseToken(parentStatement); 786 } 787 return prevStmt; 788 } 789 790 /** 791 * Gets previous statement for comment which is placed immediately under case. 792 * 793 * @param parentStatement comment's parent statement. 794 * @return comment's previous statement or null if previous statement is absent. 795 */ 796 private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) { 797 DetailAST prevStmt = null; 798 final DetailAST prevBlock = parentStatement.getPreviousSibling(); 799 if (prevBlock.getLastChild() != null) { 800 DetailAST blockBody = prevBlock.getLastChild().getLastChild(); 801 if (blockBody.getType() == TokenTypes.SEMI) { 802 blockBody = blockBody.getPreviousSibling(); 803 } 804 if (blockBody.getType() == TokenTypes.EXPR) { 805 prevStmt = findStartTokenOfMethodCallChain(blockBody); 806 } 807 else { 808 if (blockBody.getType() == TokenTypes.SLIST) { 809 prevStmt = blockBody.getParent().getParent(); 810 } 811 else { 812 prevStmt = blockBody; 813 } 814 } 815 if (isComment(prevStmt)) { 816 prevStmt = prevStmt.getNextSibling(); 817 } 818 } 819 return prevStmt; 820 } 821 822 /** 823 * Gets previous case-token for comment. 824 * 825 * @param parentStatement comment's parent statement. 826 * @return previous case-token or null if previous case-token is absent. 827 */ 828 private static DetailAST getPrevCaseToken(DetailAST parentStatement) { 829 final DetailAST prevCaseToken; 830 final DetailAST parentBlock = parentStatement.getParent(); 831 if (parentBlock.getParent().getPreviousSibling() != null 832 && parentBlock.getParent().getPreviousSibling().getType() 833 == TokenTypes.LITERAL_CASE) { 834 prevCaseToken = parentBlock.getParent().getPreviousSibling(); 835 } 836 else { 837 prevCaseToken = null; 838 } 839 return prevCaseToken; 840 } 841 842 /** 843 * Checks if comment and next code statement 844 * (or previous code stmt like <b>case</b> in switch block) are indented at the same level, 845 * e.g.: 846 * <pre> 847 * {@code 848 * // some comment - same indentation level 849 * int x = 10; 850 * // some comment - different indentation level 851 * int x1 = 5; 852 * /* 853 * * 854 * * / 855 * boolean bool = true; - same indentation level 856 * } 857 * </pre> 858 * 859 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 860 * @param prevStmt previous code statement. 861 * @param nextStmt next code statement. 862 * @return true if comment and next code statement are indented at the same level. 863 */ 864 private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt, 865 DetailAST nextStmt) { 866 return comment.getColumnNo() == getLineStart(nextStmt.getLineNo()) 867 || comment.getColumnNo() == getLineStart(prevStmt.getLineNo()); 868 } 869 870 /** 871 * Get a column number where a code starts. 872 * 873 * @param lineNo the line number to get column number in. 874 * @return the column number where a code starts. 875 */ 876 private int getLineStart(int lineNo) { 877 final char[] line = getLines()[lineNo - 1].toCharArray(); 878 int lineStart = 0; 879 while (Character.isWhitespace(line[lineStart])) { 880 lineStart++; 881 } 882 return lineStart; 883 } 884 885 /** 886 * Checks if current comment is a trailing comment. 887 * 888 * @param comment comment to check. 889 * @return true if current comment is a trailing comment. 890 */ 891 private boolean isTrailingComment(DetailAST comment) { 892 final boolean isTrailingComment; 893 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 894 isTrailingComment = isTrailingSingleLineComment(comment); 895 } 896 else { 897 isTrailingComment = isTrailingBlockComment(comment); 898 } 899 return isTrailingComment; 900 } 901 902 /** 903 * Checks if current single-line comment is trailing comment, e.g.: 904 * 905 * <p> 906 * {@code 907 * double d = 3.14; // some comment 908 * } 909 * </p> 910 * 911 * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 912 * @return true if current single-line comment is trailing comment. 913 */ 914 private boolean isTrailingSingleLineComment(DetailAST singleLineComment) { 915 final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1); 916 final int commentColumnNo = singleLineComment.getColumnNo(); 917 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, targetSourceLine); 918 } 919 920 /** 921 * Checks if current comment block is trailing comment, e.g.: 922 * 923 * <p> 924 * {@code 925 * double d = 3.14; /* some comment * / 926 * /* some comment * / double d = 18.5; 927 * } 928 * </p> 929 * 930 * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}. 931 * @return true if current comment block is trailing comment. 932 */ 933 private boolean isTrailingBlockComment(DetailAST blockComment) { 934 final String commentLine = getLine(blockComment.getLineNo() - 1); 935 final int commentColumnNo = blockComment.getColumnNo(); 936 final DetailAST nextSibling = blockComment.getNextSibling(); 937 return !CommonUtil.hasWhitespaceBefore(commentColumnNo, commentLine) 938 || nextSibling != null && TokenUtil.areOnSameLine(nextSibling, blockComment); 939 } 940 941 /** 942 * Checks if the comment is inside a method call with same indentation of first expression. e.g: 943 * 944 * <p> 945 * {@code 946 * private final boolean myList = someMethod( 947 * // Some comment here 948 * s1, 949 * s2, 950 * s3 951 * // ok 952 * ); 953 * } 954 * </p> 955 * 956 * @param comment comment to check. 957 * @return true, if comment is inside a method call with same indentation. 958 */ 959 private static boolean areInSameMethodCallWithSameIndent(DetailAST comment) { 960 return comment.getParent().getType() == TokenTypes.METHOD_CALL 961 && comment.getColumnNo() 962 == getFirstExpressionNodeFromMethodCall(comment.getParent()).getColumnNo(); 963 } 964 965 /** 966 * Returns the first EXPR DetailAST child from parent of comment. 967 * 968 * @param methodCall methodCall DetailAst from which node to be extracted. 969 * @return first EXPR DetailAST child from parent of comment. 970 */ 971 private static DetailAST getFirstExpressionNodeFromMethodCall(DetailAST methodCall) { 972 // Method call always has ELIST 973 return methodCall.findFirstToken(TokenTypes.ELIST); 974 } 975 976}