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