001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2017 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.api.AbstractCheck; 027import com.puppycrawl.tools.checkstyle.api.DetailAST; 028import com.puppycrawl.tools.checkstyle.api.TokenTypes; 029import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 030 031/** 032 * This Check controls the indentation between comments and surrounding code. 033 * Comments are indented at the same level as the surrounding code. 034 * Detailed info about such convention can be found 035 * <a href= 036 * "http://checkstyle.sourceforge.net/reports/google-java-style-20170228.html#s4.8.6.1-block-comment-style"> 037 * here</a> 038 * <p> 039 * Examples: 040 * </p> 041 * <p> 042 * To configure the Check: 043 * </p> 044 * 045 * <pre> 046 * {@code 047 * <module name="CommentsIndentation"/> 048 * } 049 * {@code 050 * /* 051 * * comment 052 * * some comment 053 * */ 054 * boolean bool = true; - such comment indentation is ok 055 * /* 056 * * comment 057 * * some comment 058 * */ 059 * double d = 3.14; - Block Comment has incorrect indentation level 7, expected 4. 060 * // some comment - comment is ok 061 * String str = ""; 062 * // some comment Comment has incorrect indentation level 8, expected 4. 063 * String str1 = ""; 064 * } 065 * </pre> 066 * 067 * @author <a href="mailto:nesterenko-aleksey@list.ru">Aleksey Nesterenko</a> 068 * @author <a href="mailto:andreyselkin@gmail.com">Andrei Selkin</a> 069 */ 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 CommonUtils.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 424 if (comment.getColumnNo() < prevStmt.getColumnNo() 425 || comment.getColumnNo() < nextStmt.getColumnNo()) { 426 logMultilineIndentation(prevStmt, comment, nextStmt); 427 } 428 } 429 430 /** 431 * Handles 'fall through' single line comment. 432 * Note, 'fall through' and similar comments can have indentation level as next or previous 433 * statement. 434 * For example: 435 * <p> 436 * {@code 437 * ... 438 * case OPTION_ONE: 439 * int someVariable = 1; 440 * // fall through - OK 441 * case OPTION_TWO: 442 * int a = 5; 443 * break; 444 * ... 445 * } 446 * </p> 447 * <p> 448 * {@code 449 * ... 450 * case OPTION_ONE: 451 * int someVariable = 1; 452 * // than init variable a - OK 453 * case OPTION_TWO: 454 * int a = 5; 455 * break; 456 * ... 457 * } 458 * </p> 459 * @param prevStmt previous statement. 460 * @param comment single line comment. 461 * @param nextStmt next statement. 462 */ 463 private void handleFallThroughComment(DetailAST prevStmt, DetailAST comment, 464 DetailAST nextStmt) { 465 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 /** 513 * Whether the comment might have been used for the next block in a multi-block structure. 514 * @param endBlockStmt the end of the current block. 515 * @return true, if the comment might have been used for the next 516 * block in a multi-block structure. 517 */ 518 private static boolean isCommentForMultiblock(DetailAST endBlockStmt) { 519 final DetailAST nextBlock = endBlockStmt.getParent().getNextSibling(); 520 final int endBlockLineNo = endBlockStmt.getLineNo(); 521 final DetailAST catchAst = endBlockStmt.getParent().getParent(); 522 final DetailAST finallyAst = catchAst.getNextSibling(); 523 return nextBlock != null && nextBlock.getLineNo() == endBlockLineNo 524 || finallyAst != null 525 && catchAst.getType() == TokenTypes.LITERAL_CATCH 526 && finallyAst.getLineNo() == endBlockLineNo; 527 } 528 529 /** 530 * Handles a comment which is placed within the empty code block. 531 * Note, if comment is placed at the end of the empty code block, we have Checkstyle's 532 * limitations to clearly detect user intention of explanation target - above or below. The 533 * only case we can assume as a violation is when a single line comment within the empty 534 * code block has indentation level that is lower than the indentation level of the closing 535 * right curly brace. For example: 536 * <p> 537 * {@code 538 * if (a == true) { 539 * // violation 540 * } 541 * } 542 * </p> 543 * 544 * @param comment comment to check. 545 * @param nextStmt next statement. 546 */ 547 private void handleCommentInEmptyCodeBlock(DetailAST comment, DetailAST nextStmt) { 548 if (comment.getColumnNo() < nextStmt.getColumnNo()) { 549 log(comment.getLineNo(), getMessageKey(comment), nextStmt.getLineNo(), 550 comment.getColumnNo(), nextStmt.getColumnNo()); 551 } 552 } 553 554 /** 555 * Does pre-order traverse of abstract syntax tree to find the previous statement of the 556 * comment. If previous statement of the comment is found, then the traverse will 557 * be finished. 558 * @param comment current statement. 559 * @return previous statement of the comment or null if the comment does not have previous 560 * statement. 561 */ 562 private DetailAST getOneLinePreviousStatement(DetailAST comment) { 563 DetailAST root = comment.getParent(); 564 while (root != null && !isBlockStart(root)) { 565 root = root.getParent(); 566 } 567 568 final Deque<DetailAST> stack = new ArrayDeque<>(); 569 DetailAST previousStatement = null; 570 while (root != null || !stack.isEmpty()) { 571 if (!stack.isEmpty()) { 572 root = stack.pop(); 573 } 574 while (root != null) { 575 previousStatement = findPreviousStatement(comment, root); 576 if (previousStatement != null) { 577 root = null; 578 stack.clear(); 579 break; 580 } 581 if (root.getNextSibling() != null) { 582 stack.push(root.getNextSibling()); 583 } 584 root = root.getFirstChild(); 585 } 586 } 587 return previousStatement; 588 } 589 590 /** 591 * Whether the ast is a comment. 592 * @param ast the ast to check. 593 * @return true if the ast is a comment. 594 */ 595 private static boolean isComment(DetailAST ast) { 596 final int astType = ast.getType(); 597 return astType == TokenTypes.SINGLE_LINE_COMMENT 598 || astType == TokenTypes.BLOCK_COMMENT_BEGIN 599 || astType == TokenTypes.COMMENT_CONTENT 600 || astType == TokenTypes.BLOCK_COMMENT_END; 601 } 602 603 /** 604 * Whether the AST node starts a block. 605 * @param root the AST node to check. 606 * @return true if the AST node starts a block. 607 */ 608 private static boolean isBlockStart(DetailAST root) { 609 return root.getType() == TokenTypes.SLIST 610 || root.getType() == TokenTypes.OBJBLOCK 611 || root.getType() == TokenTypes.ARRAY_INIT 612 || root.getType() == TokenTypes.CASE_GROUP; 613 } 614 615 /** 616 * Finds a previous statement of the comment. 617 * Uses root token of the line while searching. 618 * @param comment comment. 619 * @param root root token of the line. 620 * @return previous statement of the comment or null if previous statement was not found. 621 */ 622 private DetailAST findPreviousStatement(DetailAST comment, DetailAST root) { 623 DetailAST previousStatement = null; 624 if (root.getLineNo() >= comment.getLineNo()) { 625 // ATTENTION: parent of the comment is below the comment in case block 626 // See https://github.com/checkstyle/checkstyle/issues/851 627 previousStatement = getPrevStatementFromSwitchBlock(comment); 628 } 629 final DetailAST tokenWhichBeginsTheLine; 630 if (root.getType() == TokenTypes.EXPR 631 && root.getFirstChild().getFirstChild() != null) { 632 if (root.getFirstChild().getType() == TokenTypes.LITERAL_NEW) { 633 tokenWhichBeginsTheLine = root.getFirstChild(); 634 } 635 else { 636 tokenWhichBeginsTheLine = findTokenWhichBeginsTheLine(root); 637 } 638 } 639 else if (root.getType() == TokenTypes.PLUS) { 640 tokenWhichBeginsTheLine = root.getFirstChild(); 641 } 642 else { 643 tokenWhichBeginsTheLine = root; 644 } 645 if (tokenWhichBeginsTheLine != null 646 && !isComment(tokenWhichBeginsTheLine) 647 && isOnPreviousLineIgnoringComments(comment, tokenWhichBeginsTheLine)) { 648 previousStatement = tokenWhichBeginsTheLine; 649 } 650 return previousStatement; 651 } 652 653 /** 654 * Finds a token which begins the line. 655 * @param root root token of the line. 656 * @return token which begins the line. 657 */ 658 private static DetailAST findTokenWhichBeginsTheLine(DetailAST root) { 659 final DetailAST tokenWhichBeginsTheLine; 660 if (isUsingOfObjectReferenceToInvokeMethod(root)) { 661 tokenWhichBeginsTheLine = findStartTokenOfMethodCallChain(root); 662 } 663 else { 664 tokenWhichBeginsTheLine = root.getFirstChild().findFirstToken(TokenTypes.IDENT); 665 } 666 return tokenWhichBeginsTheLine; 667 } 668 669 /** 670 * Checks whether there is a use of an object reference to invoke an object's method on line. 671 * @param root root token of the line. 672 * @return true if there is a use of an object reference to invoke an object's method on line. 673 */ 674 private static boolean isUsingOfObjectReferenceToInvokeMethod(DetailAST root) { 675 return root.getFirstChild().getFirstChild().getFirstChild() != null 676 && root.getFirstChild().getFirstChild().getFirstChild().getNextSibling() != null; 677 } 678 679 /** 680 * Finds the start token of method call chain. 681 * @param root root token of the line. 682 * @return the start token of method call chain. 683 */ 684 private static DetailAST findStartTokenOfMethodCallChain(DetailAST root) { 685 DetailAST startOfMethodCallChain = root; 686 while (startOfMethodCallChain.getFirstChild() != null 687 && startOfMethodCallChain.getFirstChild().getLineNo() == root.getLineNo()) { 688 startOfMethodCallChain = startOfMethodCallChain.getFirstChild(); 689 } 690 if (startOfMethodCallChain.getFirstChild() != null) { 691 startOfMethodCallChain = startOfMethodCallChain.getFirstChild().getNextSibling(); 692 } 693 return startOfMethodCallChain; 694 } 695 696 /** 697 * Checks whether the checked statement is on the previous line ignoring empty lines 698 * and lines which contain only comments. 699 * @param currentStatement current statement. 700 * @param checkedStatement checked statement. 701 * @return true if checked statement is on the line which is previous to current statement 702 * ignoring empty lines and lines which contain only comments. 703 */ 704 private boolean isOnPreviousLineIgnoringComments(DetailAST currentStatement, 705 DetailAST checkedStatement) { 706 DetailAST nextToken = getNextToken(checkedStatement); 707 int distanceAim = 1; 708 if (nextToken != null && isComment(nextToken)) { 709 distanceAim += countEmptyLines(checkedStatement, currentStatement); 710 } 711 712 while (nextToken != null && nextToken != currentStatement && isComment(nextToken)) { 713 if (nextToken.getType() == TokenTypes.BLOCK_COMMENT_BEGIN) { 714 distanceAim += nextToken.getLastChild().getLineNo() - nextToken.getLineNo(); 715 } 716 distanceAim++; 717 nextToken = nextToken.getNextSibling(); 718 } 719 return currentStatement.getLineNo() - checkedStatement.getLineNo() == distanceAim; 720 } 721 722 /** 723 * Get the token to start counting the number of lines to add to the distance aim from. 724 * @param checkedStatement the checked statement. 725 * @return the token to start counting the number of lines to add to the distance aim from. 726 */ 727 private DetailAST getNextToken(DetailAST checkedStatement) { 728 DetailAST nextToken; 729 if (checkedStatement.getType() == TokenTypes.SLIST 730 || checkedStatement.getType() == TokenTypes.ARRAY_INIT 731 || checkedStatement.getType() == TokenTypes.CASE_GROUP) { 732 nextToken = checkedStatement.getFirstChild(); 733 } 734 else { 735 nextToken = checkedStatement.getNextSibling(); 736 } 737 if (nextToken != null && isComment(nextToken) && isTrailingComment(nextToken)) { 738 nextToken = nextToken.getNextSibling(); 739 } 740 return nextToken; 741 } 742 743 /** 744 * Count the number of empty lines between statements. 745 * @param startStatement start statement. 746 * @param endStatement end statement. 747 * @return the number of empty lines between statements. 748 */ 749 private int countEmptyLines(DetailAST startStatement, DetailAST endStatement) { 750 int emptyLinesNumber = 0; 751 final String[] lines = getLines(); 752 final int endLineNo = endStatement.getLineNo(); 753 for (int lineNo = startStatement.getLineNo(); lineNo < endLineNo; lineNo++) { 754 if (CommonUtils.isBlank(lines[lineNo])) { 755 emptyLinesNumber++; 756 } 757 } 758 return emptyLinesNumber; 759 } 760 761 /** 762 * Logs comment which can have the same indentation level as next or previous statement. 763 * @param comment comment. 764 * @param nextStmt next statement. 765 * @param prevStmt previous statement. 766 */ 767 private void logMultilineIndentation(DetailAST prevStmt, DetailAST comment, 768 DetailAST nextStmt) { 769 final String multilineNoTemplate = "%d, %d"; 770 log(comment.getLineNo(), getMessageKey(comment), 771 String.format(Locale.getDefault(), multilineNoTemplate, prevStmt.getLineNo(), 772 nextStmt.getLineNo()), comment.getColumnNo(), 773 String.format(Locale.getDefault(), multilineNoTemplate, 774 getLineStart(prevStmt.getLineNo()), getLineStart(nextStmt.getLineNo()))); 775 } 776 777 /** 778 * Get a message key depending on a comment type. 779 * @param comment the comment to process. 780 * @return a message key. 781 */ 782 private static String getMessageKey(DetailAST comment) { 783 final String msgKey; 784 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 785 msgKey = MSG_KEY_SINGLE; 786 } 787 else { 788 msgKey = MSG_KEY_BLOCK; 789 } 790 return msgKey; 791 } 792 793 /** 794 * Gets comment's previous statement from switch block. 795 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single-line comment}. 796 * @return comment's previous statement or null if previous statement is absent. 797 */ 798 private static DetailAST getPrevStatementFromSwitchBlock(DetailAST comment) { 799 final DetailAST prevStmt; 800 final DetailAST parentStatement = comment.getParent(); 801 if (parentStatement.getType() == TokenTypes.CASE_GROUP) { 802 prevStmt = getPrevStatementWhenCommentIsUnderCase(parentStatement); 803 } 804 else { 805 prevStmt = getPrevCaseToken(parentStatement); 806 } 807 return prevStmt; 808 } 809 810 /** 811 * Gets previous statement for comment which is placed immediately under case. 812 * @param parentStatement comment's parent statement. 813 * @return comment's previous statement or null if previous statement is absent. 814 */ 815 private static DetailAST getPrevStatementWhenCommentIsUnderCase(DetailAST parentStatement) { 816 DetailAST prevStmt = null; 817 final DetailAST prevBlock = parentStatement.getPreviousSibling(); 818 if (prevBlock.getLastChild() != null) { 819 DetailAST blockBody = prevBlock.getLastChild().getLastChild(); 820 if (blockBody.getType() == TokenTypes.SEMI) { 821 blockBody = blockBody.getPreviousSibling(); 822 } 823 if (blockBody.getType() == TokenTypes.EXPR) { 824 if (isUsingOfObjectReferenceToInvokeMethod(blockBody)) { 825 prevStmt = findStartTokenOfMethodCallChain(blockBody); 826 } 827 else { 828 prevStmt = blockBody.getFirstChild().getFirstChild(); 829 } 830 } 831 else { 832 if (blockBody.getType() == TokenTypes.SLIST) { 833 prevStmt = blockBody.getParent().getParent(); 834 } 835 else { 836 prevStmt = blockBody; 837 } 838 } 839 if (isComment(prevStmt)) { 840 prevStmt = prevStmt.getNextSibling(); 841 } 842 } 843 return prevStmt; 844 } 845 846 /** 847 * Gets previous case-token for comment. 848 * @param parentStatement comment's parent statement. 849 * @return previous case-token or null if previous case-token is absent. 850 */ 851 private static DetailAST getPrevCaseToken(DetailAST parentStatement) { 852 final DetailAST prevCaseToken; 853 final DetailAST parentBlock = parentStatement.getParent(); 854 if (parentBlock.getParent() != null 855 && parentBlock.getParent().getPreviousSibling() != null 856 && parentBlock.getParent().getPreviousSibling().getType() 857 == TokenTypes.LITERAL_CASE) { 858 prevCaseToken = parentBlock.getParent().getPreviousSibling(); 859 } 860 else { 861 prevCaseToken = null; 862 } 863 return prevCaseToken; 864 } 865 866 /** 867 * Checks if comment and next code statement 868 * (or previous code stmt like <b>case</b> in switch block) are indented at the same level, 869 * e.g.: 870 * <p> 871 * <pre> 872 * {@code 873 * // some comment - same indentation level 874 * int x = 10; 875 * // some comment - different indentation level 876 * int x1 = 5; 877 * /* 878 * * 879 * */ 880 * boolean bool = true; - same indentation level 881 * } 882 * </pre> 883 * </p> 884 * @param comment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 885 * @param prevStmt previous code statement. 886 * @param nextStmt next code statement. 887 * @return true if comment and next code statement are indented at the same level. 888 */ 889 private boolean areSameLevelIndented(DetailAST comment, DetailAST prevStmt, 890 DetailAST nextStmt) { 891 892 return comment.getColumnNo() == getLineStart(nextStmt.getLineNo()) 893 || comment.getColumnNo() == getLineStart(prevStmt.getLineNo()); 894 } 895 896 /** 897 * Get a column number where a code starts. 898 * @param lineNo the line number to get column number in. 899 * @return the column number where a code starts. 900 */ 901 private int getLineStart(int lineNo) { 902 final char[] line = getLines()[lineNo - 1].toCharArray(); 903 int lineStart = 0; 904 while (Character.isWhitespace(line[lineStart])) { 905 lineStart++; 906 } 907 return lineStart; 908 } 909 910 /** 911 * Checks if current comment is a trailing comment. 912 * @param comment comment to check. 913 * @return true if current comment is a trailing comment. 914 */ 915 private boolean isTrailingComment(DetailAST comment) { 916 final boolean isTrailingComment; 917 if (comment.getType() == TokenTypes.SINGLE_LINE_COMMENT) { 918 isTrailingComment = isTrailingSingleLineComment(comment); 919 } 920 else { 921 isTrailingComment = isTrailingBlockComment(comment); 922 } 923 return isTrailingComment; 924 } 925 926 /** 927 * Checks if current single line comment is trailing comment, e.g.: 928 * <p> 929 * {@code 930 * double d = 3.14; // some comment 931 * } 932 * </p> 933 * @param singleLineComment {@link TokenTypes#SINGLE_LINE_COMMENT single line comment}. 934 * @return true if current single line comment is trailing comment. 935 */ 936 private boolean isTrailingSingleLineComment(DetailAST singleLineComment) { 937 final String targetSourceLine = getLine(singleLineComment.getLineNo() - 1); 938 final int commentColumnNo = singleLineComment.getColumnNo(); 939 return !CommonUtils.hasWhitespaceBefore(commentColumnNo, targetSourceLine); 940 } 941 942 /** 943 * Checks if current comment block is trailing comment, e.g.: 944 * <p> 945 * {@code 946 * double d = 3.14; /* some comment */ 947 * /* some comment */ double d = 18.5; 948 * } 949 * </p> 950 * @param blockComment {@link TokenTypes#BLOCK_COMMENT_BEGIN block comment begin}. 951 * @return true if current comment block is trailing comment. 952 */ 953 private boolean isTrailingBlockComment(DetailAST blockComment) { 954 final String commentLine = getLine(blockComment.getLineNo() - 1); 955 final int commentColumnNo = blockComment.getColumnNo(); 956 final DetailAST nextSibling = blockComment.getNextSibling(); 957 return !CommonUtils.hasWhitespaceBefore(commentColumnNo, commentLine) 958 || nextSibling != null && nextSibling.getLineNo() == blockComment.getLineNo(); 959 } 960}