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