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