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